diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index d8b54019fe4..bee885c2e0d 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -6,7 +6,10 @@ import { Annotations, GrafanaAlertStateDecision, Labels, + PostableRuleGrafanaRuleDTO, PromRulesResponse, + RulerAlertingRuleDTO, + RulerRecordingRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO, } from 'app/types/unified-alerting-dto'; @@ -68,6 +71,13 @@ export interface Rule { export type AlertInstances = Record; +export interface ModifyExportPayload { + rules: Array; + name: string; + interval?: string | undefined; + source_tenants?: string[] | undefined; +} + export const alertRuleApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ preview: build.mutation< @@ -220,5 +230,17 @@ export const alertRuleApi = alertingApi.injectEndpoints({ responseType: 'text', }), }), + exportModifiedRuleGroup: build.mutation< + string, + { payload: ModifyExportPayload; format: ExportFormats; nameSpace: string } + >({ + query: ({ payload, format, nameSpace }) => ({ + url: `/api/ruler/grafana/api/v1/rules/${nameSpace}/export/`, + params: { format: format }, + responseType: 'text', + data: payload, + method: 'POST', + }), + }), }), }); diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index 3dffb337a74..02a671b35ea 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -1,4 +1,3 @@ -import { omit } from 'lodash'; import * as React from 'react'; import { useEffect, useState } from 'react'; import { useAsync } from 'react-use'; @@ -8,11 +7,9 @@ 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 { RuleIdentifier } from '../../../../../types/unified-alerting'; import { fetchEditableRuleAction, fetchRulesSourceBuildInfoAction } from '../../state/actions'; -import { RuleFormValues } from '../../types/rule-form'; -import { rulerRuleToFormValues } from '../../utils/rule-form'; +import { formValuesFromExistingRule } from '../../utils/rule-form'; import * as ruleId from '../../utils/rule-id'; import { isGrafanaRulerRule } from '../../utils/rules'; import { createUrl } from '../../utils/url'; @@ -21,18 +18,6 @@ import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExpor 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(); @@ -105,8 +90,18 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) } return ( - - {alertRule && } + + {alertRule && ( + + )} ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx index c7502b41a74..6349303e795 100644 --- a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx @@ -28,7 +28,7 @@ import { checkForPathSeparator } from './util'; export const MAX_GROUP_RESULTS = 1000; -export const useGetGroupOptionsFromFolder = (folderTitle: string) => { +export const useFolderGroupOptions = (folderTitle: string, enableProvisionedGroups: boolean) => { const dispatch = useDispatch(); // fetch the ruler rules from the database so we can figure out what other "groups" are already defined @@ -44,13 +44,18 @@ export const useGetGroupOptionsFromFolder = (folderTitle: string) => { const folderGroups = grafanaFolders.find((f) => f.name === folderTitle)?.groups ?? []; const groupOptions = folderGroups - .map>((group) => ({ - label: group.name, - value: group.name, - description: group.interval ?? MINUTE, - // we include provisioned folders, but disable the option to select them - isDisabled: isProvisionedGroup(group), - })) + .map>((group) => { + const isProvisioned = isProvisionedGroup(group); + return { + label: group.name, + value: group.name, + description: group.interval ?? MINUTE, + // we include provisioned folders, but disable the option to select them + isDisabled: !enableProvisionedGroups ? isProvisioned : false, + isProvisioned: isProvisioned, + }; + }) + .sort(sortByLabel); return { groupOptions, loading: groupfoldersForGrafana?.loading }; @@ -70,7 +75,13 @@ const findGroupMatchingLabel = (group: SelectableValue, query: string) = return group.label?.toLowerCase().includes(query.toLowerCase()); }; -export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGrafana?: RulerRulesConfigDTO | null }) { +export function FolderAndGroup({ + groupfoldersForGrafana, + enableProvisionedGroups, +}: { + groupfoldersForGrafana?: RulerRulesConfigDTO | null; + enableProvisionedGroups: boolean; +}) { const { formState: { errors }, watch, @@ -83,7 +94,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf const folder = watch('folder'); const group = watch('group'); - const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? ''); + const { groupOptions, loading } = useFolderGroupOptions(folder?.title ?? '', enableProvisionedGroups); const [isCreatingFolder, setIsCreatingFolder] = useState(false); const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false); @@ -213,8 +224,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf getOptionLabel={(option: SelectableValue) => (
{option.label} - {/* making the assumption here that it's provisioned when it's disabled, should probably change this */} - {option.isDisabled && ( + {option['isProvisioned'] && ( <> {' '} diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 8039dcb468a..0d640760a14 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -16,7 +16,7 @@ import { parsePrometheusDuration } from '../../utils/time'; import { CollapseToggle } from '../CollapseToggle'; import { EditCloudGroupModal } from '../rules/EditRuleGroupModal'; -import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup'; +import { FolderAndGroup, useFolderGroupOptions } from './FolderAndGroup'; import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker'; import { NeedHelpInfo } from './NeedHelpInfo'; import { RuleEditorSection } from './RuleEditorSection'; @@ -59,7 +59,7 @@ const forValidationOptions = (evaluateEvery: string): RegisterOptions => ({ }); const useIsNewGroup = (folder: string, group: string) => { - const { groupOptions } = useGetGroupOptionsFromFolder(folder); + const { groupOptions } = useFolderGroupOptions(folder, false); const groupIsInGroupOptions = useCallback( (group_: string) => groupOptions.some((groupInList: SelectableValue) => groupInList.label === group_), @@ -71,9 +71,11 @@ const useIsNewGroup = (folder: string, group: string) => { function FolderGroupAndEvaluationInterval({ evaluateEvery, setEvaluateEvery, + enableProvisionedGroups, }: { evaluateEvery: string; setEvaluateEvery: (value: string) => void; + enableProvisionedGroups: boolean; }) { const styles = useStyles2(getStyles); const { watch, setValue, getValues } = useFormContext(); @@ -116,7 +118,10 @@ function FolderGroupAndEvaluationInterval({ return (
- + {folderName && isEditingGroup && ( void; existing: boolean; + enableProvisionedGroups: boolean; }) { const styles = useStyles2(getStyles); const [showErrorHandling, setShowErrorHandling] = useState(false); @@ -222,7 +229,11 @@ export function GrafanaEvaluationBehavior({ // TODO remove "and alert condition" for recording rules - + {existing && ( diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 31d4d91df2f..057b1cd66d8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -1,5 +1,4 @@ import { css } from '@emotion/css'; -import { omit } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react-hook-form'; import { Link, useParams } from 'react-router-dom'; @@ -15,7 +14,6 @@ import { useCleanup } from 'app/core/hooks/useCleanup'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; 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'; @@ -23,11 +21,12 @@ import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { initialAsyncRequestState } from '../../../utils/redux'; import { + formValuesFromExistingRule, getDefaultFormValues, getDefaultQueries, + ignoreHiddenQueries, MINUTE, normalizeDefaultAnnotations, - rulerRuleToFormValues, } from '../../../utils/rule-form'; import * as ruleId from '../../../utils/rule-id'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; @@ -233,6 +232,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { evaluateEvery={evaluateEvery} setEvaluateEvery={setEvaluateEvery} existing={Boolean(existing)} + enableProvisionedGroups={false} /> )} @@ -279,17 +279,6 @@ const isCortexLokiOrRecordingRule = (watch: UseFormWatch) => { return (ruleType === RuleFormType.cloudAlerting || ruleType === RuleFormType.cloudRecording) && dataSourceName !== ''; }; -// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end -// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning. -// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"." -// It seems like we have no choice but to act like "hidden" queries don't exist in alerting. -const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => { - return { - ...ruleDefinition, - queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')), - }; -}; - function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): RuleFormValues { let ruleFromQueryParams: Partial; @@ -319,9 +308,6 @@ function formValuesFromPrefill(rule: Partial): RuleFormValues { }); } -function formValuesFromExistingRule(rule: RuleWithLocation) { - return ignoreHiddenQueries(rulerRuleToFormValues(rule)); -} const getStyles = (theme: GrafanaTheme2) => ({ buttonSpinner: css({ marginRight: theme.spacing(1), 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 index c2764afeb15..c5033010171 100644 --- 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 @@ -1,12 +1,22 @@ -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useAsync } from 'react-use'; import { Stack } from '@grafana/experimental'; -import { Button, CustomScrollbar, LinkButton } from '@grafana/ui'; +import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder } from '@grafana/ui'; +import { useAppNotification } from 'app/core/copy/appNotification'; +import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate'; +import { RulerRuleDTO, RulerRuleGroupDTO } from '../../../../../../types/unified-alerting-dto'; +import { alertRuleApi, ModifyExportPayload } from '../../../api/alertRuleApi'; +import { fetchRulerRulesGroup } from '../../../api/ruler'; +import { useDataSourceFeatures } from '../../../hooks/useCombinedRule'; import { RuleFormValues } from '../../../types/rule-form'; -import { MINUTE } from '../../../utils/rule-form'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; +import { formValuesToRulerGrafanaRuleDTO, MINUTE } from '../../../utils/rule-form'; +import { isGrafanaRulerRule } from '../../../utils/rules'; +import { FileExportPreview } from '../../export/FileExportPreview'; import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer'; import { allGrafanaExportProviders, ExportFormats } from '../../export/providers'; import { AlertRuleNameInput } from '../AlertRuleNameInput'; @@ -16,41 +26,49 @@ import { NotificationsStep } from '../NotificationsStep'; import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep'; interface ModifyExportRuleFormProps { - alertUid?: string; + alertUid: string; ruleForm?: RuleFormValues; } -type ModifyExportMode = 'rule' | 'group'; - export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFormProps) { const formAPI = useForm({ mode: 'onSubmit', defaultValues: ruleForm, shouldFocusError: true, }); + const [queryParams] = useQueryParams(); - const existing = Boolean(ruleForm); - const returnTo = `/alerting/list`; + const existing = Boolean(ruleForm); // always should be true + const notifyApp = useAppNotification(); + const returnTo = !queryParams['returnTo'] ? '/alerting/list' : String(queryParams['returnTo']); - const [showExporter, setShowExporter] = useState(undefined); + const [exportData, setExportData] = useState(undefined); const [conditionErrorMsg, setConditionErrorMsg] = useState(''); - console.log('conditionErrorMsg', conditionErrorMsg); const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? MINUTE); const checkAlertCondition = (msg = '') => { setConditionErrorMsg(msg); }; + const submit = (exportData: RuleFormValues | undefined) => { + if (conditionErrorMsg !== '') { + notifyApp.error(conditionErrorMsg); + return; + } + setExportData(exportData); + }; + + const onClose = useCallback(() => { + setExportData(undefined); + }, [setExportData]); + const actionButtons = [ - + submit(undefined)}> Cancel , - , - , ]; @@ -72,6 +90,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor evaluateEvery={evaluateEvery} setEvaluateEvery={setEvaluateEvery} existing={Boolean(existing)} + enableProvisionedGroups={true} /> {/* Step 4 & 5 */} @@ -83,32 +102,132 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
+ {exportData && } - {showExporter && ( - setShowExporter(undefined)} /> - )} ); } -interface GrafanaRuleDesignExporterProps { +const useGetGroup = (nameSpace: string, group: string) => { + const { dsFeatures } = useDataSourceFeatures(GRAFANA_RULES_SOURCE_NAME); + + const rulerConfig = dsFeatures?.rulerConfig; + + const targetGroup = useAsync(async () => { + return rulerConfig ? await fetchRulerRulesGroup(rulerConfig, nameSpace, group) : undefined; + }, [rulerConfig, nameSpace, group]); + + return targetGroup; +}; + +interface GrafanaRuleDesignExportPreviewProps { + exportFormat: ExportFormats; onClose: () => void; - exportMode: ModifyExportMode; + exportValues: RuleFormValues; + uid: string; } +export const getPayloadToExport = ( + uid: string, + formValues: RuleFormValues, + existingGroup: RulerRuleGroupDTO | null | undefined +): ModifyExportPayload => { + const grafanaRuleDto = formValuesToRulerGrafanaRuleDTO(formValues); + + const updatedRule = { ...grafanaRuleDto, grafana_alert: { ...grafanaRuleDto.grafana_alert, uid: uid } }; + if (existingGroup?.rules) { + // we have to update the rule in the group in the same position if it exists, otherwise we have to add it at the end + let alreadyExistsInGroup = false; + const updatedRules = existingGroup.rules.map((rule: RulerRuleDTO) => { + if (isGrafanaRulerRule(rule) && rule.grafana_alert.uid === uid) { + alreadyExistsInGroup = true; + return updatedRule; + } else { + return rule; + } + }); + if (!alreadyExistsInGroup) { + // we have to add the updated rule at the end of the group + updatedRules.push(updatedRule); + } + return { + ...existingGroup, + rules: updatedRules, + }; + } else { + // we have to create a new group with the updated rule + return { + name: existingGroup?.name ?? '', + rules: [updatedRule], + }; + } +}; -export const GrafanaRuleDesignExporter = ({ onClose, exportMode }: GrafanaRuleDesignExporterProps) => { - const [activeTab, setActiveTab] = useState('yaml'); - const title = exportMode === 'rule' ? 'Export Rule' : 'Export Group'; +const useGetPayloadToExport = (values: RuleFormValues, uid: string) => { + const rulerGroupDto = useGetGroup(values.folder?.title ?? '', values.group); + const payload: ModifyExportPayload = useMemo(() => { + return getPayloadToExport(uid, values, rulerGroupDto?.value); + }, [uid, rulerGroupDto, values]); + return { payload, loadingGroup: rulerGroupDto.loading }; +}; + +const GrafanaRuleDesignExportPreview = ({ + exportFormat, + exportValues, + onClose, + uid, +}: GrafanaRuleDesignExportPreviewProps) => { + const [getExport, exportData] = alertRuleApi.endpoints.exportModifiedRuleGroup.useMutation(); + const { loadingGroup, payload } = useGetPayloadToExport(exportValues, uid); + + const nameSpace = exportValues.folder?.title ?? ''; + + useEffect(() => { + !loadingGroup && getExport({ payload, format: exportFormat, nameSpace: nameSpace }); + }, [nameSpace, exportFormat, payload, getExport, loadingGroup]); + + if (exportData.isLoading) { + return ; + } + + const downloadFileName = `modify-export-${payload.name}-${uid}-${new Date().getTime()}`; return ( - - TODO - + /> ); }; + +interface GrafanaRuleDesignExporterProps { + onClose: () => void; + exportValues: RuleFormValues; + uid: string; +} + +export const GrafanaRuleDesignExporter = React.memo( + ({ onClose, exportValues, uid }: GrafanaRuleDesignExporterProps) => { + const [activeTab, setActiveTab] = useState('yaml'); + + return ( + + + + ); + } +); + +GrafanaRuleDesignExporter.displayName = 'GrafanaRuleDesignExporter'; diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts new file mode 100644 index 00000000000..1dd5e991d94 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts @@ -0,0 +1,125 @@ +import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; + +import { mockRulerGrafanaRule } from '../../../mocks'; +import { RuleFormValues } from '../../../types/rule-form'; +import { Annotation } from '../../../utils/constants'; +import { getDefaultFormValues } from '../../../utils/rule-form'; + +import { getPayloadToExport } from './ModifyExportRuleForm'; + +const rule1 = mockRulerGrafanaRule( + { + for: '1m', + labels: { severity: 'critical', region: 'region1' }, + annotations: { [Annotation.summary]: 'This grafana rule1' }, + }, + { uid: 'uid-rule-1', title: 'Rule1', data: [] } +); + +const rule2 = mockRulerGrafanaRule( + { + for: '1m', + labels: { severity: 'notcritical', region: 'region2' }, + annotations: { [Annotation.summary]: 'This grafana rule2' }, + }, + { uid: 'uid-rule-2', title: 'Rule2', data: [] } +); + +const rule3 = mockRulerGrafanaRule( + { + for: '1m', + labels: { severity: 'notcritical3', region: 'region3' }, + annotations: { [Annotation.summary]: 'This grafana rule2' }, + }, + { uid: 'uid-rule-3', title: 'Rule3', data: [] } +); + +// Prepare the form values for rule2 updated +const defaultValues = getDefaultFormValues(); +const formValuesForRule2Updated: RuleFormValues = { + ...defaultValues, + queries: [ + { + refId: 'A', + relativeTimeRange: { from: 900, to: 1000 }, + datasourceUid: 'dsuid', + model: { + refId: 'A', + hide: true, + }, + queryType: 'query', + }, + ], + condition: 'A', + forTime: 2455, + name: 'Rule2 updated', + labels: [{ key: 'newLabel', value: 'newLabel' }], + annotations: [{ key: 'summary', value: 'This grafana rule2 updated' }], +}; + +const expectedModifiedRule2 = (uid: string) => ({ + annotations: { + summary: 'This grafana rule2 updated', + }, + for: '5m', + grafana_alert: { + condition: 'A', + data: [ + { + datasourceUid: 'dsuid', + model: { + refId: 'A', + hide: true, + }, + queryType: 'query', + refId: 'A', + relativeTimeRange: { + from: 900, + to: 1000, + }, + }, + ], + exec_err_state: 'Error', + is_paused: false, + no_data_state: 'NoData', + title: 'Rule2 updated', + uid: uid, + }, + labels: { + newLabel: 'newLabel', + }, +}); + +describe('getPayloadFromDto', () => { + const groupDto: RulerRuleGroupDTO = { + name: 'Test Group', + rules: [rule1, rule2, rule3], + }; + + it('should return a ModifyExportPayload with the updated rule added to a group with this rule belongs, in the same position', () => { + const result = getPayloadToExport('uid-rule-2', formValuesForRule2Updated, groupDto); + expect(result).toEqual({ + name: 'Test Group', + rules: [rule1, expectedModifiedRule2('uid-rule-2'), rule3], + }); + }); + it('should return a ModifyExportPayload with the updated rule added to a non empty rule where this rule does not belong, in the last position', () => { + const result = getPayloadToExport('uid-rule-5', formValuesForRule2Updated, groupDto); + expect(result).toEqual({ + name: 'Test Group', + rules: [rule1, rule2, rule3, expectedModifiedRule2('uid-rule-5')], + }); + }); + + it('should return a ModifyExportPayload with the updated rule added to an empty group', () => { + const emptyGroupDto: RulerRuleGroupDTO = { + name: 'Empty Group', + rules: [], + }; + const result = getPayloadToExport('uid-rule-2', formValuesForRule2Updated, emptyGroupDto); + expect(result).toEqual({ + name: 'Empty Group', + rules: [expectedModifiedRule2('uid-rule-2')], + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts index f0eefd50da9..3bc456259a6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/util.ts +++ b/public/app/features/alerting/unified/components/rule-editor/util.ts @@ -3,11 +3,11 @@ import { ValidateResult } from 'react-hook-form'; import { DataFrame, - ThresholdsConfig, - ThresholdsMode, isTimeSeriesFrames, - PanelData, LoadingState, + PanelData, + ThresholdsConfig, + ThresholdsMode, } from '@grafana/data'; import { GraphTresholdsStyleMode } from '@grafana/schema'; import { config } from 'app/core/config'; diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index d717509921b..286c58c8d35 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -145,7 +145,6 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => { if (isGrafanaRulerRule(rulerRule) && canReadProvisioning) { moreActions.push(); - if (config.featureToggles.alertingModifiedExport) { moreActions.push( { icon="edit" onClick={() => locationService.push( - `/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export` + createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, { + returnTo: location.pathname + location.search, + }) ) } /> diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 46b058238b9..7b3d1d26612 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,3 +1,5 @@ +import { omit } from 'lodash'; + import { DataQuery, DataSourceInstanceSettings, @@ -544,3 +546,18 @@ function isPromQuery(model: AlertDataQuery): model is PromQuery { export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQuery { return 'expr' in model; } + +// the backend will always execute "hidden" queries, so we have no choice but to remove the property in the front-end +// to avoid confusion. The query editor shows them as "disabled" and that's a different semantic meaning. +// furthermore the "AlertingQueryRunner" calls `filterQuery` on each data source and those will skip running queries that are "hidden"." +// It seems like we have no choice but to act like "hidden" queries don't exist in alerting. +export const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => { + return { + ...ruleDefinition, + queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')), + }; +}; + +export function formValuesFromExistingRule(rule: RuleWithLocation) { + return ignoreHiddenQueries(rulerRuleToFormValues(rule)); +}