diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 372f5ee8754..8e54ac4c557 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -141,7 +141,6 @@ Experimental features might be changed or removed without prior notice. | `externalCorePlugins` | Allow core plugins to be loaded as external | | `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | | `httpSLOLevels` | Adds SLO level to http request metrics | -| `alertingModifiedExport` | Enables using UI for provisioned rules modification and export | | `panelMonitoring` | Enables panel monitoring through logs and measurements | | `enableNativeHTTPHistogram` | Enables native HTTP Histograms | | `transformationsVariableSupport` | Allows using variables in transformations | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b099c5c8d1f..912bae6fb32 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -134,7 +134,6 @@ export interface FeatureToggles { idForwarding?: boolean; cloudWatchWildCardDimensionValues?: boolean; externalServiceAccounts?: boolean; - alertingModifiedExport?: boolean; panelMonitoring?: boolean; enableNativeHTTPHistogram?: boolean; transformationsVariableSupport?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9f31c513c05..76203671901 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -810,13 +810,6 @@ var ( RequiresDevMode: true, Owner: grafanaAuthnzSquad, }, - { - Name: "alertingModifiedExport", - Description: "Enables using UI for provisioned rules modification and export", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaAlertingSquad, - }, { Name: "panelMonitoring", Description: "Enables panel monitoring through logs and measurements", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d0366d7b2fa..2a03ae63894 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -115,7 +115,6 @@ httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false idForwarding,experimental,@grafana/grafana-authnz-team,true,false,false,false cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false,false externalServiceAccounts,experimental,@grafana/grafana-authnz-team,true,false,false,false -alertingModifiedExport,experimental,@grafana/alerting-squad,false,false,false,false panelMonitoring,experimental,@grafana/dataviz-squad,false,false,false,true enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false,false transformationsVariableSupport,experimental,@grafana/grafana-bi-squad,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 40e731ea05d..2b93a1f7310 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -471,10 +471,6 @@ const ( // Automatic service account and token setup for plugins FlagExternalServiceAccounts = "externalServiceAccounts" - // FlagAlertingModifiedExport - // Enables using UI for provisioned rules modification and export - FlagAlertingModifiedExport = "alertingModifiedExport" - // FlagPanelMonitoring // Enables panel monitoring through logs and measurements FlagPanelMonitoring = "panelMonitoring" diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index c1d39295cd0..5e67be4822d 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -1,6 +1,5 @@ import { uniq } from 'lodash'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPage'; @@ -247,15 +246,13 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting/:id/modify-export', pageClass: 'page-alerting', - roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate]), - component: config.featureToggles.alertingModifiedExport - ? SafeDynamicImport( - () => - import( - /* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport' - ) + roles: evaluateAccess([AccessControlAction.AlertingRuleRead]), + component: SafeDynamicImport( + () => + import( + /* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport' ) - : () => , + ), }, { path: '/alerting/:sourceName/:id/view', diff --git a/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx b/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx index ba1d61121ec..fcf0f5e6b48 100644 --- a/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx +++ b/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx @@ -7,12 +7,20 @@ import { Button, Dropdown, Icon, LinkButton, Menu, MenuItem } from '@grafana/ui' import { logInfo, LogMessages } from './Analytics'; import { GrafanaRulesExporter } from './components/export/GrafanaRulesExporter'; -import { useRulesAccess } from './utils/accessControlHooks'; +import { AlertSourceAction, useAlertSourceAbility } from './hooks/useAbilities'; interface Props {} export function MoreActionsRuleButtons({}: Props) { - const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess(); + const [_, viewRuleAllowed] = useAlertSourceAbility(AlertSourceAction.ViewAlertRule); + const [createRuleSupported, createRuleAllowed] = useAlertSourceAbility(AlertSourceAction.CreateAlertRule); + const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertSourceAbility( + AlertSourceAction.CreateExternalAlertRule + ); + + const canCreateGrafanaRules = createRuleSupported && createRuleAllowed; + const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed; + const location = useLocation(); const [showExportDrawer, toggleShowExportDrawer] = useToggle(false); const newMenu = ( @@ -25,7 +33,7 @@ export function MoreActionsRuleButtons({}: Props) { label="New recording rule" /> )} - {canReadProvisioning && } + {viewRuleAllowed && } ); diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 23487beea4d..4928fc27cb5 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -57,6 +57,9 @@ const RuleEditor = ({ match }: RuleEditorProps) => { if (identifier) { await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: identifier.ruleSourceName })); } + if (copyFromIdentifier) { + await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: copyFromIdentifier.ruleSourceName })); + } }, [dispatch]); const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 07de53cb27a..ff1eddc5d38 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -684,12 +684,27 @@ describe('RuleList', () => { describe('RBAC Enabled', () => { describe('Export button', () => { - it('Export button should be visible when the user has alert provisioning read permissions', async () => { - grantUserPermissions([AccessControlAction.AlertingProvisioningRead]); + it('Export button should be visible when the user has alert read permissions', async () => { + grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.FoldersRead]); mocks.getAllDataSourcesMock.mockReturnValue([]); setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRules.mockResolvedValue([ + mockPromRuleNamespace({ + name: 'foofolder', + dataSourceName: GRAFANA_RULES_SOURCE_NAME, + groups: [ + mockPromRuleGroup({ + name: 'grafana-group', + rules: [ + mockPromAlertingRule({ + query: '[]', + }), + ], + }), + ], + }), + ]); mocks.api.fetchRulerRules.mockResolvedValue({}); renderRuleList(); @@ -697,32 +712,6 @@ describe('RuleList', () => { await userEvent.click(ui.moreButton.get()); expect(ui.exportButton.get()).toBeInTheDocument(); }); - it('Export button should be visible when the user has alert provisioning read secrets permissions', async () => { - grantUserPermissions([AccessControlAction.AlertingProvisioningReadSecrets]); - - mocks.getAllDataSourcesMock.mockReturnValue([]); - setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); - mocks.api.fetchRulerRules.mockResolvedValue({}); - - renderRuleList(); - - await userEvent.click(ui.moreButton.get()); - expect(ui.exportButton.get()).toBeInTheDocument(); - }); - it('Export button should not be visible when the user has no alert provisioning read permissions', async () => { - grantUserPermissions([AccessControlAction.AlertingRuleCreate, AccessControlAction.FoldersRead]); - - mocks.getAllDataSourcesMock.mockReturnValue([]); - setDataSourceSrv(new MockDataSourceSrv({})); - mocks.api.fetchRules.mockResolvedValue([]); - mocks.api.fetchRulerRules.mockResolvedValue({}); - - renderRuleList(); - - await userEvent.click(ui.moreButton.get()); - 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 () => { diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index 6aaaec90e1d..655b52a9f3d 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -24,7 +24,6 @@ import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces'; import { useFilteredRules, useRulesFilter } from './hooks/useFilteredRules'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAllPromAndRulerRulesAction } from './state/actions'; -import { useRulesAccess } from './utils/accessControlHooks'; import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants'; import { getAllRulesSourceNames } from './utils/datasource'; @@ -91,8 +90,6 @@ const RuleList = withErrorBoundary( const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces(); const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState); - const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess(); - return ( // We don't want to show the Loading... indicator for the whole page. // We show separate indicators for Grafana-managed and Cloud rules @@ -116,11 +113,9 @@ const RuleList = withErrorBoundary( )} - {(canCreateGrafanaRules || canCreateCloudRules || canReadProvisioning) && ( - - - - )} + + + )} diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 05f84437288..864d68d104e 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -1,16 +1,12 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Disable, Enable } from 'react-enable'; -import { useParams } from 'react-router-dom'; -import { Button, HorizontalGroup, withErrorBoundary } from '@grafana/ui'; -import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; +import { withErrorBoundary } from '@grafana/ui'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { GrafanaRuleExporter } from './components/export/GrafanaRuleExporter'; import { AlertingFeature } from './features'; -import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; const DetailViewV1 = SafeDynamicImport(() => import('./components/rule-viewer/RuleViewer.v1')); const DetailViewV2 = SafeDynamicImport(() => import('./components/rule-viewer/v2/RuleViewer.v2')); @@ -21,25 +17,8 @@ type RuleViewerProps = GrafanaRouteComponentProps<{ }>; const RuleViewer = (props: RuleViewerProps): JSX.Element => { - const routeParams = useParams<{ type: string; id: string }>(); - const uidFromParams = routeParams.id; - - const sourceName = props.match.params.sourceName; - - const [showYaml, setShowYaml] = useState(false); - const actionButtons = - sourceName === GRAFANA_RULES_SOURCE_NAME ? ( - - - - ) : null; - return ( - - {showYaml && setShowYaml(false)} />} diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index bee885c2e0d..85c07792765 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -43,10 +43,6 @@ export interface Datasource { export const PREVIEW_URL = '/api/v1/rule/test/grafana'; export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules'; -function getProvisioningExportUrl(ruleUid: string, format: 'yaml' | 'json' | 'hcl' = 'yaml') { - return `/api/v1/provisioning/alert-rules/${ruleUid}/export?format=${format}`; -} - export interface Data { refId: string; relativeTimeRange: RelativeTimeRange; @@ -71,6 +67,13 @@ export interface Rule { export type AlertInstances = Record; +interface ExportRulesParams { + format: ExportFormats; + folderUid?: string; + group?: string; + ruleUid?: string; +} + export interface ModifyExportPayload { rules: Array; name: string; @@ -192,20 +195,10 @@ export const alertRuleApi = alertingApi.injectEndpoints({ }, }), - exportRule: build.query({ - query: ({ uid, format }) => ({ url: getProvisioningExportUrl(uid, format), responseType: 'text' }), - }), - exportRuleGroup: build.query({ - query: ({ folderUid, groupName, format }) => ({ - url: `/api/v1/provisioning/folder/${folderUid}/rule-groups/${groupName}/export`, - params: { format: format }, - responseType: 'text', - }), - }), - exportRules: build.query({ - query: ({ format }) => ({ - url: `/api/v1/provisioning/alert-rules/export`, - params: { format: format }, + exportRules: build.query({ + query: ({ format, folderUid, group, ruleUid }) => ({ + url: `/api/ruler/grafana/api/v1/export/rules`, + params: { format: format, folderUid: folderUid, group: group, ruleUid: ruleUid }, responseType: 'text', }), }), diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx new file mode 100644 index 00000000000..0f44e23c14f --- /dev/null +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.test.tsx @@ -0,0 +1,138 @@ +import { render, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import { AutoSizerProps } from 'react-virtualized-auto-sizer'; +import { byRole, byTestId, byText } from 'testing-library-selector'; + +import { selectors } from '@grafana/e2e-selectors'; +import { locationService } from '@grafana/runtime'; + +import { TestProvider } from '../../../../../../test/helpers/TestProvider'; +import { AlertmanagerChoice } from '../../../../../plugins/datasource/alertmanager/types'; +import { DashboardSearchItemType } from '../../../../search/types'; +import { mockAlertRuleApi, mockApi, mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi'; +import { getGrafanaRule, mockDataSource } from '../../mocks'; +import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi'; +import { setupDataSources } from '../../testSetup/datasources'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; + +import GrafanaModifyExport from './GrafanaModifyExport'; + +jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ + AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
{actions}
, +})); + +jest.mock('react-virtualized-auto-sizer', () => { + return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 }); +}); +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: ({ value }: { value: string }) =>