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,
|
Annotations,
|
||||||
GrafanaAlertStateDecision,
|
GrafanaAlertStateDecision,
|
||||||
Labels,
|
Labels,
|
||||||
|
PostableRuleGrafanaRuleDTO,
|
||||||
PromRulesResponse,
|
PromRulesResponse,
|
||||||
|
RulerAlertingRuleDTO,
|
||||||
|
RulerRecordingRuleDTO,
|
||||||
RulerRuleGroupDTO,
|
RulerRuleGroupDTO,
|
||||||
RulerRulesConfigDTO,
|
RulerRulesConfigDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
@ -68,6 +71,13 @@ export interface Rule {
|
|||||||
|
|
||||||
export type AlertInstances = Record<string, string>;
|
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({
|
export const alertRuleApi = alertingApi.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
preview: build.mutation<
|
preview: build.mutation<
|
||||||
@ -220,5 +230,17 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
|||||||
responseType: 'text',
|
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 * as React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
@ -8,11 +7,9 @@ import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
|||||||
|
|
||||||
import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
|
import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
|
||||||
import { useDispatch } from '../../../../../types';
|
import { useDispatch } from '../../../../../types';
|
||||||
import { RuleIdentifier, RuleWithLocation } from '../../../../../types/unified-alerting';
|
import { RuleIdentifier } from '../../../../../types/unified-alerting';
|
||||||
import { RulerRuleDTO } from '../../../../../types/unified-alerting-dto';
|
|
||||||
import { fetchEditableRuleAction, fetchRulesSourceBuildInfoAction } from '../../state/actions';
|
import { fetchEditableRuleAction, fetchRulesSourceBuildInfoAction } from '../../state/actions';
|
||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { formValuesFromExistingRule } from '../../utils/rule-form';
|
||||||
import { rulerRuleToFormValues } from '../../utils/rule-form';
|
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { createUrl } from '../../utils/url';
|
import { createUrl } from '../../utils/url';
|
||||||
@ -21,18 +18,6 @@ import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExpor
|
|||||||
|
|
||||||
interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {}
|
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) {
|
export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@ -105,8 +90,18 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
|
<AlertingPageWrapper
|
||||||
{alertRule && <ModifyExportRuleForm ruleForm={alertRule ? formValuesFromExistingRule(alertRule) : undefined} />}
|
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>
|
</AlertingPageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ import { checkForPathSeparator } from './util';
|
|||||||
|
|
||||||
export const MAX_GROUP_RESULTS = 1000;
|
export const MAX_GROUP_RESULTS = 1000;
|
||||||
|
|
||||||
export const useGetGroupOptionsFromFolder = (folderTitle: string) => {
|
export const useFolderGroupOptions = (folderTitle: string, enableProvisionedGroups: boolean) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
|
// 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 folderGroups = grafanaFolders.find((f) => f.name === folderTitle)?.groups ?? [];
|
||||||
|
|
||||||
const groupOptions = folderGroups
|
const groupOptions = folderGroups
|
||||||
.map<SelectableValue<string>>((group) => ({
|
.map<SelectableValue<string>>((group) => {
|
||||||
label: group.name,
|
const isProvisioned = isProvisionedGroup(group);
|
||||||
value: group.name,
|
return {
|
||||||
description: group.interval ?? MINUTE,
|
label: group.name,
|
||||||
// we include provisioned folders, but disable the option to select them
|
value: group.name,
|
||||||
isDisabled: isProvisionedGroup(group),
|
description: group.interval ?? MINUTE,
|
||||||
}))
|
// we include provisioned folders, but disable the option to select them
|
||||||
|
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
||||||
|
isProvisioned: isProvisioned,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
.sort(sortByLabel);
|
.sort(sortByLabel);
|
||||||
|
|
||||||
return { groupOptions, loading: groupfoldersForGrafana?.loading };
|
return { groupOptions, loading: groupfoldersForGrafana?.loading };
|
||||||
@ -70,7 +75,13 @@ const findGroupMatchingLabel = (group: SelectableValue<string>, query: string) =
|
|||||||
return group.label?.toLowerCase().includes(query.toLowerCase());
|
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 {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
watch,
|
watch,
|
||||||
@ -83,7 +94,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
|||||||
const folder = watch('folder');
|
const folder = watch('folder');
|
||||||
const group = watch('group');
|
const group = watch('group');
|
||||||
|
|
||||||
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
const { groupOptions, loading } = useFolderGroupOptions(folder?.title ?? '', enableProvisionedGroups);
|
||||||
|
|
||||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||||
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
|
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
|
||||||
@ -213,8 +224,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
|||||||
getOptionLabel={(option: SelectableValue<string>) => (
|
getOptionLabel={(option: SelectableValue<string>) => (
|
||||||
<div>
|
<div>
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
{/* making the assumption here that it's provisioned when it's disabled, should probably change this */}
|
{option['isProvisioned'] && (
|
||||||
{option.isDisabled && (
|
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<ProvisioningBadge />
|
<ProvisioningBadge />
|
||||||
|
@ -16,7 +16,7 @@ import { parsePrometheusDuration } from '../../utils/time';
|
|||||||
import { CollapseToggle } from '../CollapseToggle';
|
import { CollapseToggle } from '../CollapseToggle';
|
||||||
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
|
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
|
||||||
|
|
||||||
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
import { FolderAndGroup, useFolderGroupOptions } from './FolderAndGroup';
|
||||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||||
import { NeedHelpInfo } from './NeedHelpInfo';
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
@ -59,7 +59,7 @@ const forValidationOptions = (evaluateEvery: string): RegisterOptions => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const useIsNewGroup = (folder: string, group: string) => {
|
const useIsNewGroup = (folder: string, group: string) => {
|
||||||
const { groupOptions } = useGetGroupOptionsFromFolder(folder);
|
const { groupOptions } = useFolderGroupOptions(folder, false);
|
||||||
|
|
||||||
const groupIsInGroupOptions = useCallback(
|
const groupIsInGroupOptions = useCallback(
|
||||||
(group_: string) => groupOptions.some((groupInList: SelectableValue<string>) => groupInList.label === group_),
|
(group_: string) => groupOptions.some((groupInList: SelectableValue<string>) => groupInList.label === group_),
|
||||||
@ -71,9 +71,11 @@ const useIsNewGroup = (folder: string, group: string) => {
|
|||||||
function FolderGroupAndEvaluationInterval({
|
function FolderGroupAndEvaluationInterval({
|
||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
setEvaluateEvery,
|
setEvaluateEvery,
|
||||||
|
enableProvisionedGroups,
|
||||||
}: {
|
}: {
|
||||||
evaluateEvery: string;
|
evaluateEvery: string;
|
||||||
setEvaluateEvery: (value: string) => void;
|
setEvaluateEvery: (value: string) => void;
|
||||||
|
enableProvisionedGroups: boolean;
|
||||||
}) {
|
}) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { watch, setValue, getValues } = useFormContext<RuleFormValues>();
|
const { watch, setValue, getValues } = useFormContext<RuleFormValues>();
|
||||||
@ -116,7 +118,10 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FolderAndGroup groupfoldersForGrafana={groupfoldersForGrafana?.result} />
|
<FolderAndGroup
|
||||||
|
groupfoldersForGrafana={groupfoldersForGrafana?.result}
|
||||||
|
enableProvisionedGroups={enableProvisionedGroups}
|
||||||
|
/>
|
||||||
{folderName && isEditingGroup && (
|
{folderName && isEditingGroup && (
|
||||||
<EditCloudGroupModal
|
<EditCloudGroupModal
|
||||||
namespace={existingNamespace ?? emptyNamespace}
|
namespace={existingNamespace ?? emptyNamespace}
|
||||||
@ -206,10 +211,12 @@ export function GrafanaEvaluationBehavior({
|
|||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
setEvaluateEvery,
|
setEvaluateEvery,
|
||||||
existing,
|
existing,
|
||||||
|
enableProvisionedGroups,
|
||||||
}: {
|
}: {
|
||||||
evaluateEvery: string;
|
evaluateEvery: string;
|
||||||
setEvaluateEvery: (value: string) => void;
|
setEvaluateEvery: (value: string) => void;
|
||||||
existing: boolean;
|
existing: boolean;
|
||||||
|
enableProvisionedGroups: boolean;
|
||||||
}) {
|
}) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
||||||
@ -222,7 +229,11 @@ export function GrafanaEvaluationBehavior({
|
|||||||
// TODO remove "and alert condition" for recording rules
|
// TODO remove "and alert condition" for recording rules
|
||||||
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription()}>
|
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription()}>
|
||||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
<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} />
|
<ForInput evaluateEvery={evaluateEvery} />
|
||||||
|
|
||||||
{existing && (
|
{existing && (
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { omit } from 'lodash';
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react-hook-form';
|
import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react-hook-form';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
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 { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
|
||||||
|
|
||||||
import { LogMessages, trackNewAlerRuleFormError } from '../../../Analytics';
|
import { LogMessages, trackNewAlerRuleFormError } from '../../../Analytics';
|
||||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||||
@ -23,11 +21,12 @@ import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions';
|
|||||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||||
import { initialAsyncRequestState } from '../../../utils/redux';
|
import { initialAsyncRequestState } from '../../../utils/redux';
|
||||||
import {
|
import {
|
||||||
|
formValuesFromExistingRule,
|
||||||
getDefaultFormValues,
|
getDefaultFormValues,
|
||||||
getDefaultQueries,
|
getDefaultQueries,
|
||||||
|
ignoreHiddenQueries,
|
||||||
MINUTE,
|
MINUTE,
|
||||||
normalizeDefaultAnnotations,
|
normalizeDefaultAnnotations,
|
||||||
rulerRuleToFormValues,
|
|
||||||
} from '../../../utils/rule-form';
|
} from '../../../utils/rule-form';
|
||||||
import * as ruleId from '../../../utils/rule-id';
|
import * as ruleId from '../../../utils/rule-id';
|
||||||
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
|
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
|
||||||
@ -233,6 +232,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
evaluateEvery={evaluateEvery}
|
evaluateEvery={evaluateEvery}
|
||||||
setEvaluateEvery={setEvaluateEvery}
|
setEvaluateEvery={setEvaluateEvery}
|
||||||
existing={Boolean(existing)}
|
existing={Boolean(existing)}
|
||||||
|
enableProvisionedGroups={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -279,17 +279,6 @@ const isCortexLokiOrRecordingRule = (watch: UseFormWatch<RuleFormValues>) => {
|
|||||||
return (ruleType === RuleFormType.cloudAlerting || ruleType === RuleFormType.cloudRecording) && dataSourceName !== '';
|
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 {
|
function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType): RuleFormValues {
|
||||||
let ruleFromQueryParams: Partial<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) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
buttonSpinner: css({
|
buttonSpinner: css({
|
||||||
marginRight: theme.spacing(1),
|
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 { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import { Stack } from '@grafana/experimental';
|
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 { 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 { 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 { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
|
||||||
import { allGrafanaExportProviders, ExportFormats } from '../../export/providers';
|
import { allGrafanaExportProviders, ExportFormats } from '../../export/providers';
|
||||||
import { AlertRuleNameInput } from '../AlertRuleNameInput';
|
import { AlertRuleNameInput } from '../AlertRuleNameInput';
|
||||||
@ -16,41 +26,49 @@ import { NotificationsStep } from '../NotificationsStep';
|
|||||||
import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
|
import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
|
||||||
|
|
||||||
interface ModifyExportRuleFormProps {
|
interface ModifyExportRuleFormProps {
|
||||||
alertUid?: string;
|
alertUid: string;
|
||||||
ruleForm?: RuleFormValues;
|
ruleForm?: RuleFormValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyExportMode = 'rule' | 'group';
|
|
||||||
|
|
||||||
export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFormProps) {
|
export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFormProps) {
|
||||||
const formAPI = useForm<RuleFormValues>({
|
const formAPI = useForm<RuleFormValues>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
defaultValues: ruleForm,
|
defaultValues: ruleForm,
|
||||||
shouldFocusError: true,
|
shouldFocusError: true,
|
||||||
});
|
});
|
||||||
|
const [queryParams] = useQueryParams();
|
||||||
|
|
||||||
const existing = Boolean(ruleForm);
|
const existing = Boolean(ruleForm); // always should be true
|
||||||
const returnTo = `/alerting/list`;
|
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('');
|
const [conditionErrorMsg, setConditionErrorMsg] = useState('');
|
||||||
console.log('conditionErrorMsg', conditionErrorMsg);
|
|
||||||
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? MINUTE);
|
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? MINUTE);
|
||||||
|
|
||||||
const checkAlertCondition = (msg = '') => {
|
const checkAlertCondition = (msg = '') => {
|
||||||
setConditionErrorMsg(msg);
|
setConditionErrorMsg(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submit = (exportData: RuleFormValues | undefined) => {
|
||||||
|
if (conditionErrorMsg !== '') {
|
||||||
|
notifyApp.error(conditionErrorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExportData(exportData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setExportData(undefined);
|
||||||
|
}, [setExportData]);
|
||||||
|
|
||||||
const actionButtons = [
|
const actionButtons = [
|
||||||
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary">
|
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
|
||||||
Cancel
|
Cancel
|
||||||
</LinkButton>,
|
</LinkButton>,
|
||||||
<Button key="export-rule" size="sm" onClick={() => setShowExporter('rule')}>
|
<Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues))}>
|
||||||
Export Rule
|
Export
|
||||||
</Button>,
|
|
||||||
<Button key="export-group" size="sm" onClick={() => setShowExporter('group')}>
|
|
||||||
Export Group
|
|
||||||
</Button>,
|
</Button>,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -72,6 +90,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
|
|||||||
evaluateEvery={evaluateEvery}
|
evaluateEvery={evaluateEvery}
|
||||||
setEvaluateEvery={setEvaluateEvery}
|
setEvaluateEvery={setEvaluateEvery}
|
||||||
existing={Boolean(existing)}
|
existing={Boolean(existing)}
|
||||||
|
enableProvisionedGroups={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Step 4 & 5 */}
|
{/* Step 4 & 5 */}
|
||||||
@ -83,32 +102,132 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
|
|||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{exportData && <GrafanaRuleDesignExporter exportValues={exportData} onClose={onClose} uid={alertUid} />}
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
{showExporter && (
|
|
||||||
<GrafanaRuleDesignExporter exportMode={showExporter} onClose={() => setShowExporter(undefined)} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GrafanaRuleDesignExporterProps {
|
const useGetGroup = (nameSpace: string, group: string) => {
|
||||||
onClose: () => void;
|
const { dsFeatures } = useDataSourceFeatures(GRAFANA_RULES_SOURCE_NAME);
|
||||||
exportMode: ModifyExportMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GrafanaRuleDesignExporter = ({ onClose, exportMode }: GrafanaRuleDesignExporterProps) => {
|
const rulerConfig = dsFeatures?.rulerConfig;
|
||||||
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
|
||||||
const title = exportMode === 'rule' ? 'Export Rule' : 'Export Group';
|
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 (
|
return (
|
||||||
<GrafanaExportDrawer
|
<FileExportPreview
|
||||||
title={title}
|
format={exportFormat}
|
||||||
activeTab={activeTab}
|
textDefinition={exportData.data ?? ''}
|
||||||
onTabChange={setActiveTab}
|
downloadFileName={downloadFileName}
|
||||||
onClose={onClose}
|
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 {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
|
isTimeSeriesFrames,
|
||||||
|
LoadingState,
|
||||||
|
PanelData,
|
||||||
ThresholdsConfig,
|
ThresholdsConfig,
|
||||||
ThresholdsMode,
|
ThresholdsMode,
|
||||||
isTimeSeriesFrames,
|
|
||||||
PanelData,
|
|
||||||
LoadingState,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { GraphTresholdsStyleMode } from '@grafana/schema';
|
import { GraphTresholdsStyleMode } from '@grafana/schema';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
@ -145,7 +145,6 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
|
|||||||
|
|
||||||
if (isGrafanaRulerRule(rulerRule) && canReadProvisioning) {
|
if (isGrafanaRulerRule(rulerRule) && canReadProvisioning) {
|
||||||
moreActions.push(<Menu.Item label="Export" icon="download-alt" onClick={toggleShowExportDrawer} />);
|
moreActions.push(<Menu.Item label="Export" icon="download-alt" onClick={toggleShowExportDrawer} />);
|
||||||
|
|
||||||
if (config.featureToggles.alertingModifiedExport) {
|
if (config.featureToggles.alertingModifiedExport) {
|
||||||
moreActions.push(
|
moreActions.push(
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@ -153,7 +152,9 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
|
|||||||
icon="edit"
|
icon="edit"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
locationService.push(
|
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 {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
@ -544,3 +546,18 @@ function isPromQuery(model: AlertDataQuery): model is PromQuery {
|
|||||||
export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQuery {
|
export function isPromOrLokiQuery(model: AlertDataQuery): model is PromOrLokiQuery {
|
||||||
return 'expr' in model;
|
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