Files
grafana/public/app/features/alerting/unified/RuleList.test.tsx

901 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { SerializedError } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
import { SetupServer } from 'msw/node';
import { TestProvider } from 'test/helpers/TestProvider';
import { render, screen, waitFor, within } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { PluginExtensionTypes } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourceSrv, locationService, setAppEvents, setDataSourceSrv, usePluginLinks } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import { mockUserApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure';
import * as actions from 'app/features/alerting/unified/state/actions';
import { getMockUser } from 'app/features/users/__mocks__/userMocks';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto';
import * as analytics from './Analytics';
import RuleList from './RuleList';
import { discoverFeatures } from './api/buildInfo';
import { fetchRules } from './api/prometheus';
import * as apiRuler from './api/ruler';
import { fetchRulerRules } from './api/ruler';
import {
MockDataSourceSrv,
getPotentiallyPausedRulerRules,
grantUserPermissions,
mockDataSource,
mockPromAlert,
mockPromAlertingRule,
mockPromRecordingRule,
mockPromRuleGroup,
mockPromRuleNamespace,
pausedPromRules,
somePromRules,
someRulerRules,
} from './mocks';
import { setupPluginsExtensionsHook } from './testSetup/plugins';
import * as config from './utils/config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getPluginLinkExtensions: jest.fn(),
usePluginLinks: jest.fn(),
useReturnToPrevious: jest.fn(),
}));
jest.mock('./api/buildInfo');
jest.mock('./api/prometheus');
jest.mock('./api/ruler');
jest.mock('../../../core/hooks/useMediaQueryChange');
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
jest.spyOn(analytics, 'logInfo');
jest.spyOn(config, 'getAllDataSources');
jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]);
jest.spyOn(apiRuler, 'rulerUrlBuilder');
setAppEvents(appEvents);
setupPluginsExtensionsHook();
const mocks = {
getAllDataSourcesMock: jest.mocked(config.getAllDataSources),
usePluginLinksMock: jest.mocked(usePluginLinks),
rulesInSameGroupHaveInvalidForMock: jest.mocked(actions.rulesInSameGroupHaveInvalidFor),
api: {
discoverFeatures: jest.mocked(discoverFeatures),
fetchRules: jest.mocked(fetchRules),
fetchRulerRules: jest.mocked(fetchRulerRules),
rulerBuilderMock: jest.mocked(apiRuler.rulerUrlBuilder),
},
};
const renderRuleList = () => {
locationService.push('/');
return render(
<TestProvider>
<RuleList />
</TestProvider>,
{ renderWithRouter: false }
);
};
const dataSources = {
prom: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
}),
promdisabled: mockDataSource({
name: 'Prometheus-disabled',
type: DataSourceType.Prometheus,
jsonData: {
manageAlerts: false,
},
}),
loki: mockDataSource({
name: 'Loki',
type: DataSourceType.Loki,
}),
promBroken: mockDataSource({
name: 'Prometheus-broken',
type: DataSourceType.Prometheus,
}),
};
const ui = {
ruleGroup: byTestId('rule-group'),
pausedRuleGroup: byText(/groupPaused/),
cloudRulesSourceErrors: byTestId('cloud-rulessource-errors'),
groupCollapseToggle: byTestId(selectors.components.AlertRules.groupToggle),
ruleCollapseToggle: byTestId(selectors.components.AlertRules.toggle),
rulesTable: byTestId('rules-table'),
ruleRow: byTestId('row'),
expandedContent: byTestId(selectors.components.AlertRules.expandedContent),
rulesFilterInput: byTestId('search-query-input'),
moreErrorsButton: byRole('button', { name: /more errors/ }),
editCloudGroupIcon: byTestId('edit-group'),
newRuleButton: byText(/new alert rule/i),
exportButton: byText(/export rules/i),
editGroupModal: {
dialog: byRole('dialog'),
namespaceInput: byRole('textbox', { name: /^Namespace/ }),
ruleGroupInput: byRole('textbox', { name: /Evaluation group/ }),
intervalInput: byRole('textbox', {
name: /Evaluation interval How often is the rule evaluated. Applies to every rule within the group./i,
}),
saveButton: byRole('button', { name: /Save/ }),
},
stateTags: {
paused: byText(/^Paused/),
},
actionButtons: {
more: byRole('button', { name: /More/ }),
},
moreActionItems: {
resume: byRole('menuitem', { name: /resume evaluation/i }),
},
};
const server = setupMswServer();
const configureMockServer = (server: SetupServer) => {
mockUserApi(server).user(getMockUser());
setAlertmanagerChoices(AlertmanagerChoice.All, 1);
};
describe('RuleList', () => {
beforeEach(() => {
configureMockServer(server);
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.rulesInSameGroupHaveInvalidForMock.mockReturnValue([]);
mocks.usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-ml-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Run investigation',
category: 'Sift',
description: 'Run a Sift investigation for this alert',
onClick: jest.fn(),
},
],
isLoading: false,
});
});
afterEach(() => {
jest.resetAllMocks();
setDataSourceSrv(undefined as unknown as DataSourceSrv);
});
it('load & show rule groups from multiple cloud data sources', async () => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Prometheus,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === dataSources.prom.name) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'default',
dataSourceName: dataSources.prom.name,
groups: [
mockPromRuleGroup({
name: 'group-2',
}),
mockPromRuleGroup({
name: 'group-1',
}),
],
}),
]);
} else if (dataSourceName === dataSources.loki.name) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'default',
dataSourceName: dataSources.loki.name,
groups: [
mockPromRuleGroup({
name: 'group-1',
}),
],
}),
mockPromRuleNamespace({
name: 'lokins',
dataSourceName: dataSources.loki.name,
groups: [
mockPromRuleGroup({
name: 'group-1',
}),
],
}),
]);
} else if (dataSourceName === dataSources.promBroken.name) {
return Promise.reject({ message: 'this datasource is broken' } as SerializedError);
} else if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'foofolder',
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
groups: [
mockPromRuleGroup({
name: 'grafana-group',
rules: [
mockPromAlertingRule({
query: '[]',
}),
],
}),
],
}),
]);
}
return Promise.reject(new Error(`unexpected datasourceName: ${dataSourceName}`));
});
mocks.api.fetchRulerRules.mockRejectedValue({ status: 500, data: { message: 'Server error' } });
await renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(4));
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(5);
expect(groups[0]).toHaveTextContent('foofolder');
expect(groups[1]).toHaveTextContent('default group-1');
expect(groups[2]).toHaveTextContent('default group-1');
expect(groups[3]).toHaveTextContent('default group-2');
expect(groups[4]).toHaveTextContent('lokins group-1');
const errors = await ui.cloudRulesSourceErrors.find();
expect(errors).not.toHaveTextContent(
'Failed to load rules state from Prometheus-broken: this datasource is broken'
);
await userEvent.click(ui.moreErrorsButton.get());
expect(errors).toHaveTextContent('Failed to load rules state from Prometheus-broken: this datasource is broken');
});
it('expand rule group, rule and alert details', async () => {
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([]);
} else {
return Promise.resolve([
mockPromRuleNamespace({
groups: [
mockPromRuleGroup({
name: 'group-1',
}),
mockPromRuleGroup({
name: 'group-2',
rules: [
mockPromRecordingRule({
name: 'recordingrule',
}),
mockPromAlertingRule({
name: 'alertingrule',
labels: {
severity: 'warning',
foo: 'bar',
},
query: 'topk(5, foo)[5m]',
annotations: {
message: 'great alert',
},
alerts: [
mockPromAlert({
labels: {
foo: 'bar',
severity: 'warning',
},
value: '2e+10',
annotations: {
message: 'first alert message',
},
}),
mockPromAlert({
labels: {
foo: 'baz',
severity: 'error',
},
value: '3e+11',
annotations: {
message: 'first alert message',
},
}),
],
}),
mockPromAlertingRule({
name: 'p-rule',
alerts: [],
state: PromAlertingRuleState.Pending,
}),
mockPromAlertingRule({
name: 'i-rule',
alerts: [],
state: PromAlertingRuleState.Inactive,
}),
],
}),
],
}),
]);
}
});
await renderRuleList();
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(2);
await waitFor(() => expect(groups[0]).toHaveTextContent(/firing|pending|normal/));
await waitFor(() => expect(groups[1]).toHaveTextContent(/firing|pending|normal/));
expect(groups[0]).toHaveTextContent('1 firing');
expect(groups[1]).toHaveTextContent('1 firing');
expect(groups[1]).toHaveTextContent('1 pending');
expect(groups[1]).toHaveTextContent('1 recording');
expect(groups[1]).toHaveTextContent('1 normal');
// expand second group to see rules table
expect(ui.rulesTable.query()).not.toBeInTheDocument();
await userEvent.click(ui.groupCollapseToggle.get(groups[1]));
const table = await ui.rulesTable.find(groups[1]);
// check that rule rows are rendered properly
const ruleRows = ui.ruleRow.getAll(table);
expect(ruleRows).toHaveLength(4);
expect(ruleRows[0]).toHaveTextContent('Recording rule');
expect(ruleRows[0]).toHaveTextContent('recordingrule');
expect(ruleRows[1]).toHaveTextContent('Firing');
expect(ruleRows[1]).toHaveTextContent('alertingrule');
expect(ruleRows[2]).toHaveTextContent('Pending');
expect(ruleRows[2]).toHaveTextContent('p-rule');
expect(ruleRows[3]).toHaveTextContent('Normal');
expect(ruleRows[3]).toHaveTextContent('i-rule');
expect(byText('Labels').query()).not.toBeInTheDocument();
// expand alert details
await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[1]));
const ruleDetails = ui.expandedContent.get(ruleRows[1]);
const labels = byTestId('label-value').getAll(ruleDetails);
expect(labels[0]).toHaveTextContent('severitywarning');
expect(labels[1]).toHaveTextContent('foobar');
expect(ruleDetails).toHaveTextContent('Expressiontopk ( 5 , foo ) [ 5m ]');
expect(ruleDetails).toHaveTextContent('messagegreat alert');
expect(ruleDetails).toHaveTextContent('Instances');
// finally, check instances table
const instancesTable = byTestId('dynamic-table').get(ruleDetails);
expect(instancesTable).toBeInTheDocument();
const instanceRows = byTestId('row').getAll(instancesTable);
expect(instanceRows).toHaveLength(2);
expect(instanceRows![0]).toHaveTextContent('Firingfoobarseveritywarning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firingfoobazseverityerror2021-03-18 08:47:05');
// expand details of an instance
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));
const alertDetails = byTestId(selectors.components.AlertRules.expandedContent).get(instanceRows[0]);
expect(alertDetails).toHaveTextContent('Value2e+10');
expect(alertDetails).toHaveTextContent('messagefirst alert message');
// collapse everything again
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));
expect(byTestId(selectors.components.AlertRules.expandedContent).query(instanceRows[0])).not.toBeInTheDocument();
await userEvent.click(ui.ruleCollapseToggle.getAll(ruleRows[1])[0]);
await userEvent.click(ui.groupCollapseToggle.get(groups[1]));
expect(ui.rulesTable.query()).not.toBeInTheDocument();
});
it('filters rules and alerts by labels', async () => {
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRulerRules.mockResolvedValue({});
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([]);
} else {
return Promise.resolve([
mockPromRuleNamespace({
groups: [
mockPromRuleGroup({
name: 'group-1',
rules: [
mockPromAlertingRule({
name: 'alertingrule',
labels: {
severity: 'warning',
foo: 'bar',
},
query: 'topk(5, foo)[5m]',
annotations: {
message: 'great alert',
},
alerts: [
mockPromAlert({
labels: {
foo: 'bar',
severity: 'warning',
},
value: '2e+10',
annotations: {
message: 'first alert message',
},
}),
mockPromAlert({
labels: {
foo: 'baz',
severity: 'error',
},
value: '3e+11',
annotations: {
message: 'first alert message',
},
}),
],
}),
],
}),
mockPromRuleGroup({
name: 'group-2',
rules: [
mockPromAlertingRule({
name: 'alertingrule2',
labels: {
severity: 'error',
foo: 'buzz',
},
query: 'topk(5, foo)[5m]',
annotations: {
message: 'great alert',
},
alerts: [
mockPromAlert({
labels: {
foo: 'buzz',
severity: 'error',
region: 'EU',
},
value: '2e+10',
annotations: {
message: 'alert message',
},
}),
mockPromAlert({
labels: {
foo: 'buzz',
severity: 'error',
region: 'US',
},
value: '3e+11',
annotations: {
message: 'alert message',
},
}),
],
}),
],
}),
],
}),
]);
}
});
await renderRuleList();
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(2);
const filterInput = ui.rulesFilterInput.get();
await userEvent.type(filterInput, 'label:foo=bar{Enter}');
// Input is debounced so wait for it to be visible
await waitFor(() => expect(filterInput).toHaveValue('label:foo=bar'));
// Group doesn't contain matching labels
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1));
await userEvent.click(ui.groupCollapseToggle.get(groups[0]));
const ruleRows = ui.ruleRow.getAll(groups[0]);
expect(ruleRows).toHaveLength(1);
await userEvent.click(ui.ruleCollapseToggle.get(ruleRows[0]));
const ruleDetails = ui.expandedContent.get(ruleRows[0]);
const labels = byTestId('label-value').getAll(ruleDetails);
expect(labels[0]).toHaveTextContent('severitywarning');
expect(labels[1]).toHaveTextContent('foobar');
// Check for different label matchers
await userEvent.clear(filterInput);
await userEvent.type(filterInput, 'label:foo!=bar label:foo!=baz{Enter}');
// Group doesn't contain matching labels
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1));
await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2'));
await userEvent.clear(filterInput);
await userEvent.type(filterInput, 'label:"foo=~b.+"{Enter}');
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(2));
await userEvent.clear(filterInput);
await userEvent.type(filterInput, 'label:region=US{Enter}');
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1));
await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2'));
});
it.skip('uses entire group when reordering after filtering', async () => {
const user = userEvent.setup();
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRulerRules.mockImplementation(() => Promise.resolve(someRulerRules));
mocks.api.fetchRules.mockImplementation((dataSourceName: string) => {
if (dataSourceName === GRAFANA_RULES_SOURCE_NAME) {
return Promise.resolve([
mockPromRuleNamespace({
name: 'foofolder',
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
groups: [
mockPromRuleGroup({
name: 'grafana-group',
rules: [
mockPromAlertingRule({
query: '[]',
}),
],
}),
],
}),
]);
} else {
return Promise.resolve([]);
}
});
renderRuleList();
const [firstReorderButton] = await screen.findAllByLabelText(/reorder/i);
const filterInput = ui.rulesFilterInput.get();
await userEvent.type(filterInput, 'alert1a{Enter}');
await user.click(firstReorderButton);
const reorderDialog = await screen.findByRole('dialog');
const alertsInReorder = within(reorderDialog).getAllByTestId('reorder-alert-rule');
// We've filtered down to one rule, but the reorder dialog should still
// have everything in the group visible for reordering
// If this were not the case, rules could be deleted ⚠️
expect(alertsInReorder).toHaveLength(2);
});
describe.skip('pausing rules', () => {
beforeEach(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRulerRules.mockImplementation(() => Promise.resolve(getPotentiallyPausedRulerRules(true)));
mocks.api.fetchRules.mockImplementation((sourceName) =>
Promise.resolve(sourceName === 'grafana' ? pausedPromRules('grafana') : [])
);
mocks.api.rulerBuilderMock.mockReturnValue({
rules: () => ({ path: `api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules` }),
namespace: () => ({ path: 'ruler' }),
namespaceGroup: () => ({
path: `api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules/NAMESPACE_UID/groupPaused`,
}),
});
});
test('resuming paused alert rule', async () => {
const user = userEvent.setup();
renderRuleList();
// Expand the paused rule group so we can assert the rule state
await user.click(await ui.pausedRuleGroup.find());
expect(await ui.stateTags.paused.find()).toBeInTheDocument();
// TODO: Migrate all testing logic to MSW and so we aren't manually tweaking the API response behaviour
mocks.api.fetchRulerRules.mockImplementationOnce(() => {
return Promise.resolve(getPotentiallyPausedRulerRules(false));
});
await user.click(await ui.actionButtons.more.find());
await user.click(await ui.moreActionItems.resume.find());
await waitFor(() => expect(ui.stateTags.paused.query()).not.toBeInTheDocument());
});
});
/**
* @TODO port these tests to MSW they rely on mocks a whole lot, and since we're looking to refactor the list view
* I imagine we'd need to rewrite these anyway.
*
* These actions are currently tested in the "useProduceNewRuleGroup" hook(s).
*/
describe.skip('edit lotex groups, namespaces', () => {
const testDatasources = {
prom: dataSources.prom,
};
function testCase(name: string, fn: () => Promise<void>) {
it(name, async () => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(testDatasources));
setDataSourceSrv(new MockDataSourceSrv(testDatasources));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockImplementation((sourceName) =>
Promise.resolve(sourceName === testDatasources.prom.name ? somePromRules() : [])
);
mocks.api.fetchRulerRules.mockImplementation(({ dataSourceName }) =>
Promise.resolve(dataSourceName === testDatasources.prom.name ? someRulerRules : {})
);
await renderRuleList();
expect(await ui.rulesFilterInput.find()).toHaveValue('');
await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(3));
const groups = await ui.ruleGroup.findAll();
expect(groups).toHaveLength(3);
// open edit dialog
await userEvent.click(ui.editCloudGroupIcon.get(groups[0]));
await waitFor(() => expect(ui.editGroupModal.dialog.get()).toBeInTheDocument());
expect(ui.editGroupModal.namespaceInput.get()).toHaveDisplayValue('namespace1');
expect(ui.editGroupModal.ruleGroupInput.get()).toHaveDisplayValue('group1');
await fn();
});
}
testCase('rename both lotex namespace and group', async () => {
// make changes to form
await userEvent.clear(ui.editGroupModal.namespaceInput.get());
await userEvent.type(ui.editGroupModal.namespaceInput.get(), 'super namespace');
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
await userEvent.clear(ui.editGroupModal.intervalInput.get());
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
// submit, check that appropriate calls were made
await userEvent.click(ui.editGroupModal.saveButton.get());
await waitFor(() => expect(ui.editGroupModal.namespaceInput.query()).not.toBeInTheDocument());
expect(mocks.api.fetchRulerRules).toHaveBeenCalledTimes(4);
});
testCase('rename just the lotex group', async () => {
// make changes to form
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
await userEvent.clear(ui.editGroupModal.intervalInput.get());
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
// submit, check that appropriate calls were made
await userEvent.click(ui.editGroupModal.saveButton.get());
await waitFor(() => expect(ui.editGroupModal.namespaceInput.query()).not.toBeInTheDocument());
expect(mocks.api.fetchRulerRules).toHaveBeenCalledTimes(4);
});
testCase('edit lotex group eval interval, no renaming', async () => {
// make changes to form
await userEvent.clear(ui.editGroupModal.intervalInput.get());
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
// submit, check that appropriate calls were made
await userEvent.click(ui.editGroupModal.saveButton.get());
await waitFor(() => expect(ui.editGroupModal.namespaceInput.query()).not.toBeInTheDocument());
expect(mocks.api.fetchRulerRules).toHaveBeenCalledTimes(4);
});
});
describe('RBAC Enabled', () => {
describe('Export button', () => {
it('Export button should be visible when the user has alert read permissions', async () => {
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue([
mockPromRuleNamespace({
name: 'foofolder',
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
groups: [
mockPromRuleGroup({
name: 'grafana-group',
rules: [
mockPromAlertingRule({
query: '[]',
}),
],
}),
],
}),
]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
const groupRows = await ui.ruleGroup.findAll();
expect(groupRows).toHaveLength(1);
expect(ui.exportButton.get()).toBeInTheDocument();
});
});
describe('Grafana Managed Alerts', () => {
it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => {
grantUserPermissions([
AccessControlAction.FoldersRead,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue([]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
it('New alert button should be visible when the user has alert rule create and folder read permissions and rules already exists', async () => {
grantUserPermissions([
AccessControlAction.FoldersRead,
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue(somePromRules('grafana'));
mocks.api.fetchRulerRules.mockResolvedValue(someRulerRules);
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
});
describe('Cloud Alerts', () => {
it('New alert button should be visible when the user has the alert rule external write and datasource read permissions and no rules exists', async () => {
grantUserPermissions([
// AccessControlAction.AlertingRuleRead,
AccessControlAction.DataSourcesRead,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockResolvedValue([]);
mocks.api.fetchRulerRules.mockResolvedValue({});
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
it('New alert button should be visible when the user has the alert rule external write and data source read permissions and rules already exists', async () => {
grantUserPermissions([
AccessControlAction.DataSourcesRead,
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]);
setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom }));
mocks.api.discoverFeatures.mockResolvedValue({
application: PromApplication.Cortex,
features: {
rulerApiEnabled: true,
},
});
mocks.api.fetchRules.mockResolvedValue(somePromRules('Cortex'));
mocks.api.fetchRulerRules.mockResolvedValue(someRulerRules);
renderRuleList();
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.newRuleButton.get()).toBeInTheDocument();
});
});
});
});