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',