mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Use new endpoints in the Modify Export (#75796)
Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
parent
94c15e4926
commit
fbbf9b1a8f
@ -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 {
|
||||
onClose: () => void;
|
||||
exportMode: ModifyExportMode;
|
||||
}
|
||||
const useGetGroup = (nameSpace: string, group: string) => {
|
||||
const { dsFeatures } = useDataSourceFeatures(GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
export const GrafanaRuleDesignExporter = ({ onClose, exportMode }: GrafanaRuleDesignExporterProps) => {
|
||||
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||
const title = exportMode === 'rule' ? 'Export Rule' : 'Export Group';
|
||||
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;
|
||||
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],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
isTimeSeriesFrames,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
ThresholdsConfig,
|
||||
ThresholdsMode,
|
||||
isTimeSeriesFrames,
|
||||
PanelData,
|
||||
LoadingState,
|
||||
} 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…
Reference in New Issue
Block a user