From 169c5262a5e7d04234119a3733b00bb787b8bf90 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:07:45 +0200 Subject: [PATCH] Alerting: Add Modify export feature for Grafana-managed alert rules (#75114) * Initial POC for modified rule expor * Add rule and group export options to modified export * Add feature toggle for modifier export * Rename GrafanaRuleDesigner to ModifyExportRuleForm to identify it easily as a rule form * Refactor naming and folder for RuleDesigner => ModifyExport * Don't render more action drop-down button when no more actions are allowed * Redirect cancel button to alert list view * Fix modify export page being reloaded correctly without errors * Fix test * Protect modify-export route when toggle-feature is not enabled * Fix css betterer error * Address pr review coments --------- Co-authored-by: Konrad Lalik --- .betterer.results | 6 - .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + public/app/features/alerting/routes.tsx | 14 ++ .../alerting/unified/CloneRuleEditor.tsx | 2 +- .../alerting/unified/ExistingRuleEditor.tsx | 2 +- .../features/alerting/unified/RuleEditor.tsx | 2 +- .../components/export/GrafanaExportDrawer.tsx | 5 +- .../components/export/GrafanaModifyExport.tsx | 112 ++++++++++++++ .../rule-editor/AlertRuleNameInput.tsx | 51 +++++++ .../{ => alert-rule-form}/AlertRuleForm.tsx | 137 +++++------------- .../alert-rule-form/ModifyExportRuleForm.tsx | 114 +++++++++++++++ .../components/rules/RuleActionsButtons.tsx | 45 ++++-- .../components/rules/RulesTable.test.tsx | 8 +- 17 files changed, 384 insertions(+), 128 deletions(-) create mode 100644 public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx rename public/app/features/alerting/unified/components/rule-editor/{ => alert-rule-form}/AlertRuleForm.tsx (76%) create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx 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 ( + <> + + +
e.preventDefault()}> +
+ + + {/* Step 1 */} + + {/* Step 2 */} + + {/* Step 3-4-5 */} + + + + {/* Step 4 & 5 */} + {/* Annotations only for cloud and Grafana */} + + {/* Notifications step*/} + + + +
+
+
+ {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', () => {