From d44f250aea3269daf0b830c5569de078dd018b2e Mon Sep 17 00:00:00 2001
From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
Date: Mon, 30 Jan 2023 11:13:08 +0100
Subject: [PATCH] Alerting: Add export button for exporting all alert rules in
alert list view (#62416)
* Add export button for exporting all alert rules in alert list view
* Add RBAC for export button
* Use provisioningPermissions.read in getRulesAccess method instead of directly using AccessControlAction.AlertingProvisioningRead
---
.../alerting/unified/RuleList.test.tsx | 33 +++++++++++++++++--
.../features/alerting/unified/RuleList.tsx | 30 +++++++++++------
.../unified/hooks/useFilteredRules.ts | 2 +-
.../alerting/unified/utils/access-control.ts | 6 ++++
public/app/types/accessControl.ts | 4 +++
5 files changed, 62 insertions(+), 13 deletions(-)
diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx
index 44a8508b672..115a777edfe 100644
--- a/public/app/features/alerting/unified/RuleList.test.tsx
+++ b/public/app/features/alerting/unified/RuleList.test.tsx
@@ -1,12 +1,12 @@
import { SerializedError } from '@reduxjs/toolkit';
-import { render, waitFor, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { byRole, byTestId, byText } from 'testing-library-selector';
-import { locationService, setDataSourceSrv, logInfo, setBackendSrv } from '@grafana/runtime';
+import { locationService, logInfo, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
@@ -123,6 +123,7 @@ const ui = {
editCloudGroupIcon: byTestId('edit-group'),
newRuleButton: byRole('link', { name: 'New alert rule' }),
+ exportButton: byRole('button', { name: /export/i }),
editGroupModal: {
namespaceInput: byRole('textbox', { hidden: true, name: /namespace/i }),
@@ -681,6 +682,34 @@ describe('RuleList', () => {
});
describe('RBAC Enabled', () => {
+ describe('Export button', () => {
+ it('Export button should be visible when the user has alert provisioning read permissions', async () => {
+ enableRBAC();
+
+ grantUserPermissions([AccessControlAction.AlertingProvisioningRead]);
+
+ mocks.getAllDataSourcesMock.mockReturnValue([]);
+ setDataSourceSrv(new MockDataSourceSrv({}));
+ mocks.api.fetchRules.mockResolvedValue([]);
+ mocks.api.fetchRulerRules.mockResolvedValue({});
+
+ renderRuleList();
+
+ expect(ui.exportButton.get()).toBeInTheDocument();
+ });
+ it('Export button should not be visible when the user has no alert provisioning read permissions', async () => {
+ enableRBAC();
+
+ mocks.getAllDataSourcesMock.mockReturnValue([]);
+ setDataSourceSrv(new MockDataSourceSrv({}));
+ mocks.api.fetchRules.mockResolvedValue([]);
+ mocks.api.fetchRulerRules.mockResolvedValue({});
+
+ renderRuleList();
+
+ expect(ui.exportButton.query()).not.toBeInTheDocument();
+ });
+ });
describe('Grafana Managed Alerts', () => {
it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => {
enableRBAC();
diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx
index c819a0b81bc..bf354bc42eb 100644
--- a/public/app/features/alerting/unified/RuleList.tsx
+++ b/public/app/features/alerting/unified/RuleList.tsx
@@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
import { useAsyncFn, useInterval } from 'react-use';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
+import { Stack } from '@grafana/experimental';
import { logInfo } from '@grafana/runtime';
import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
@@ -32,6 +33,8 @@ const VIEWS = {
state: RuleListStateView,
};
+const onExport = () => window.open(`/api/v1/provisioning/alert-rules/export?download=true`);
+
const RuleList = withErrorBoundary(
() => {
const dispatch = useDispatch();
@@ -43,7 +46,7 @@ const RuleList = withErrorBoundary(
const [queryParams] = useQueryParams();
const { filterState, hasActiveFilters } = useRulesFilter();
- const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess();
+ const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess();
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
? (queryParams['view'] as keyof typeof VIEWS)
@@ -106,15 +109,22 @@ const RuleList = withErrorBoundary(
)}
- {(canCreateGrafanaRules || canCreateCloudRules) && (
- logInfo(LogMessages.alertRuleFromScratch)}
- >
- New alert rule
-
- )}
+
+ {canReadProvisioning && (
+
+ )}
+ {(canCreateGrafanaRules || canCreateCloudRules) && (
+ logInfo(LogMessages.alertRuleFromScratch)}
+ >
+ New alert rule
+
+ )}
+
>
)}
diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
index 061c90a7ead..80666cb91ed 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
@@ -7,7 +7,7 @@ import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
-import { getSearchFilterFromQuery, RulesFilter, applySearchFilterToQuery } from '../search/rulesSearchParser';
+import { applySearchFilterToQuery, getSearchFilterFromQuery, RulesFilter } from '../search/rulesSearchParser';
import { labelsMatchMatchers, matcherToMatcherField, parseMatcher, parseMatchers } from '../utils/alertmanager';
import { isCloudRulesSource } from '../utils/datasource';
import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } from '../utils/rules';
diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts
index 1ccd6399ab0..767b5b2c127 100644
--- a/public/app/features/alerting/unified/utils/access-control.ts
+++ b/public/app/features/alerting/unified/utils/access-control.ts
@@ -47,6 +47,11 @@ export const notificationsPermissions = {
},
};
+export const provisioningPermissions = {
+ read: AccessControlAction.AlertingProvisioningRead,
+ write: AccessControlAction.AlertingProvisioningWrite,
+};
+
const rulesPermissions = {
read: {
grafana: AccessControlAction.AlertingRuleRead,
@@ -118,5 +123,6 @@ export function getRulesAccess() {
rulesSourceName === GRAFANA_RULES_SOURCE_NAME ? contextSrv.hasEditPermissionInFolders : contextSrv.isEditor;
return contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, permissionFallback);
},
+ canReadProvisioning: contextSrv.hasAccess(provisioningPermissions.read, contextSrv.isGrafanaAdmin),
};
}
diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts
index ef517ce387c..010f64426a5 100644
--- a/public/app/types/accessControl.ts
+++ b/public/app/types/accessControl.ts
@@ -117,6 +117,10 @@ export enum AccessControlAction {
AlertingNotificationsExternalWrite = 'alert.notifications.external:write',
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
+ // Alerting provisioning actions
+ AlertingProvisioningRead = 'alert.provisioning:read',
+ AlertingProvisioningWrite = 'alert.provisioning:write',
+
ActionAPIKeysRead = 'apikeys:read',
ActionAPIKeysCreate = 'apikeys:create',
ActionAPIKeysDelete = 'apikeys:delete',