mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
a9d44aa795
commit
d44f250aea
@ -1,12 +1,12 @@
|
|||||||
import { SerializedError } from '@reduxjs/toolkit';
|
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 userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
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 { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
|
||||||
@ -123,6 +123,7 @@ const ui = {
|
|||||||
editCloudGroupIcon: byTestId('edit-group'),
|
editCloudGroupIcon: byTestId('edit-group'),
|
||||||
|
|
||||||
newRuleButton: byRole('link', { name: 'New alert rule' }),
|
newRuleButton: byRole('link', { name: 'New alert rule' }),
|
||||||
|
exportButton: byRole('button', { name: /export/i }),
|
||||||
|
|
||||||
editGroupModal: {
|
editGroupModal: {
|
||||||
namespaceInput: byRole('textbox', { hidden: true, name: /namespace/i }),
|
namespaceInput: byRole('textbox', { hidden: true, name: /namespace/i }),
|
||||||
@ -681,6 +682,34 @@ describe('RuleList', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('RBAC Enabled', () => {
|
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', () => {
|
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 () => {
|
it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => {
|
||||||
enableRBAC();
|
enableRBAC();
|
||||||
|
@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
|
|||||||
import { useAsyncFn, useInterval } from 'react-use';
|
import { useAsyncFn, useInterval } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||||
|
import { Stack } from '@grafana/experimental';
|
||||||
import { logInfo } from '@grafana/runtime';
|
import { logInfo } from '@grafana/runtime';
|
||||||
import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
|
import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
@ -32,6 +33,8 @@ const VIEWS = {
|
|||||||
state: RuleListStateView,
|
state: RuleListStateView,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExport = () => window.open(`/api/v1/provisioning/alert-rules/export?download=true`);
|
||||||
|
|
||||||
const RuleList = withErrorBoundary(
|
const RuleList = withErrorBoundary(
|
||||||
() => {
|
() => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -43,7 +46,7 @@ const RuleList = withErrorBoundary(
|
|||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
const { filterState, hasActiveFilters } = useRulesFilter();
|
const { filterState, hasActiveFilters } = useRulesFilter();
|
||||||
|
|
||||||
const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess();
|
const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess();
|
||||||
|
|
||||||
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
|
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
|
||||||
? (queryParams['view'] as keyof typeof VIEWS)
|
? (queryParams['view'] as keyof typeof VIEWS)
|
||||||
@ -106,15 +109,22 @@ const RuleList = withErrorBoundary(
|
|||||||
)}
|
)}
|
||||||
<RuleStats namespaces={filteredNamespaces} includeTotal />
|
<RuleStats namespaces={filteredNamespaces} includeTotal />
|
||||||
</div>
|
</div>
|
||||||
{(canCreateGrafanaRules || canCreateCloudRules) && (
|
<Stack direction="row" gap={0.5}>
|
||||||
<LinkButton
|
{canReadProvisioning && (
|
||||||
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
|
<Button icon="download-alt" type="button" onClick={onExport}>
|
||||||
icon="plus"
|
Export
|
||||||
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
|
</Button>
|
||||||
>
|
)}
|
||||||
New alert rule
|
{(canCreateGrafanaRules || canCreateCloudRules) && (
|
||||||
</LinkButton>
|
<LinkButton
|
||||||
)}
|
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
|
||||||
|
icon="plus"
|
||||||
|
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
|
||||||
|
>
|
||||||
|
New alert rule
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -7,7 +7,7 @@ import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
|||||||
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
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 { labelsMatchMatchers, matcherToMatcherField, parseMatcher, parseMatchers } from '../utils/alertmanager';
|
||||||
import { isCloudRulesSource } from '../utils/datasource';
|
import { isCloudRulesSource } from '../utils/datasource';
|
||||||
import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } from '../utils/rules';
|
import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } from '../utils/rules';
|
||||||
|
@ -47,6 +47,11 @@ export const notificationsPermissions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const provisioningPermissions = {
|
||||||
|
read: AccessControlAction.AlertingProvisioningRead,
|
||||||
|
write: AccessControlAction.AlertingProvisioningWrite,
|
||||||
|
};
|
||||||
|
|
||||||
const rulesPermissions = {
|
const rulesPermissions = {
|
||||||
read: {
|
read: {
|
||||||
grafana: AccessControlAction.AlertingRuleRead,
|
grafana: AccessControlAction.AlertingRuleRead,
|
||||||
@ -118,5 +123,6 @@ export function getRulesAccess() {
|
|||||||
rulesSourceName === GRAFANA_RULES_SOURCE_NAME ? contextSrv.hasEditPermissionInFolders : contextSrv.isEditor;
|
rulesSourceName === GRAFANA_RULES_SOURCE_NAME ? contextSrv.hasEditPermissionInFolders : contextSrv.isEditor;
|
||||||
return contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, permissionFallback);
|
return contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, permissionFallback);
|
||||||
},
|
},
|
||||||
|
canReadProvisioning: contextSrv.hasAccess(provisioningPermissions.read, contextSrv.isGrafanaAdmin),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,10 @@ export enum AccessControlAction {
|
|||||||
AlertingNotificationsExternalWrite = 'alert.notifications.external:write',
|
AlertingNotificationsExternalWrite = 'alert.notifications.external:write',
|
||||||
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
|
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
|
||||||
|
|
||||||
|
// Alerting provisioning actions
|
||||||
|
AlertingProvisioningRead = 'alert.provisioning:read',
|
||||||
|
AlertingProvisioningWrite = 'alert.provisioning:write',
|
||||||
|
|
||||||
ActionAPIKeysRead = 'apikeys:read',
|
ActionAPIKeysRead = 'apikeys:read',
|
||||||
ActionAPIKeysCreate = 'apikeys:create',
|
ActionAPIKeysCreate = 'apikeys:create',
|
||||||
ActionAPIKeysDelete = 'apikeys:delete',
|
ActionAPIKeysDelete = 'apikeys:delete',
|
||||||
|
Loading…
Reference in New Issue
Block a user