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
This commit is contained in:
Sonia Aguilar 2023-01-30 11:13:08 +01:00 committed by GitHub
parent a9d44aa795
commit d44f250aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 62 additions and 13 deletions

View File

@ -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();

View File

@ -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(
)}
<RuleStats namespaces={filteredNamespaces} includeTotal />
</div>
{(canCreateGrafanaRules || canCreateCloudRules) && (
<LinkButton
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
New alert rule
</LinkButton>
)}
<Stack direction="row" gap={0.5}>
{canReadProvisioning && (
<Button icon="download-alt" type="button" onClick={onExport}>
Export
</Button>
)}
{(canCreateGrafanaRules || canCreateCloudRules) && (
<LinkButton
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
New alert rule
</LinkButton>
)}
</Stack>
</div>
</>
)}

View File

@ -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';

View File

@ -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),
};
}

View File

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