Alerting: Add GMA export to the new list page (#109784)

* Add GMA export to the More menu action on the list page

* Add Grafana rules export option to the new list page

* Update translations
This commit is contained in:
Konrad Lalik
2025-08-19 09:08:15 +02:00
committed by GitHub
parent 2f4166e456
commit dff7050aaa
4 changed files with 111 additions and 4 deletions
@@ -645,7 +645,7 @@ export const grantUserPermissions = (permissions: AccessControlAction[]) => {
};
export const grantUserRole = (role: string) => {
jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true);
jest.spyOn(contextSrv, 'hasRole').mockImplementation((checkRole) => checkRole === role);
};
export function mockUnifiedAlertingStore(unifiedAlerting?: Partial<StoreState['unifiedAlerting']>) {
@@ -1,11 +1,14 @@
import { HttpResponse } from 'msw';
import { render } from 'test/test-utils';
import { byRole, byTestId } from 'testing-library-selector';
import { OrgRole } from '@grafana/data';
import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
import { AccessControlAction } from 'app/types/accessControl';
import { setupMswServer } from '../mockApi';
import { grantUserPermissions } from '../mocks';
import { grantUserPermissions, grantUserRole } from '../mocks';
import { setGrafanaRuleGroupExportResolver } from '../mocks/server/configure';
import { alertingFactory } from '../mocks/server/db';
import { RulesFilter } from '../search/rulesSearchParser';
import { testWithFeatureToggles } from '../test/test-utils';
@@ -162,11 +165,16 @@ describe('RuleListActions', () => {
newAlertRuleForExport: byRole('link', { name: /new alert rule for export/i }),
newGrafanaRecordingRule: byRole('link', { name: /new grafana recording rule/i }),
newDataSourceRecordingRule: byRole('link', { name: /new data source recording rule/i }),
importAlertRules: byRole('link', { name: /import alert rules/i }),
exportAllGrafanaRules: byRole('menuitem', { name: /export all grafana rules/i }),
},
exportDrawer: byRole('dialog', { name: /export/i }),
};
beforeEach(() => {
jest.clearAllMocks();
// Default to Viewer role (non-admin)
grantUserRole(OrgRole.Viewer);
});
it.each([
@@ -243,6 +251,90 @@ describe('RuleListActions', () => {
expect(ui.menuOptions.newGrafanaRecordingRule.query(menu)).toBeInTheDocument();
expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).toBeInTheDocument();
});
describe('Import Alert Rules', () => {
testWithFeatureToggles(['alertingMigrationUI']);
it('should show "Import alert rules" option when user is admin and feature toggle is enabled', async () => {
grantUserRole(OrgRole.Admin);
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
const { user } = render(<RuleListActions />);
await user.click(ui.moreButton.get());
const menu = await ui.moreMenu.find();
expect(ui.menuOptions.importAlertRules.query(menu)).toBeInTheDocument();
});
it('should not show "Import alert rules" option when user is not admin', async () => {
// Keep default Viewer role
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
const { user } = render(<RuleListActions />);
await user.click(ui.moreButton.get());
const menu = await ui.moreMenu.find();
expect(ui.menuOptions.importAlertRules.query(menu)).not.toBeInTheDocument();
});
it('should have correct URL for "Import alert rules" menu item', async () => {
grantUserRole(OrgRole.Admin);
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
const { user } = render(<RuleListActions />);
await user.click(ui.moreButton.get());
const menu = await ui.moreMenu.find();
const importMenuItem = ui.menuOptions.importAlertRules.get(menu);
expect(importMenuItem).toHaveAttribute('href', '/alerting/import-datasource-managed-rules');
});
});
describe('Export All Grafana Rules', () => {
it('should show "Export all Grafana rules" option when user has export permissions', async () => {
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
const { user } = render(<RuleListActions />);
await user.click(ui.moreButton.get());
const menu = await ui.moreMenu.find();
expect(ui.menuOptions.exportAllGrafanaRules.query(menu)).toBeInTheDocument();
});
it('should not show "Export all Grafana rules" option when user lacks export permissions', async () => {
grantUserPermissions([]); // No permissions
const { user } = render(<RuleListActions />);
await user.click(ui.moreButton.get());
const menu = await ui.moreMenu.find();
expect(ui.menuOptions.exportAllGrafanaRules.query(menu)).not.toBeInTheDocument();
});
it('should open export drawer when "Export all Grafana rules" is clicked', async () => {
// Set up MSW mock for export endpoint
setGrafanaRuleGroupExportResolver(() => {
return HttpResponse.text('# Mock YAML export content\ngroups: []');
});
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
const { user } = render(<RuleListActions />);
await user.click(ui.moreButton.get());
const menu = await ui.moreMenu.find();
const exportMenuItem = ui.menuOptions.exportAllGrafanaRules.get(menu);
await user.click(exportMenuItem);
expect(ui.exportDrawer.query()).toBeInTheDocument();
});
});
});
describe('RuleList v2 - View switching', () => {
@@ -1,10 +1,12 @@
import { useMemo } from 'react';
import { useToggle } from 'react-use';
import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui';
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
import { GrafanaRulesExporter } from '../components/export/GrafanaRulesExporter';
import RulesFilter from '../components/rules/Filter/RulesFilter';
import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelector';
import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton';
@@ -35,13 +37,17 @@ function RuleList() {
export function RuleListActions() {
const [createGrafanaRuleSupported, createGrafanaRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const canCreateGrafanaRules = createGrafanaRuleSupported && createGrafanaRuleAllowed;
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
const canExportRules = exportRulesSupported && exportRulesAllowed;
const canCreateRules = canCreateGrafanaRules || canCreateCloudRules;
const canImportRulesToGMA = isAdmin() && config.featureToggles.alertingMigrationUI;
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const moreActionsMenu = useMemo(
() => (
<Menu>
@@ -51,10 +57,17 @@ export function RuleListActions() {
icon="file-export"
url="/alerting/export-new-rule"
/>
{canExportRules && (
<Menu.Item
label={t('alerting.rule-list.export-all-grafana-rules', 'Export all Grafana rules')}
icon="download-alt"
onClick={toggleShowExportDrawer}
/>
)}
{canImportRulesToGMA && (
<Menu.Item
label={t('alerting.rule-list-v2.import-to-gma', 'Import alert rules')}
icon="import"
icon="upload"
url="/alerting/import-datasource-managed-rules"
/>
)}
@@ -77,7 +90,7 @@ export function RuleListActions() {
</Menu.Group>
</Menu>
),
[canCreateGrafanaRules, canCreateCloudRules, canImportRulesToGMA]
[canCreateGrafanaRules, canCreateCloudRules, canImportRulesToGMA, canExportRules, toggleShowExportDrawer]
);
return (
@@ -93,6 +106,7 @@ export function RuleListActions() {
<Trans i18nKey="alerting.rule-list.more">More</Trans> <Icon name="angle-down" />
</Button>
</Dropdown>
{canExportRules && showExportDrawer && <GrafanaRulesExporter onClose={toggleShowExportDrawer} />}
</Stack>
);
}
+1
View File
@@ -2467,6 +2467,7 @@
},
"empty-data-source": "No rules found",
"error-button": "Error",
"export-all-grafana-rules": "Export all Grafana rules",
"filter-view": {
"cancel-search": "Cancel search",
"no-more-results": "No more results – found {{numberOfRules}} rules",