From 13ebcf1d2c80a6d5f32e188772134d22ce25c270 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 9 May 2025 16:20:45 +0200 Subject: [PATCH] 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 --- .../unified/AlertsFolderView.test.tsx | 142 +++--------------- .../alerting/unified/AlertsFolderView.tsx | 65 ++++---- .../alerting/unified/mocks/server/db.ts | 32 ++++ .../unified/mocks/server/handlers/folders.ts | 6 +- .../mocks/server/handlers/grafanaRuler.ts | 3 + .../rule-editor/CloneRuleEditor.test.tsx | 2 +- .../rule-editor/RuleEditorExisting.test.tsx | 2 +- .../RuleEditorGrafanaRules.test.tsx | 2 +- .../BrowseFolderAlertingPage.test.tsx | 90 +++++------ .../BrowseFolderAlertingPage.tsx | 45 +++++- public/app/types/unified-alerting-dto.ts | 4 +- public/locales/en-US/grafana.json | 4 + 12 files changed, 176 insertions(+), 221 deletions(-) diff --git a/public/app/features/alerting/unified/AlertsFolderView.test.tsx b/public/app/features/alerting/unified/AlertsFolderView.test.tsx index 05f88cff4c5..ed79fa1feb0 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.test.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.test.tsx @@ -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 => { - 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(); + render(); // 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(); - - // 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(); + const { user } = render(); 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(); + const { user } = render(); await user.type(ui.filter.label.get(), 'severity=critical'); diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx index 3308e9b952e..d5996c696e7 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.tsx @@ -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> = [ { 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) => { - {pageItems.map((currentRule) => ( + {pageItems.map(({ grafana_alert, labels = {} }) => ( - {currentRule.name} + {grafana_alert.title} `${label}=${value}`)} + tags={Object.entries(labels).map(([label, value]) => `${label}=${value}`)} /> @@ -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) => ({ diff --git a/public/app/features/alerting/unified/mocks/server/db.ts b/public/app/features/alerting/unified/mocks/server/db.ts index fdb7baddeb9..a100519b4b4 100644 --- a/public/app/features/alerting/unified/mocks/server/db.ts +++ b/public/app/features/alerting/unified/mocks/server/db.ts @@ -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, { } ); +const rulerGrafanaGroupFactory = Factory.define(({ sequence }) => ({ + name: `test-group-${sequence}`, + interval: '1m', + rules: grafanaAlertingRuleFactory.buildList(3), +})); + class DataSourceFactory extends Factory { vanillaPrometheus() { return this.params({ uid: PROMETHEUS_DATASOURCE_UID, name: 'Prometheus' }); @@ -179,6 +189,26 @@ const grafanaRecordingRule = Factory.define>( + ({ 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 { 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, diff --git a/public/app/features/alerting/unified/mocks/server/handlers/folders.ts b/public/app/features/alerting/unified/mocks/server/handlers/folders.ts index a08d7fb63ba..a1599b815e1 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/folders.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/folders.ts @@ -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, diff --git a/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts b/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts index 36fb9b1318d..6177a07618d 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts @@ -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 = { diff --git a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx index c59101fca8e..fd04f5c3b8e 100644 --- a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx @@ -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]); }); }); diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx index b8f3992e31e..9a99b587bcd 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx @@ -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 diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx index 0c9433406c7..f6267271e56 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx @@ -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 diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx index 727793d04d2..bc2ffeb8caa 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx @@ -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) { - rtlRender({ui}, 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(); - 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(); - 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(); - 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` + ); }); }); diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx index 74931bc2170..b031a3c4c5b 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx @@ -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 ( {folderDTO && }} > - - + + {!folderDTO && ( + + )} + {!!rulerNamespaceError && ( + + {stringifyErrorLike(rulerNamespaceError)} + + )} + {folderDTO && } ); diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index cb5bd7e0237..d8793729370 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -301,8 +301,8 @@ export interface RulerGrafanaRuleDTO { grafana_alert: T; for?: string; keep_firing_for?: string; - annotations: Annotations; - labels: Labels; + annotations?: Annotations; + labels?: Labels; } export type TopLevelGrafanaRuleDTOField = keyof Omit; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6a39e0815a4..eb2c51b4ac6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" },