diff --git a/.betterer.results b/.betterer.results
index 404054712b4..8e2ee8c27e9 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -2333,12 +2333,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
- "public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"]
- ],
"public/app/features/alerting/unified/components/rule-editor/AnnotationKeyInput.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
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 6b78f7762f0..9c06877d5aa 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -141,6 +141,7 @@ 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 |
## Development feature toggles
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 0da1820f5c6..a7b6d374bd9 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -134,4 +134,5 @@ export interface FeatureToggles {
idForwarding?: boolean;
cloudWatchWildCardDimensionValues?: boolean;
externalServiceAccounts?: boolean;
+ alertingModifiedExport?: boolean;
}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 9ffe55bc9da..850200fadba 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -808,5 +808,12 @@ var (
RequiresDevMode: true,
Owner: grafanaAuthnzSquad,
},
+ {
+ Name: "alertingModifiedExport",
+ Description: "Enables using UI for provisioned rules modification and export",
+ Stage: FeatureStageExperimental,
+ FrontendOnly: false,
+ Owner: grafanaAlertingSquad,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 805502d12f1..e7c71f8f1e5 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -115,3 +115,4 @@ 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
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 9e309ffa662..6a2aaefc12f 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -470,4 +470,8 @@ const (
// FlagExternalServiceAccounts
// Automatic service account and token setup for plugins
FlagExternalServiceAccounts = "externalServiceAccounts"
+
+ // FlagAlertingModifiedExport
+ // Enables using UI for provisioned rules modification and export
+ FlagAlertingModifiedExport = "alertingModifiedExport"
)
diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index 66e2b708fd7..c1d39295cd0 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -1,5 +1,6 @@
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';
@@ -243,6 +244,19 @@ const unifiedRoutes: RouteDescriptor[] = [
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
),
},
+ {
+ 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'
+ )
+ )
+ : () => ,
+ },
{
path: '/alerting/:sourceName/:id/view',
pageClass: 'page-alerting',
diff --git a/public/app/features/alerting/unified/CloneRuleEditor.tsx b/public/app/features/alerting/unified/CloneRuleEditor.tsx
index 249261b064a..76586428e1e 100644
--- a/public/app/features/alerting/unified/CloneRuleEditor.tsx
+++ b/public/app/features/alerting/unified/CloneRuleEditor.tsx
@@ -9,7 +9,7 @@ import { useDispatch } from '../../../types';
import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting';
import { RulerRuleDTO } from '../../../types/unified-alerting-dto';
-import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
+import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
import { fetchEditableRuleAction } from './state/actions';
import { generateCopiedName } from './utils/duplicate';
import { rulerRuleToFormValues } from './utils/rule-form';
diff --git a/public/app/features/alerting/unified/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/ExistingRuleEditor.tsx
index cab88173af3..d5b10acb2af 100644
--- a/public/app/features/alerting/unified/ExistingRuleEditor.tsx
+++ b/public/app/features/alerting/unified/ExistingRuleEditor.tsx
@@ -6,7 +6,7 @@ import { useDispatch } from 'app/types';
import { RuleIdentifier } from 'app/types/unified-alerting';
import { AlertWarning } from './AlertWarning';
-import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
+import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchEditableRuleAction } from './state/actions';
diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx
index 159b0c8b291..23487beea4d 100644
--- a/public/app/features/alerting/unified/RuleEditor.tsx
+++ b/public/app/features/alerting/unified/RuleEditor.tsx
@@ -11,7 +11,7 @@ import { AlertWarning } from './AlertWarning';
import { CloneRuleEditor } from './CloneRuleEditor';
import { ExistingRuleEditor } from './ExistingRuleEditor';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
-import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
+import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
import { useURLSearchParams } from './hooks/useURLSearchParams';
import { fetchRulesSourceBuildInfoAction } from './state/actions';
import { useRulesAccess } from './utils/accessControlHooks';
diff --git a/public/app/features/alerting/unified/components/export/GrafanaExportDrawer.tsx b/public/app/features/alerting/unified/components/export/GrafanaExportDrawer.tsx
index 01201433084..80daa8cdb8a 100644
--- a/public/app/features/alerting/unified/components/export/GrafanaExportDrawer.tsx
+++ b/public/app/features/alerting/unified/components/export/GrafanaExportDrawer.tsx
@@ -12,6 +12,7 @@ interface GrafanaExportDrawerProps {
children: React.ReactNode;
onClose: () => void;
formatProviders: Array>;
+ title?: string;
}
export function GrafanaExportDrawer({
@@ -20,15 +21,15 @@ export function GrafanaExportDrawer({
children,
onClose,
formatProviders,
+ title = 'Export',
}: GrafanaExportDrawerProps) {
const grafanaRulesTabs = Object.values(formatProviders).map((provider) => ({
label: provider.name,
value: provider.exportFormat,
}));
-
return (
tabs={grafanaRulesTabs} setActiveTab={onTabChange} activeTab={activeTab} />
diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx
new file mode 100644
index 00000000000..3dffb337a74
--- /dev/null
+++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx
@@ -0,0 +1,112 @@
+import { omit } from 'lodash';
+import * as React from 'react';
+import { useEffect, useState } from 'react';
+import { useAsync } from 'react-use';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, LoadingPlaceholder } from '@grafana/ui';
+
+import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
+import { useDispatch } from '../../../../../types';
+import { RuleIdentifier, RuleWithLocation } from '../../../../../types/unified-alerting';
+import { RulerRuleDTO } from '../../../../../types/unified-alerting-dto';
+import { fetchEditableRuleAction, fetchRulesSourceBuildInfoAction } from '../../state/actions';
+import { RuleFormValues } from '../../types/rule-form';
+import { rulerRuleToFormValues } from '../../utils/rule-form';
+import * as ruleId from '../../utils/rule-id';
+import { isGrafanaRulerRule } from '../../utils/rules';
+import { createUrl } from '../../utils/url';
+import { AlertingPageWrapper } from '../AlertingPageWrapper';
+import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm';
+
+interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {}
+
+// TODO Duplicated in AlertRuleForm
+const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => {
+ return {
+ ...ruleDefinition,
+ queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')),
+ };
+};
+
+function formValuesFromExistingRule(rule: RuleWithLocation) {
+ return ignoreHiddenQueries(rulerRuleToFormValues(rule));
+}
+
+export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
+ const dispatch = useDispatch();
+
+ // Get rule source build info
+ const [ruleIdentifier, setRuleIdentifier] = useState(undefined);
+
+ useEffect(() => {
+ const identifier = ruleId.tryParse(match.params.id, true);
+ setRuleIdentifier(identifier);
+ }, [match.params.id]);
+
+ const { loading: loadingBuildInfo = true } = useAsync(async () => {
+ if (ruleIdentifier) {
+ await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: ruleIdentifier.ruleSourceName }));
+ }
+ }, [dispatch, ruleIdentifier]);
+
+ // Get rule
+ const {
+ loading,
+ value: alertRule,
+ error,
+ } = useAsync(async () => {
+ if (!ruleIdentifier) {
+ return;
+ }
+ return await dispatch(fetchEditableRuleAction(ruleIdentifier)).unwrap();
+ }, [ruleIdentifier, loadingBuildInfo]);
+
+ if (!ruleIdentifier) {
+ return Rule not found
;
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+
+ if (!alertRule && !loading && !loadingBuildInfo) {
+ // alert rule does not exist
+ return (
+
+ locationService.replace(createUrl('/alerting/list'))}
+ />
+
+ );
+ }
+
+ if (alertRule && !isGrafanaRulerRule(alertRule.rule)) {
+ // alert rule exists but is not a grafana-managed rule
+ return (
+
+ locationService.replace(createUrl('/alerting/list'))}
+ />
+
+ );
+ }
+
+ return (
+
+ {alertRule && }
+
+ );
+}
diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx
new file mode 100644
index 00000000000..ad8158e7b77
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+
+import { Field, Input, Text } from '@grafana/ui';
+
+import { RuleFormType, RuleFormValues } from '../../types/rule-form';
+
+import { RuleEditorSection } from './RuleEditorSection';
+
+const recordingRuleNameValidationPattern = {
+ message:
+ 'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
+ value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
+};
+
+export const AlertRuleNameInput = () => {
+ const {
+ register,
+ watch,
+ formState: { errors },
+ } = useFormContext();
+
+ const ruleFormType = watch('type');
+ const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
+
+ return (
+
+ {/* sigh language rules – we should use translations ideally but for now we deal with "a" and "an" */}
+ Enter {entityName === 'alert rule' ? 'an' : 'a'} {entityName} name to identify your alert.
+
+ }
+ >
+
+
+
+
+ );
+};
diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
similarity index 76%
rename from public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx
rename to public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
index b0fd59a46b6..ea1bcc5f6a4 100644
--- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
@@ -1,23 +1,13 @@
import { css } from '@emotion/css';
import { omit } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
-import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
+import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react-hook-form';
import { Link, useParams } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { config, logInfo } from '@grafana/runtime';
-import {
- Button,
- ConfirmModal,
- CustomScrollbar,
- Field,
- HorizontalGroup,
- Input,
- Spinner,
- Text,
- useStyles2,
-} from '@grafana/ui';
+import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/core';
@@ -27,73 +17,29 @@ import { useDispatch } from 'app/types';
import { RuleWithLocation } from 'app/types/unified-alerting';
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
-import { LogMessages, trackNewAlerRuleFormError } from '../../Analytics';
-import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
-import { deleteRuleAction, saveRuleFormAction } from '../../state/actions';
-import { RuleFormType, RuleFormValues } from '../../types/rule-form';
-import { initialAsyncRequestState } from '../../utils/redux';
+import { LogMessages, trackNewAlerRuleFormError } from '../../../Analytics';
+import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
+import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions';
+import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
+import { initialAsyncRequestState } from '../../../utils/redux';
import {
getDefaultFormValues,
getDefaultQueries,
MINUTE,
normalizeDefaultAnnotations,
rulerRuleToFormValues,
-} from '../../utils/rule-form';
-import * as ruleId from '../../utils/rule-id';
-import { GrafanaRuleExporter } from '../export/GrafanaRuleExporter';
-
-import AnnotationsStep from './AnnotationsStep';
-import { CloudEvaluationBehavior } from './CloudEvaluationBehavior';
-import { GrafanaEvaluationBehavior } from './GrafanaEvaluationBehavior';
-import { NotificationsStep } from './NotificationsStep';
-import { RecordingRulesNameSpaceAndGroupStep } from './RecordingRulesNameSpaceAndGroupStep';
-import { RuleEditorSection } from './RuleEditorSection';
-import { RuleInspector } from './RuleInspector';
-import { QueryAndExpressionsStep } from './query-and-alert-condition/QueryAndExpressionsStep';
-import { translateRouteParamToRuleType } from './util';
-
-const recordingRuleNameValidationPattern = {
- message:
- 'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
- value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
-};
-
-const AlertRuleNameInput = () => {
- const {
- register,
- watch,
- formState: { errors },
- } = useFormContext();
-
- const ruleFormType = watch('type');
- const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
-
- return (
-
- {/* sigh language rules – we should use translations ideally but for now we deal with "a" and "an" */}
- Enter {entityName === 'alert rule' ? 'an' : 'a'} {entityName} name to identify your alert.
-
- }
- >
-
-
-
-
- );
-};
+} from '../../../utils/rule-form';
+import * as ruleId from '../../../utils/rule-id';
+import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
+import { AlertRuleNameInput } from '../AlertRuleNameInput';
+import AnnotationsStep from '../AnnotationsStep';
+import { CloudEvaluationBehavior } from '../CloudEvaluationBehavior';
+import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
+import { NotificationsStep } from '../NotificationsStep';
+import { RecordingRulesNameSpaceAndGroupStep } from '../RecordingRulesNameSpaceAndGroupStep';
+import { RuleInspector } from '../RuleInspector';
+import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
+import { translateRouteParamToRuleType } from '../util';
type Props = {
existing?: RuleWithLocation;
@@ -374,27 +320,24 @@ function formValuesFromPrefill(rule: Partial): RuleFormValues {
function formValuesFromExistingRule(rule: RuleWithLocation) {
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
}
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- buttonSpinner: css`
- margin-right: ${theme.spacing(1)};
- `,
- form: css`
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- `,
- contentOuter: css`
- background: ${theme.colors.background.primary};
- overflow: hidden;
- flex: 1;
- `,
- flexRow: css`
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- `,
- };
-};
+const getStyles = (theme: GrafanaTheme2) => ({
+ buttonSpinner: css({
+ marginRight: theme.spacing(1),
+ }),
+ form: css({
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ }),
+ contentOuter: css({
+ background: theme.colors.background.primary,
+ overflow: 'hidden',
+ flex: 1,
+ }),
+ flexRow: css({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ }),
+});
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
new file mode 100644
index 00000000000..c2764afeb15
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
@@ -0,0 +1,114 @@
+import React, { useState } from 'react';
+import { FormProvider, useForm } from 'react-hook-form';
+
+import { Stack } from '@grafana/experimental';
+import { Button, CustomScrollbar, LinkButton } from '@grafana/ui';
+
+import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate';
+import { RuleFormValues } from '../../../types/rule-form';
+import { MINUTE } from '../../../utils/rule-form';
+import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
+import { allGrafanaExportProviders, ExportFormats } from '../../export/providers';
+import { AlertRuleNameInput } from '../AlertRuleNameInput';
+import AnnotationsStep from '../AnnotationsStep';
+import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
+import { NotificationsStep } from '../NotificationsStep';
+import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
+
+interface ModifyExportRuleFormProps {
+ alertUid?: string;
+ ruleForm?: RuleFormValues;
+}
+
+type ModifyExportMode = 'rule' | 'group';
+
+export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFormProps) {
+ const formAPI = useForm({
+ mode: 'onSubmit',
+ defaultValues: ruleForm,
+ shouldFocusError: true,
+ });
+
+ const existing = Boolean(ruleForm);
+ const returnTo = `/alerting/list`;
+
+ const [showExporter, setShowExporter] = useState(undefined);
+
+ const [conditionErrorMsg, setConditionErrorMsg] = useState('');
+ console.log('conditionErrorMsg', conditionErrorMsg);
+ const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? MINUTE);
+
+ const checkAlertCondition = (msg = '') => {
+ setConditionErrorMsg(msg);
+ };
+
+ const actionButtons = [
+
+ Cancel
+ ,
+ ,
+ ,
+ ];
+
+ return (
+ <>
+
+
+
+
+ {showExporter && (
+ setShowExporter(undefined)} />
+ )}
+ >
+ );
+}
+
+interface GrafanaRuleDesignExporterProps {
+ onClose: () => void;
+ exportMode: ModifyExportMode;
+}
+
+export const GrafanaRuleDesignExporter = ({ onClose, exportMode }: GrafanaRuleDesignExporterProps) => {
+ const [activeTab, setActiveTab] = useState('yaml');
+ const title = exportMode === 'rule' ? 'Export Rule' : 'Export Group';
+
+ return (
+
+ TODO
+
+ );
+};
diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
index 1867dfc929f..d717509921b 100644
--- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
@@ -6,6 +6,7 @@ import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
+import { config, locationService } from '@grafana/runtime';
import {
Button,
ClipboardButton,
@@ -144,6 +145,20 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
if (isGrafanaRulerRule(rulerRule) && canReadProvisioning) {
moreActions.push();
+
+ if (config.featureToggles.alertingModifiedExport) {
+ moreActions.push(
+
+ locationService.push(
+ `/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`
+ )
+ }
+ />
+ );
+ }
}
moreActions.push(
@@ -162,20 +177,22 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
{buttons.map((button, index) => (
{button}
))}
-
- {moreActions.map((action) => (
- {action}
- ))}
-
- }
- >
-
-
+ {moreActions.length > 0 && (
+
+ {moreActions.map((action) => (
+ {action}
+ ))}
+
+ }
+ >
+
+
+ )}
{!!ruleToDelete && (
{
it('Should not render Delete button for users without the delete permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
- const user = userEvent.setup();
renderRulesTable(grafanaRule);
- await user.click(ui.actionButtons.more.get());
- expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
+ expect(ui.actionButtons.more.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {
@@ -91,11 +89,9 @@ describe('RulesTable RBAC', () => {
it('Should not render Delete button for users without the delete permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
- const user = userEvent.setup();
renderRulesTable(cloudRule);
- await user.click(ui.actionButtons.more.get());
- expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
+ expect(ui.actionButtons.more.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {