From dff7050aaac3a7ef502f6e91a6a9a99421e9408a Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 19 Aug 2025 09:08:15 +0200 Subject: [PATCH] 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 --- public/app/features/alerting/unified/mocks.ts | 2 +- .../unified/rule-list/RuleList.v2.test.tsx | 94 ++++++++++++++++++- .../unified/rule-list/RuleList.v2.tsx | 18 +++- public/locales/en-US/grafana.json | 1 + 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 0202406bf8e..6378aa33ddc 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -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) { diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx index 9417ffa2230..3b150a698df 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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', () => { diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index a677cb1d248..7f0ae393755 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -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( () => ( @@ -51,10 +57,17 @@ export function RuleListActions() { icon="file-export" url="/alerting/export-new-rule" /> + {canExportRules && ( + + )} {canImportRulesToGMA && ( )} @@ -77,7 +90,7 @@ export function RuleListActions() { ), - [canCreateGrafanaRules, canCreateCloudRules, canImportRulesToGMA] + [canCreateGrafanaRules, canCreateCloudRules, canImportRulesToGMA, canExportRules, toggleShowExportDrawer] ); return ( @@ -93,6 +106,7 @@ export function RuleListActions() { More + {canExportRules && showExportDrawer && } ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c49a81b5ae7..7ca4d97c8e3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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",