Alerting: Use new endpoints in the Modify Export (#75796)

Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
pull/76357/head^2
Sonia Aguilar 2 years ago committed by GitHub
parent 94c15e4926
commit fbbf9b1a8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      public/app/features/alerting/unified/api/alertRuleApi.ts
  2. 33
      public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx
  3. 34
      public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx
  4. 19
      public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx
  5. 20
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  6. 183
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
  7. 125
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/getPayloadToExport.test.ts
  8. 6
      public/app/features/alerting/unified/components/rule-editor/util.ts
  9. 5
      public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx
  10. 17
      public/app/features/alerting/unified/utils/rule-form.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<string, string>;
export interface ModifyExportPayload {
rules: Array<RulerAlertingRuleDTO | RulerRecordingRuleDTO | PostableRuleGrafanaRuleDTO>;
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',
}),
}),
}),
});

@ -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<RulerRuleDTO>) {
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
}
export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
const dispatch = useDispatch();
@ -105,8 +90,18 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps)
}
return (
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
{alertRule && <ModifyExportRuleForm ruleForm={alertRule ? formValuesFromExistingRule(alertRule) : undefined} />}
<AlertingPageWrapper
isLoading={loading}
pageId="alert-list"
pageNav={{
text: 'Modify export',
subTitle:
'Modify the current alert rule and export the rule definition in the format of your choice. Any changes you make will not be saved.',
}}
>
{alertRule && (
<ModifyExportRuleForm ruleForm={formValuesFromExistingRule(alertRule)} alertUid={match.params.id ?? ''} />
)}
</AlertingPageWrapper>
);
}

@ -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<SelectableValue<string>>((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<SelectableValue<string>>((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<string>, 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<string>) => (
<div>
<span>{option.label}</span>
{/* making the assumption here that it's provisioned when it's disabled, should probably change this */}
{option.isDisabled && (
{option['isProvisioned'] && (
<>
{' '}
<ProvisioningBadge />

@ -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<string>) => 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<RuleFormValues>();
@ -116,7 +118,10 @@ function FolderGroupAndEvaluationInterval({
return (
<div>
<FolderAndGroup groupfoldersForGrafana={groupfoldersForGrafana?.result} />
<FolderAndGroup
groupfoldersForGrafana={groupfoldersForGrafana?.result}
enableProvisionedGroups={enableProvisionedGroups}
/>
{folderName && isEditingGroup && (
<EditCloudGroupModal
namespace={existingNamespace ?? emptyNamespace}
@ -206,10 +211,12 @@ export function GrafanaEvaluationBehavior({
evaluateEvery,
setEvaluateEvery,
existing,
enableProvisionedGroups,
}: {
evaluateEvery: string;
setEvaluateEvery: (value: string) => 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
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription()}>
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
<FolderGroupAndEvaluationInterval setEvaluateEvery={setEvaluateEvery} evaluateEvery={evaluateEvery} />
<FolderGroupAndEvaluationInterval
setEvaluateEvery={setEvaluateEvery}
evaluateEvery={evaluateEvery}
enableProvisionedGroups={enableProvisionedGroups}
/>
<ForInput evaluateEvery={evaluateEvery} />
{existing && (

@ -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<RuleFormValues>) => {
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<RuleFormValues>;
@ -319,9 +308,6 @@ function formValuesFromPrefill(rule: Partial<RuleFormValues>): RuleFormValues {
});
}
function formValuesFromExistingRule(rule: RuleWithLocation<RulerRuleDTO>) {
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
}
const getStyles = (theme: GrafanaTheme2) => ({
buttonSpinner: css({
marginRight: theme.spacing(1),

@ -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<RuleFormValues>({
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<ModifyExportMode | undefined>(undefined);
const [exportData, setExportData] = useState<RuleFormValues | undefined>(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 = [
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary">
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
Cancel
</LinkButton>,
<Button key="export-rule" size="sm" onClick={() => setShowExporter('rule')}>
Export Rule
</Button>,
<Button key="export-group" size="sm" onClick={() => setShowExporter('group')}>
Export Group
<Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues))}>
Export
</Button>,
];
@ -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
</CustomScrollbar>
</div>
</form>
{exportData && <GrafanaRuleDesignExporter exportValues={exportData} onClose={onClose} uid={alertUid} />}
</FormProvider>
{showExporter && (
<GrafanaRuleDesignExporter exportMode={showExporter} onClose={() => 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<RulerRuleDTO> | 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<ExportFormats>('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 <LoadingPlaceholder text="Loading...." />;
}
const downloadFileName = `modify-export-${payload.name}-${uid}-${new Date().getTime()}`;
return (
<GrafanaExportDrawer
title={title}
activeTab={activeTab}
onTabChange={setActiveTab}
<FileExportPreview
format={exportFormat}
textDefinition={exportData.data ?? ''}
downloadFileName={downloadFileName}
onClose={onClose}
formatProviders={Object.values(allGrafanaExportProviders)}
>
TODO
</GrafanaExportDrawer>
/>
);
};
interface GrafanaRuleDesignExporterProps {
onClose: () => void;
exportValues: RuleFormValues;
uid: string;
}
export const GrafanaRuleDesignExporter = React.memo(
({ onClose, exportValues, uid }: GrafanaRuleDesignExporterProps) => {
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
return (
<GrafanaExportDrawer
title={'Export Group'}
activeTab={activeTab}
onTabChange={setActiveTab}
onClose={onClose}
formatProviders={Object.values(allGrafanaExportProviders)}
>
<GrafanaRuleDesignExportPreview
exportFormat={activeTab}
onClose={onClose}
exportValues={exportValues}
uid={uid}
/>
</GrafanaExportDrawer>
);
}
);
GrafanaRuleDesignExporter.displayName = 'GrafanaRuleDesignExporter';

@ -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<RulerRuleDTO> = {
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<RulerRuleDTO> = {
name: 'Empty Group',
rules: [],
};
const result = getPayloadToExport('uid-rule-2', formValuesForRule2Updated, emptyGroupDto);
expect(result).toEqual({
name: 'Empty Group',
rules: [expectedModifiedRule2('uid-rule-2')],
});
});
});

@ -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';

@ -145,7 +145,6 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
if (isGrafanaRulerRule(rulerRule) && canReadProvisioning) {
moreActions.push(<Menu.Item label="Export" icon="download-alt" onClick={toggleShowExportDrawer} />);
if (config.featureToggles.alertingModifiedExport) {
moreActions.push(
<Menu.Item
@ -153,7 +152,9 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
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,
})
)
}
/>

@ -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<RulerRuleDTO>) {
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
}

Loading…
Cancel
Save