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>
pull/104579/head^2
Konrad Lalik 2 months ago committed by GitHub
parent a687c4a757
commit 13ebcf1d2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 142
      public/app/features/alerting/unified/AlertsFolderView.test.tsx
  2. 65
      public/app/features/alerting/unified/AlertsFolderView.tsx
  3. 32
      public/app/features/alerting/unified/mocks/server/db.ts
  4. 6
      public/app/features/alerting/unified/mocks/server/handlers/folders.ts
  5. 3
      public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts
  6. 2
      public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx
  7. 2
      public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx
  8. 2
      public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
  9. 92
      public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx
  10. 45
      public/app/features/browse-dashboards/BrowseFolderAlertingPage.tsx
  11. 4
      public/app/types/unified-alerting-dto.ts
  12. 4
      public/locales/en-US/grafana.json

@ -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 alertRules = matchingNamespace?.groups.flatMap((group) => group.rules) ?? [];
const filteredRules = filterAndSortRules(alertRules, nameFilter, labelFilter, sortOrder ?? SortOrder.Ascending);
const filteredRules = filterAndSortRules(rules, nameFilter, labelFilter, sortOrder ?? SortOrder.Ascending);
const hasNoResults = filteredRules.length === 0;
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, (rule) => rule.grafana_alert.title.toLowerCase(), [
sortOrder === SortOrder.Ascending ? 'asc' : 'desc',
]);
}
return orderBy(rules, (x) => x.name.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);
}
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
config: {
...jest.requireActual('@grafana/runtime').config,
unifiedAlertingEnabled: true,
},
}));
// Use the folder and rules from the mocks
const folder = DEFAULT_FOLDERS[0];
const { uid: folderUid, title: folderTitle } = folder;
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"
},

Loading…
Cancel
Save