2024-05-30 12:55:06 +02:00
|
|
|
import { within } from '@testing-library/react';
|
2024-04-30 10:34:52 +02:00
|
|
|
import { render, waitFor, screen, userEvent } from 'test/test-utils';
|
2024-01-23 15:04:12 +01:00
|
|
|
import { byText, byRole } from 'testing-library-selector';
|
|
|
|
|
|
2024-09-13 09:23:18 +02:00
|
|
|
import { setPluginLinksHook } from '@grafana/runtime';
|
2024-05-16 09:34:07 +01:00
|
|
|
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
|
|
|
|
import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure';
|
|
|
|
|
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
2024-01-23 15:04:12 +01:00
|
|
|
import { AccessControlAction } from 'app/types';
|
|
|
|
|
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
|
|
|
|
|
2024-04-30 10:34:52 +02:00
|
|
|
import {
|
|
|
|
|
getCloudRule,
|
|
|
|
|
getGrafanaRule,
|
2024-09-02 10:32:47 +02:00
|
|
|
getVanillaPromRule,
|
2024-04-30 10:34:52 +02:00
|
|
|
grantUserPermissions,
|
|
|
|
|
mockDataSource,
|
|
|
|
|
mockPluginLinkExtension,
|
2024-09-02 10:32:47 +02:00
|
|
|
mockPromAlertingRule,
|
2024-04-30 10:34:52 +02:00
|
|
|
} from '../../mocks';
|
2024-07-11 19:12:19 +02:00
|
|
|
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
|
2024-04-30 10:34:52 +02:00
|
|
|
import { setupDataSources } from '../../testSetup/datasources';
|
2024-03-14 15:18:01 +01:00
|
|
|
import { Annotation } from '../../utils/constants';
|
2024-06-05 20:09:26 +01:00
|
|
|
import { DataSourceType } from '../../utils/datasource';
|
2024-03-14 15:18:01 +01:00
|
|
|
import * as ruleId from '../../utils/rule-id';
|
2024-09-02 10:32:47 +02:00
|
|
|
import { stringifyIdentifier } from '../../utils/rule-id';
|
2024-01-23 15:04:12 +01:00
|
|
|
|
|
|
|
|
import { AlertRuleProvider } from './RuleContext';
|
2024-09-02 10:32:47 +02:00
|
|
|
import RuleViewer, { ActiveTab } from './RuleViewer';
|
2024-01-23 15:04:12 +01:00
|
|
|
|
|
|
|
|
// metadata and interactive elements
|
|
|
|
|
const ELEMENTS = {
|
|
|
|
|
loading: byText(/Loading rule/i),
|
|
|
|
|
metadata: {
|
|
|
|
|
summary: (text: string) => byText(text),
|
|
|
|
|
runbook: (url: string) => byRole('link', { name: url }),
|
|
|
|
|
dashboardAndPanel: byRole('link', { name: 'View panel' }),
|
|
|
|
|
evaluationInterval: (interval: string) => byText(`Every ${interval}`),
|
|
|
|
|
label: ([key, value]: [string, string]) => byRole('listitem', { name: `${key}: ${value}` }),
|
|
|
|
|
},
|
2024-09-02 10:32:47 +02:00
|
|
|
details: {
|
|
|
|
|
pendingPeriod: byText(/Pending period/i),
|
|
|
|
|
},
|
2024-01-23 15:04:12 +01:00
|
|
|
actions: {
|
|
|
|
|
edit: byRole('link', { name: 'Edit' }),
|
|
|
|
|
more: {
|
|
|
|
|
button: byRole('button', { name: /More/i }),
|
|
|
|
|
actions: {
|
2024-05-16 09:34:07 +01:00
|
|
|
silence: byRole('menuitem', { name: /Silence/i }),
|
2024-01-23 15:04:12 +01:00
|
|
|
duplicate: byRole('menuitem', { name: /Duplicate/i }),
|
|
|
|
|
copyLink: byRole('menuitem', { name: /Copy link/i }),
|
|
|
|
|
export: byRole('menuitem', { name: /Export/i }),
|
|
|
|
|
delete: byRole('menuitem', { name: /Delete/i }),
|
|
|
|
|
},
|
2024-04-30 10:34:52 +02:00
|
|
|
pluginActions: {
|
2024-04-30 16:17:55 +01:00
|
|
|
sloDashboard: byRole('menuitem', { name: /SLO dashboard/i }),
|
2024-04-30 10:34:52 +02:00
|
|
|
declareIncident: byRole('link', { name: /Declare incident/i }),
|
2024-04-30 16:17:55 +01:00
|
|
|
assertsWorkbench: byRole('menuitem', { name: /Open workbench/i }),
|
2024-04-30 10:34:52 +02:00
|
|
|
},
|
2024-01-23 15:04:12 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-16 09:34:07 +01:00
|
|
|
setupMswServer();
|
2024-04-30 10:34:52 +02:00
|
|
|
setupDataSources(mockDataSource({ type: DataSourceType.Prometheus, name: 'mimir-1' }));
|
2024-09-13 09:23:18 +02:00
|
|
|
setPluginLinksHook(() => ({
|
|
|
|
|
links: [
|
2024-04-30 10:34:52 +02:00
|
|
|
mockPluginLinkExtension({ pluginId: 'grafana-slo-app', title: 'SLO dashboard', path: '/a/grafana-slo-app' }),
|
|
|
|
|
mockPluginLinkExtension({
|
|
|
|
|
pluginId: 'grafana-asserts-app',
|
|
|
|
|
title: 'Open workbench',
|
|
|
|
|
path: '/a/grafana-asserts-app',
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
isLoading: false,
|
|
|
|
|
}));
|
|
|
|
|
|
2024-06-05 20:09:26 +01:00
|
|
|
/**
|
|
|
|
|
* "Grants" permissions via contextSrv mock, and additionally sets folder access control
|
|
|
|
|
* API response to match
|
|
|
|
|
*/
|
|
|
|
|
const grantPermissionsHelper = (permissions: AccessControlAction[]) => {
|
|
|
|
|
const permissionsHash = permissions.reduce((hash, permission) => ({ ...hash, [permission]: true }), {});
|
|
|
|
|
grantUserPermissions(permissions);
|
|
|
|
|
setFolderAccessControl(permissionsHash);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openSilenceDrawer = async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
await user.click(ELEMENTS.actions.more.button.get());
|
|
|
|
|
await user.click(ELEMENTS.actions.more.actions.silence.get());
|
|
|
|
|
await screen.findByText(/Configure silences/i);
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-30 10:34:52 +02:00
|
|
|
beforeAll(() => {
|
2024-06-05 20:09:26 +01:00
|
|
|
grantPermissionsHelper([
|
2024-04-30 10:34:52 +02:00
|
|
|
AccessControlAction.AlertingRuleCreate,
|
|
|
|
|
AccessControlAction.AlertingRuleRead,
|
|
|
|
|
AccessControlAction.AlertingRuleUpdate,
|
|
|
|
|
AccessControlAction.AlertingRuleDelete,
|
|
|
|
|
AccessControlAction.AlertingInstanceCreate,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
2024-01-23 15:04:12 +01:00
|
|
|
describe('RuleViewer', () => {
|
|
|
|
|
describe('Grafana managed alert rule', () => {
|
|
|
|
|
const mockRule = getGrafanaRule(
|
|
|
|
|
{
|
|
|
|
|
name: 'Test alert',
|
|
|
|
|
annotations: {
|
|
|
|
|
[Annotation.dashboardUID]: 'dashboard-1',
|
|
|
|
|
[Annotation.panelID]: 'panel-1',
|
|
|
|
|
[Annotation.summary]: 'This is the summary for the rule',
|
|
|
|
|
[Annotation.runbookURL]: 'https://runbook.site/',
|
|
|
|
|
},
|
|
|
|
|
labels: {
|
|
|
|
|
team: 'operations',
|
|
|
|
|
severity: 'low',
|
|
|
|
|
},
|
|
|
|
|
group: {
|
|
|
|
|
name: 'my-group',
|
|
|
|
|
interval: '15m',
|
|
|
|
|
rules: [],
|
|
|
|
|
totals: { alerting: 1 },
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-05-30 12:55:06 +02:00
|
|
|
{ uid: grafanaRulerRule.grafana_alert.uid }
|
2024-01-23 15:04:12 +01:00
|
|
|
);
|
|
|
|
|
const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule);
|
|
|
|
|
|
2024-05-16 09:34:07 +01:00
|
|
|
beforeAll(() => {
|
2024-06-05 20:09:26 +01:00
|
|
|
grantPermissionsHelper([
|
2024-05-16 09:34:07 +01:00
|
|
|
AccessControlAction.AlertingRuleCreate,
|
|
|
|
|
AccessControlAction.AlertingRuleRead,
|
|
|
|
|
AccessControlAction.AlertingRuleUpdate,
|
|
|
|
|
AccessControlAction.AlertingRuleDelete,
|
2024-05-30 12:55:06 +02:00
|
|
|
AccessControlAction.AlertingInstanceRead,
|
2024-05-16 09:34:07 +01:00
|
|
|
AccessControlAction.AlertingInstanceCreate,
|
2024-06-05 20:09:26 +01:00
|
|
|
AccessControlAction.AlertingInstanceRead,
|
|
|
|
|
AccessControlAction.AlertingInstancesExternalRead,
|
|
|
|
|
AccessControlAction.AlertingInstancesExternalWrite,
|
2024-05-16 09:34:07 +01:00
|
|
|
]);
|
2024-06-05 20:09:26 +01:00
|
|
|
|
|
|
|
|
const dataSources = {
|
|
|
|
|
am: mockDataSource<AlertManagerDataSourceJsonData>({
|
|
|
|
|
name: 'Alertmanager',
|
|
|
|
|
type: DataSourceType.Alertmanager,
|
|
|
|
|
jsonData: {
|
|
|
|
|
handleGrafanaManagedAlerts: true,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
setupDataSources(dataSources.am);
|
2024-05-16 09:34:07 +01:00
|
|
|
});
|
|
|
|
|
|
2024-01-23 15:04:12 +01:00
|
|
|
it('should render a Grafana managed alert rule', async () => {
|
|
|
|
|
await renderRuleViewer(mockRule, mockRuleIdentifier);
|
|
|
|
|
|
|
|
|
|
// assert on basic info to be visible
|
|
|
|
|
expect(screen.getByText('Test alert')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText('Firing')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
// alert rule metadata
|
|
|
|
|
const ruleSummary = mockRule.annotations[Annotation.summary];
|
|
|
|
|
const runBookURL = mockRule.annotations[Annotation.runbookURL];
|
|
|
|
|
const groupInterval = mockRule.group.interval;
|
|
|
|
|
const labels = mockRule.labels;
|
|
|
|
|
|
|
|
|
|
expect(ELEMENTS.metadata.summary(ruleSummary).get()).toBeInTheDocument();
|
|
|
|
|
expect(ELEMENTS.metadata.dashboardAndPanel.get()).toBeInTheDocument();
|
|
|
|
|
expect(ELEMENTS.metadata.runbook(runBookURL).get()).toBeInTheDocument();
|
|
|
|
|
expect(ELEMENTS.metadata.evaluationInterval(groupInterval!).get()).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
for (const label in labels) {
|
|
|
|
|
expect(ELEMENTS.metadata.label([label, labels[label]]).get()).toBeInTheDocument();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// actions
|
2024-07-30 13:16:54 +01:00
|
|
|
expect(await ELEMENTS.actions.edit.find()).toBeInTheDocument();
|
|
|
|
|
expect(ELEMENTS.actions.more.button.get()).toBeInTheDocument();
|
2024-01-23 15:04:12 +01:00
|
|
|
|
|
|
|
|
// check the "more actions" button
|
|
|
|
|
await userEvent.click(ELEMENTS.actions.more.button.get());
|
|
|
|
|
const menuItems = Object.values(ELEMENTS.actions.more.actions);
|
|
|
|
|
for (const menuItem of menuItems) {
|
|
|
|
|
expect(menuItem.get()).toBeInTheDocument();
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-05-16 09:34:07 +01:00
|
|
|
|
2024-06-05 20:09:26 +01:00
|
|
|
it('renders silencing form correctly and shows alert rule name', async () => {
|
2024-05-16 09:34:07 +01:00
|
|
|
await renderRuleViewer(mockRule, mockRuleIdentifier);
|
2024-06-05 20:09:26 +01:00
|
|
|
await openSilenceDrawer();
|
2024-05-16 09:34:07 +01:00
|
|
|
|
2024-05-30 12:55:06 +02:00
|
|
|
const silenceDrawer = await screen.findByRole('dialog', { name: 'Drawer title Silence alert rule' });
|
|
|
|
|
expect(await within(silenceDrawer).findByLabelText(/^alert rule/i)).toHaveValue(
|
|
|
|
|
grafanaRulerRule.grafana_alert.title
|
|
|
|
|
);
|
2024-05-16 09:34:07 +01:00
|
|
|
});
|
2024-01-23 15:04:12 +01:00
|
|
|
});
|
|
|
|
|
|
2024-04-30 10:34:52 +02:00
|
|
|
describe('Data source managed alert rule', () => {
|
|
|
|
|
const mockRule = getCloudRule({
|
|
|
|
|
name: 'cloud test alert',
|
|
|
|
|
annotations: { [Annotation.summary]: 'cloud summary', [Annotation.runbookURL]: 'https://runbook.example.com' },
|
|
|
|
|
group: { name: 'Cloud group', interval: '15m', rules: [], totals: { alerting: 1 } },
|
|
|
|
|
});
|
2024-01-23 15:04:12 +01:00
|
|
|
const mockRuleIdentifier = ruleId.fromCombinedRule('mimir-1', mockRule);
|
|
|
|
|
|
|
|
|
|
beforeAll(() => {
|
|
|
|
|
grantUserPermissions([
|
|
|
|
|
AccessControlAction.AlertingRuleExternalRead,
|
|
|
|
|
AccessControlAction.AlertingRuleExternalWrite,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should render a data source managed alert rule', () => {
|
|
|
|
|
renderRuleViewer(mockRule, mockRuleIdentifier);
|
|
|
|
|
|
|
|
|
|
// assert on basic info to be vissible
|
2024-04-30 10:34:52 +02:00
|
|
|
expect(screen.getByText('cloud test alert')).toBeInTheDocument();
|
2024-01-23 15:04:12 +01:00
|
|
|
expect(screen.getByText('Firing')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText(mockRule.annotations[Annotation.summary])).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole('link', { name: mockRule.annotations[Annotation.runbookURL] })).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText(`Every ${mockRule.group.interval}`)).toBeInTheDocument();
|
|
|
|
|
});
|
2024-04-30 10:34:52 +02:00
|
|
|
|
|
|
|
|
it('should render custom plugin actions for a plugin-provided rule', async () => {
|
|
|
|
|
const sloRule = getCloudRule({
|
|
|
|
|
name: 'slo test alert',
|
|
|
|
|
labels: { __grafana_origin: 'plugin/grafana-slo-app' },
|
|
|
|
|
});
|
|
|
|
|
const sloRuleIdentifier = ruleId.fromCombinedRule('mimir-1', sloRule);
|
|
|
|
|
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
|
|
|
|
|
renderRuleViewer(sloRule, sloRuleIdentifier);
|
|
|
|
|
|
|
|
|
|
expect(ELEMENTS.actions.more.button.get()).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
await user.click(ELEMENTS.actions.more.button.get());
|
|
|
|
|
|
|
|
|
|
expect(ELEMENTS.actions.more.pluginActions.sloDashboard.get()).toBeInTheDocument();
|
|
|
|
|
expect(ELEMENTS.actions.more.pluginActions.assertsWorkbench.query()).not.toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should render different custom plugin actions for a different plugin-provided rule', async () => {
|
|
|
|
|
const assertsRule = getCloudRule({
|
|
|
|
|
name: 'asserts test alert',
|
|
|
|
|
labels: { __grafana_origin: 'plugin/grafana-asserts-app' },
|
|
|
|
|
});
|
|
|
|
|
const assertsRuleIdentifier = ruleId.fromCombinedRule('mimir-1', assertsRule);
|
|
|
|
|
|
|
|
|
|
renderRuleViewer(assertsRule, assertsRuleIdentifier);
|
|
|
|
|
|
|
|
|
|
expect(ELEMENTS.actions.more.button.get()).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
await userEvent.click(ELEMENTS.actions.more.button.get());
|
|
|
|
|
|
|
|
|
|
expect(ELEMENTS.actions.more.pluginActions.assertsWorkbench.get()).toBeInTheDocument();
|
|
|
|
|
expect(ELEMENTS.actions.more.pluginActions.sloDashboard.query()).not.toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(ELEMENTS.actions.more.pluginActions.declareIncident.get()).toBeEnabled());
|
|
|
|
|
});
|
2024-01-23 15:04:12 +01:00
|
|
|
});
|
2024-09-02 10:32:47 +02:00
|
|
|
|
|
|
|
|
describe('Vanilla Prometheus rule', () => {
|
|
|
|
|
const mockRule = getVanillaPromRule({
|
|
|
|
|
name: 'prom test alert',
|
|
|
|
|
annotations: { [Annotation.summary]: 'prom summary', [Annotation.runbookURL]: 'https://runbook.example.com' },
|
|
|
|
|
promRule: {
|
|
|
|
|
...mockPromAlertingRule(),
|
|
|
|
|
duration: 900, // 15 minutes
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mockRuleIdentifier = ruleId.fromCombinedRule('prometheus', mockRule);
|
|
|
|
|
|
|
|
|
|
it('should render pending period for vanilla Prometheus alert rule', async () => {
|
|
|
|
|
renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Details);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('prom test alert')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
// One summary is rendered by the Title component, and the other by the DetailsTab component
|
|
|
|
|
expect(ELEMENTS.metadata.summary(mockRule.annotations[Annotation.summary]).getAll()).toHaveLength(2);
|
|
|
|
|
|
|
|
|
|
expect(within(ELEMENTS.details.pendingPeriod.get()).getByText(/15m/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-01-23 15:04:12 +01:00
|
|
|
});
|
|
|
|
|
|
2024-09-02 10:32:47 +02:00
|
|
|
const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier, tab: ActiveTab = ActiveTab.Query) => {
|
|
|
|
|
const path = `/alerting/${identifier.ruleSourceName}/${stringifyIdentifier(identifier)}/view?tab=${tab}`;
|
2024-01-23 15:04:12 +01:00
|
|
|
render(
|
|
|
|
|
<AlertRuleProvider identifier={identifier} rule={rule}>
|
|
|
|
|
<RuleViewer />
|
2024-09-02 10:32:47 +02:00
|
|
|
</AlertRuleProvider>,
|
|
|
|
|
{ historyOptions: { initialEntries: [path] } }
|
2024-01-23 15:04:12 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(ELEMENTS.loading.query()).not.toBeInTheDocument());
|
|
|
|
|
};
|
2024-02-07 18:02:20 +01:00
|
|
|
|
|
|
|
|
jest.mock('@grafana/runtime', () => ({
|
|
|
|
|
...jest.requireActual('@grafana/runtime'),
|
|
|
|
|
useReturnToPrevious: jest.fn(),
|
|
|
|
|
}));
|