Alerting: Optimize rules fetching on the folder's rules tab (#104777)
* Mark labels and annotations as optional in Grafana ruler DTO * Refactor AlertsFolderView to use folder-specific endpoint for rules loading * Improve tests for BrowserFolderAlertingPage * Update translations * Revert go changes * update gen files --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
co-authored by
Gilles De Mey
parent
a687c4a757
commit
13ebcf1d2c
@@ -1,13 +1,9 @@
|
||||
import { render } from 'test/test-utils';
|
||||
import { byTestId } from 'testing-library-selector';
|
||||
|
||||
import { FolderState } from 'app/types';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertsFolderView } from './AlertsFolderView';
|
||||
import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
|
||||
import { mockCombinedRule } from './mocks';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { mockFolder } from './mocks';
|
||||
import { alertingFactory } from './mocks/server/db';
|
||||
|
||||
const ui = {
|
||||
filter: {
|
||||
@@ -19,122 +15,39 @@ const ui = {
|
||||
},
|
||||
};
|
||||
|
||||
const combinedNamespaceMock = jest.fn(useCombinedRuleNamespaces);
|
||||
jest.mock('./hooks/useCombinedRuleNamespaces', () => ({
|
||||
useCombinedRuleNamespaces: () => combinedNamespaceMock(),
|
||||
}));
|
||||
|
||||
const mockFolder = (folderOverride: Partial<FolderState> = {}): FolderState => {
|
||||
return {
|
||||
id: 1,
|
||||
title: 'Folder with alerts',
|
||||
uid: 'folder-1',
|
||||
hasChanged: false,
|
||||
canSave: false,
|
||||
url: '/folder-1',
|
||||
version: 1,
|
||||
canDelete: false,
|
||||
...folderOverride,
|
||||
};
|
||||
};
|
||||
const alertingRuleBuilder = alertingFactory.ruler.grafana.alertingRule;
|
||||
|
||||
describe('AlertsFolderView tests', () => {
|
||||
it('Should display grafana alert rules when the folder uid matches the name space uid', () => {
|
||||
// Arrange
|
||||
const folder = mockFolder();
|
||||
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: folder.title,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
uid: 'folder-1',
|
||||
groups: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [
|
||||
mockCombinedRule({ name: 'Test Alert 1' }),
|
||||
mockCombinedRule({ name: 'Test Alert 2' }),
|
||||
mockCombinedRule({ name: 'Test Alert 3' }),
|
||||
],
|
||||
totals: {},
|
||||
},
|
||||
{
|
||||
name: 'group2',
|
||||
rules: [
|
||||
mockCombinedRule({ name: 'Test Alert 4' }),
|
||||
mockCombinedRule({ name: 'Test Alert 5' }),
|
||||
mockCombinedRule({ name: 'Test Alert 6' }),
|
||||
],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
combinedNamespaceMock.mockReturnValue([grafanaNamespace]);
|
||||
const folderRules = alertingRuleBuilder.buildList(6);
|
||||
|
||||
// Act
|
||||
render(<AlertsFolderView folder={folder} />);
|
||||
render(<AlertsFolderView folder={folder} rules={folderRules} />);
|
||||
|
||||
// Assert
|
||||
const alertRows = ui.ruleList.row.queryAll();
|
||||
expect(alertRows).toHaveLength(6);
|
||||
expect(alertRows[0]).toHaveTextContent('Test Alert 1');
|
||||
expect(alertRows[1]).toHaveTextContent('Test Alert 2');
|
||||
expect(alertRows[2]).toHaveTextContent('Test Alert 3');
|
||||
expect(alertRows[3]).toHaveTextContent('Test Alert 4');
|
||||
expect(alertRows[4]).toHaveTextContent('Test Alert 5');
|
||||
expect(alertRows[5]).toHaveTextContent('Test Alert 6');
|
||||
});
|
||||
|
||||
it('Should not display alert rules when the namespace uid does not match the folder uid', () => {
|
||||
// Arrange
|
||||
const folder = mockFolder();
|
||||
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: 'Folder without alerts',
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
uid: 'folder-2',
|
||||
groups: [
|
||||
{
|
||||
name: 'default',
|
||||
rules: [
|
||||
mockCombinedRule({ name: 'Test Alert from other folder 1' }),
|
||||
mockCombinedRule({ name: 'Test Alert from other folder 2' }),
|
||||
],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
combinedNamespaceMock.mockReturnValue([grafanaNamespace]);
|
||||
|
||||
// Act
|
||||
render(<AlertsFolderView folder={folder} />);
|
||||
|
||||
// Assert
|
||||
expect(ui.ruleList.row.queryAll()).toHaveLength(0);
|
||||
expect(alertRows[0]).toHaveTextContent('Alerting rule 1');
|
||||
expect(alertRows[1]).toHaveTextContent('Alerting rule 2');
|
||||
expect(alertRows[2]).toHaveTextContent('Alerting rule 3');
|
||||
expect(alertRows[3]).toHaveTextContent('Alerting rule 4');
|
||||
expect(alertRows[4]).toHaveTextContent('Alerting rule 5');
|
||||
expect(alertRows[5]).toHaveTextContent('Alerting rule 6');
|
||||
});
|
||||
|
||||
it('Should filter alert rules by the name, case insensitive', async () => {
|
||||
// Arrange
|
||||
const folder = mockFolder();
|
||||
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: folder.title,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
uid: 'folder-1',
|
||||
groups: [
|
||||
{
|
||||
name: 'default',
|
||||
rules: [mockCombinedRule({ name: 'CPU Alert' }), mockCombinedRule({ name: 'RAM usage alert' })],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
combinedNamespaceMock.mockReturnValue([grafanaNamespace]);
|
||||
const folderRules = [
|
||||
alertingRuleBuilder.build({ grafana_alert: { title: 'CPU Alert' } }),
|
||||
alertingRuleBuilder.build({ grafana_alert: { title: 'RAM usage alert' } }),
|
||||
];
|
||||
|
||||
// Act
|
||||
const { user } = render(<AlertsFolderView folder={folder} />);
|
||||
const { user } = render(<AlertsFolderView folder={folder} rules={folderRules} />);
|
||||
|
||||
await user.type(ui.filter.name.get(), 'cpu');
|
||||
|
||||
@@ -147,26 +60,13 @@ describe('AlertsFolderView tests', () => {
|
||||
// Arrange
|
||||
const folder = mockFolder();
|
||||
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: folder.title,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
uid: 'folder-1',
|
||||
groups: [
|
||||
{
|
||||
name: 'default',
|
||||
rules: [
|
||||
mockCombinedRule({ name: 'CPU Alert', labels: {} }),
|
||||
mockCombinedRule({ name: 'RAM usage alert', labels: { severity: 'critical' } }),
|
||||
],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
combinedNamespaceMock.mockReturnValue([grafanaNamespace]);
|
||||
const folderRules = [
|
||||
alertingRuleBuilder.build({ grafana_alert: { title: 'CPU Alert' }, labels: {} }),
|
||||
alertingRuleBuilder.build({ grafana_alert: { title: 'RAM usage alert' }, labels: { severity: 'critical' } }),
|
||||
];
|
||||
|
||||
// Act
|
||||
const { user } = render(<AlertsFolderView folder={folder} />);
|
||||
const { user } = render(<AlertsFolderView folder={folder} rules={folderRules} />);
|
||||
|
||||
await user.type(ui.filter.label.get(), 'severity=critical');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { orderBy } from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
@@ -8,20 +8,19 @@ import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { getQueryParamValue } from 'app/core/utils/query';
|
||||
import { FolderState, useDispatch } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { FolderDTO } from 'app/types';
|
||||
import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
|
||||
import { usePagination } from './hooks/usePagination';
|
||||
import { useURLSearchParams } from './hooks/useURLSearchParams';
|
||||
import { fetchPromRulesAction, fetchRulerRulesAction } from './state/actions';
|
||||
import { combineMatcherStrings, labelsMatchMatchers } from './utils/alertmanager';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
|
||||
import { createViewLink } from './utils/misc';
|
||||
import { rulesNav } from './utils/navigation';
|
||||
|
||||
interface Props {
|
||||
folder: FolderState;
|
||||
folder: FolderDTO;
|
||||
rules: RulerGrafanaRuleDTO[];
|
||||
}
|
||||
|
||||
enum SortOrder {
|
||||
@@ -34,31 +33,20 @@ const sortOptions: Array<SelectableValue<SortOrder>> = [
|
||||
{ label: 'Alphabetically [Z-A]', value: SortOrder.Descending },
|
||||
];
|
||||
|
||||
export const AlertsFolderView = ({ folder }: Props) => {
|
||||
export const AlertsFolderView = ({ folder, rules }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onTagClick = (tagName: string) => {
|
||||
const matchersString = combineMatcherStrings(labelFilter, tagName);
|
||||
setLabelFilter(matchersString);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchPromRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
||||
}, [dispatch]);
|
||||
|
||||
const combinedNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
|
||||
const { nameFilter, labelFilter, sortOrder, setNameFilter, setLabelFilter, setSortOrder } =
|
||||
useAlertsFolderViewParams();
|
||||
|
||||
const matchingNamespace = combinedNamespaces.find((namespace) => namespace.uid === folder.uid);
|
||||
const filteredRules = filterAndSortRules(rules, nameFilter, labelFilter, sortOrder ?? SortOrder.Ascending);
|
||||
const hasNoResults = filteredRules.length === 0;
|
||||
|
||||
const alertRules = matchingNamespace?.groups.flatMap((group) => group.rules) ?? [];
|
||||
|
||||
const filteredRules = filterAndSortRules(alertRules, nameFilter, labelFilter, sortOrder ?? SortOrder.Ascending);
|
||||
|
||||
const hasNoResults = alertRules.length === 0 || filteredRules.length === 0;
|
||||
const { page, numberOfPages, onPageChange, pageItems } = usePagination(filteredRules, 1, DEFAULT_PER_PAGE_PAGINATION);
|
||||
|
||||
return (
|
||||
@@ -96,18 +84,18 @@ export const AlertsFolderView = ({ folder }: Props) => {
|
||||
</Stack>
|
||||
|
||||
<Stack direction="column" gap={1}>
|
||||
{pageItems.map((currentRule) => (
|
||||
{pageItems.map(({ grafana_alert, labels = {} }) => (
|
||||
<Card
|
||||
key={Boolean(currentRule.uid) ? currentRule.uid : currentRule.name}
|
||||
href={createViewLink('grafana', currentRule, '')}
|
||||
key={grafana_alert.uid}
|
||||
href={createGrafanaRuleViewLink(grafana_alert)}
|
||||
className={styles.card}
|
||||
data-testid="alert-card-row"
|
||||
>
|
||||
<Card.Heading>{currentRule.name}</Card.Heading>
|
||||
<Card.Heading>{grafana_alert.title}</Card.Heading>
|
||||
<Card.Tags>
|
||||
<TagList
|
||||
onClick={onTagClick}
|
||||
tags={Object.entries(currentRule.labels).map(([label, value]) => `${label}=${value}`)}
|
||||
tags={Object.entries(labels).map(([label, value]) => `${label}=${value}`)}
|
||||
/>
|
||||
</Card.Tags>
|
||||
<Card.Meta>
|
||||
@@ -178,17 +166,32 @@ function useAlertsFolderViewParams() {
|
||||
}
|
||||
|
||||
function filterAndSortRules(
|
||||
originalRules: CombinedRule[],
|
||||
originalRules: RulerGrafanaRuleDTO[],
|
||||
nameFilter: string,
|
||||
labelFilter: string,
|
||||
sortOrder: SortOrder
|
||||
) {
|
||||
const matchers = parsePromQLStyleMatcherLooseSafe(labelFilter);
|
||||
const rules = originalRules.filter(
|
||||
(rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers)
|
||||
);
|
||||
const rules = originalRules.filter((rule) => {
|
||||
const nameMatch = rule.grafana_alert.title.toLowerCase().includes(nameFilter.toLowerCase());
|
||||
const labelMatch = labelsMatchMatchers(rule.labels ?? {}, matchers);
|
||||
return nameMatch && labelMatch;
|
||||
});
|
||||
|
||||
return orderBy(rules, (x) => x.name.toLowerCase(), [sortOrder === SortOrder.Ascending ? 'asc' : 'desc']);
|
||||
return orderBy(rules, (rule) => rule.grafana_alert.title.toLowerCase(), [
|
||||
sortOrder === SortOrder.Ascending ? 'asc' : 'desc',
|
||||
]);
|
||||
}
|
||||
|
||||
function createGrafanaRuleViewLink(ruleDefinition: GrafanaRuleDefinition): string {
|
||||
return rulesNav.detailsPageLink(
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
{
|
||||
uid: ruleDefinition.uid,
|
||||
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
||||
@@ -6,6 +6,8 @@ import { config } from '@grafana/runtime';
|
||||
import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { FolderDTO } from 'app/types';
|
||||
import {
|
||||
GrafanaAlertStateDecision,
|
||||
GrafanaAlertingRuleDefinition,
|
||||
GrafanaRecordingRuleDefinition,
|
||||
PromAlertingRuleDTO,
|
||||
PromAlertingRuleState,
|
||||
@@ -14,11 +16,13 @@ import {
|
||||
RulerAlertingRuleDTO,
|
||||
RulerCloudRuleDTO,
|
||||
RulerGrafanaRuleDTO,
|
||||
RulerGrafanaRuleGroupDTO,
|
||||
RulerRecordingRuleDTO,
|
||||
RulerRuleGroupDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { DataSourceType } from '../../utils/datasource';
|
||||
import { namespaces } from '../mimirRulerApi';
|
||||
|
||||
@@ -95,6 +99,12 @@ const rulerGroupFactory = Factory.define<RulerRuleGroupDTO<RulerCloudRuleDTO>, {
|
||||
}
|
||||
);
|
||||
|
||||
const rulerGrafanaGroupFactory = Factory.define<RulerGrafanaRuleGroupDTO>(({ sequence }) => ({
|
||||
name: `test-group-${sequence}`,
|
||||
interval: '1m',
|
||||
rules: grafanaAlertingRuleFactory.buildList(3),
|
||||
}));
|
||||
|
||||
class DataSourceFactory extends Factory<DataSourceInstanceSettings> {
|
||||
vanillaPrometheus() {
|
||||
return this.params({ uid: PROMETHEUS_DATASOURCE_UID, name: 'Prometheus' });
|
||||
@@ -179,6 +189,26 @@ const grafanaRecordingRule = Factory.define<RulerGrafanaRuleDTO<GrafanaRecording
|
||||
annotations: {}, // @TODO recording rules don't have annotations, we need to fix this type definition
|
||||
}));
|
||||
|
||||
const grafanaAlertingRuleFactory = Factory.define<RulerGrafanaRuleDTO<GrafanaAlertingRuleDefinition>>(
|
||||
({ sequence }) => ({
|
||||
grafana_alert: {
|
||||
id: String(sequence),
|
||||
uid: uniqueId(),
|
||||
title: `Alerting rule ${sequence}`,
|
||||
namespace_uid: 'test-namespace',
|
||||
rule_group: 'test-group',
|
||||
condition: 'A',
|
||||
data: [],
|
||||
is_paused: false,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
} satisfies GrafanaAlertingRuleDefinition,
|
||||
for: '5m',
|
||||
labels: { 'label-key-1': 'label-value-1' },
|
||||
annotations: { [Annotation.summary]: 'test alert' },
|
||||
})
|
||||
);
|
||||
|
||||
class GrafanaContactPointFactory extends Factory<GrafanaManagedContactPoint> {
|
||||
withIntegrations(builder: (factory: GrafanaReceiverConfigFactory) => GrafanaManagedReceiverConfig[]) {
|
||||
return this.params({
|
||||
@@ -282,7 +312,9 @@ export const alertingFactory = {
|
||||
alertingRule: rulerAlertingRuleFactory,
|
||||
recordingRule: rulerRecordingRuleFactory,
|
||||
grafana: {
|
||||
group: rulerGrafanaGroupFactory,
|
||||
recordingRule: grafanaRecordingRule,
|
||||
alertingRule: grafanaAlertingRuleFactory,
|
||||
},
|
||||
},
|
||||
dataSource: dataSourceFactory,
|
||||
|
||||
@@ -4,11 +4,11 @@ import { mockFolder } from 'app/features/alerting/unified/mocks';
|
||||
import { grafanaRulerRule } from 'app/features/alerting/unified/mocks/grafanaRulerApi';
|
||||
import { FolderDTO } from 'app/types';
|
||||
|
||||
const DEFAULT_FOLDERS: FolderDTO[] = [
|
||||
export const DEFAULT_FOLDERS: FolderDTO[] = [
|
||||
mockFolder({
|
||||
id: 1,
|
||||
uid: 'uid',
|
||||
title: 'title',
|
||||
uid: 'e3d1f4fd-9e7c-4f63-9a9e-2b5a1d2e6a9c',
|
||||
title: 'Alerting-folder',
|
||||
}),
|
||||
mockFolder({
|
||||
id: 2,
|
||||
|
||||
@@ -180,6 +180,9 @@ export const rulerRuleVersionHistoryHandler = () => {
|
||||
draft.grafana_alert.version = 2;
|
||||
draft.grafana_alert.updated = '2025-01-14T09:35:17.000Z';
|
||||
draft.for = '2h';
|
||||
if (!draft.labels) {
|
||||
draft.labels = {};
|
||||
}
|
||||
draft.labels.foo = 'bar';
|
||||
draft.grafana_alert.notification_settings = { receiver: 'another receiver' };
|
||||
draft.grafana_alert.updated_by = {
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('CloneRuleEditor', function () {
|
||||
name: 'region: nasa',
|
||||
}).get()
|
||||
).toBeInTheDocument();
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveTextContent(grafanaRulerRule.annotations[Annotation.summary]);
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveTextContent(grafanaRulerRule.annotations![Annotation.summary]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
expect(nameInput).toHaveValue(grafanaRulerRule.grafana_alert.title);
|
||||
//check that folder is in the list
|
||||
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations[Annotation.summary]);
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations?.[Annotation.summary]);
|
||||
expect(screen.getByText('New folder')).toBeInTheDocument();
|
||||
|
||||
//check that slashed folders are not in the list
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
expect(nameInput).toHaveValue(grafanaRulerRule.grafana_alert.title);
|
||||
//check that folder is in the list
|
||||
await waitFor(() => expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title)));
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations[Annotation.summary]);
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations?.[Annotation.summary]);
|
||||
|
||||
expect(ui.manualRestoreBanner.get()).toBeInTheDocument(); // check that manual restore banner is shown
|
||||
|
||||
|
||||
@@ -1,44 +1,31 @@
|
||||
import { render as rtlRender, screen } from '@testing-library/react';
|
||||
import { Chance } from 'chance';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { SetupServer, setupServer } from 'msw/node';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
|
||||
import { rulerTestDb } from '../alerting/unified/mocks/grafanaRulerApi';
|
||||
import { alertingFactory } from '../alerting/unified/mocks/server/db';
|
||||
import { DEFAULT_FOLDERS } from '../alerting/unified/mocks/server/handlers/folders';
|
||||
|
||||
import BrowseFolderAlertingPage from './BrowseFolderAlertingPage';
|
||||
import { getPrometheusRulesResponse, getRulerRulesResponse } from './fixtures/alertRules.fixture';
|
||||
import * as permissions from './permissions';
|
||||
|
||||
function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
||||
rtlRender(<TestProvider>{ui}</TestProvider>, options);
|
||||
}
|
||||
// Use the folder and rules from the mocks
|
||||
const folder = DEFAULT_FOLDERS[0];
|
||||
const { uid: folderUid, title: folderTitle } = folder;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
unifiedAlertingEnabled: true,
|
||||
},
|
||||
}));
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useParams: jest.fn(),
|
||||
useParams: jest.fn(() => ({ uid: folderUid })),
|
||||
}));
|
||||
const mockFolderName = 'myFolder';
|
||||
const mockFolderUid = '12345';
|
||||
|
||||
const random = Chance(1);
|
||||
const rule_uid = random.guid();
|
||||
const mockRulerRulesResponse = getRulerRulesResponse(mockFolderName, mockFolderUid, rule_uid);
|
||||
const mockPrometheusRulesResponse = getPrometheusRulesResponse(mockFolderName, mockFolderUid, rule_uid);
|
||||
config.unifiedAlertingEnabled = true;
|
||||
|
||||
setupMswServer();
|
||||
|
||||
describe('browse-dashboards BrowseFolderAlertingPage', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid });
|
||||
let server: SetupServer;
|
||||
const mockPermissions = {
|
||||
canCreateDashboards: true,
|
||||
canEditDashboards: true,
|
||||
@@ -49,28 +36,6 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => {
|
||||
canSetPermissions: true,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
server = setupServer(
|
||||
http.get('/api/folders/:uid', () => {
|
||||
return HttpResponse.json({
|
||||
title: mockFolderName,
|
||||
uid: mockFolderUid,
|
||||
});
|
||||
}),
|
||||
http.get('api/ruler/grafana/api/v1/rules', () => {
|
||||
return HttpResponse.json(mockRulerRulesResponse);
|
||||
}),
|
||||
http.get('api/prometheus/grafana/api/v1/rules', () => {
|
||||
return HttpResponse.json(mockPrometheusRulesResponse);
|
||||
})
|
||||
);
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
@@ -78,12 +43,11 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('displays the folder title', async () => {
|
||||
render(<BrowseFolderAlertingPage />);
|
||||
expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('heading', { name: folderTitle })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the "Folder actions" button', async () => {
|
||||
@@ -102,7 +66,7 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => {
|
||||
};
|
||||
});
|
||||
render(<BrowseFolderAlertingPage />);
|
||||
expect(await screen.findByRole('heading', { name: mockFolderName })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('heading', { name: folderTitle })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -118,10 +82,24 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => {
|
||||
expect(await screen.findByRole('tab', { name: 'Alert rules' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('displays the alert rules returned by the API', async () => {
|
||||
it('displays rules from the folder', async () => {
|
||||
const ruleUid = 'xYz1A2b3C4';
|
||||
|
||||
const group = alertingFactory.ruler.grafana.group.build({
|
||||
name: 'test-group',
|
||||
rules: [
|
||||
alertingFactory.ruler.grafana.alertingRule.build({
|
||||
grafana_alert: { title: 'Grafana-rule', namespace_uid: folderUid, rule_group: 'test-group', uid: ruleUid },
|
||||
}),
|
||||
],
|
||||
});
|
||||
rulerTestDb.addGroup(group, { name: folderTitle, uid: folderUid });
|
||||
render(<BrowseFolderAlertingPage />);
|
||||
|
||||
const ruleName = mockPrometheusRulesResponse.data.groups[0].rules[0].name;
|
||||
expect(await screen.findByRole('link', { name: ruleName })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('heading', { name: folderTitle })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('link', { name: 'Grafana-rule' })).toHaveAttribute(
|
||||
'href',
|
||||
`/alerting/grafana/${ruleUid}/view`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { buildNavModel, getAlertingTabID } from 'app/features/folders/state/navModel';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { AlertsFolderView } from '../alerting/unified/AlertsFolderView';
|
||||
import { alertRuleApi } from '../alerting/unified/api/alertRuleApi';
|
||||
import { GRAFANA_RULER_CONFIG } from '../alerting/unified/api/featureDiscoveryApi';
|
||||
import { stringifyErrorLike } from '../alerting/unified/utils/misc';
|
||||
import { rulerRuleType } from '../alerting/unified/utils/rules';
|
||||
|
||||
import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboardsAPI';
|
||||
import { FolderActionsButton } from './components/FolderActionsButton';
|
||||
|
||||
const { useRulerNamespaceQuery } = alertRuleApi;
|
||||
|
||||
export function BrowseFolderAlertingPage() {
|
||||
const { uid: folderUID = '' } = useParams();
|
||||
const { data: folderDTO } = useGetFolderQuery(folderUID);
|
||||
const folder = useSelector((state) => state.folder);
|
||||
const { data: folderDTO, isLoading: isFolderLoading } = useGetFolderQuery(folderUID);
|
||||
|
||||
const {
|
||||
data: rulerNamespace = {},
|
||||
isLoading: isRulerNamespaceLoading,
|
||||
error: rulerNamespaceError,
|
||||
} = useRulerNamespaceQuery({
|
||||
rulerConfig: GRAFANA_RULER_CONFIG,
|
||||
namespace: folderUID,
|
||||
});
|
||||
|
||||
const [saveFolder] = useSaveFolderMutation();
|
||||
|
||||
const navModel = useMemo(() => {
|
||||
@@ -45,6 +61,12 @@ export function BrowseFolderAlertingPage() {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const isLoading = isFolderLoading || isRulerNamespaceLoading;
|
||||
const folderRules = Object.values(rulerNamespace)
|
||||
.flatMap((group) => group)
|
||||
.flatMap((group) => group.rules)
|
||||
.filter(rulerRuleType.grafana.rule);
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="dashboards/browse"
|
||||
@@ -52,8 +74,21 @@ export function BrowseFolderAlertingPage() {
|
||||
onEditTitle={onEditTitle}
|
||||
actions={<>{folderDTO && <FolderActionsButton folder={folderDTO} />}</>}
|
||||
>
|
||||
<Page.Contents>
|
||||
<AlertsFolderView folder={folder} />
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
{!folderDTO && (
|
||||
<Alert
|
||||
title={t('browse-dashboards.browse-folder-alerting-page.title-folder-not-found', 'Folder not found')}
|
||||
/>
|
||||
)}
|
||||
{!!rulerNamespaceError && (
|
||||
<Alert
|
||||
title={t('browse-dashboards.browse-folder-alerting-page.title-ruler-namespace-error', 'Cannot load rules')}
|
||||
severity="error"
|
||||
>
|
||||
{stringifyErrorLike(rulerNamespaceError)}
|
||||
</Alert>
|
||||
)}
|
||||
{folderDTO && <AlertsFolderView folder={folderDTO} rules={folderRules} />}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -301,8 +301,8 @@ export interface RulerGrafanaRuleDTO<T = GrafanaRuleDefinition> {
|
||||
grafana_alert: T;
|
||||
for?: string;
|
||||
keep_firing_for?: string;
|
||||
annotations: Annotations;
|
||||
labels: Labels;
|
||||
annotations?: Annotations;
|
||||
labels?: Labels;
|
||||
}
|
||||
|
||||
export type TopLevelGrafanaRuleDTOField = keyof Omit<RulerGrafanaRuleDTO, 'grafana_alert'>;
|
||||
|
||||
@@ -2799,6 +2799,10 @@
|
||||
"actions": {
|
||||
"button-to-recently-deleted": "Recently deleted"
|
||||
},
|
||||
"browse-folder-alerting-page": {
|
||||
"title-folder-not-found": "Folder not found",
|
||||
"title-ruler-namespace-error": "Cannot load rules"
|
||||
},
|
||||
"browse-view": {
|
||||
"this-folder-is-empty": "This folder is empty"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user