mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Implement UI for grafana-managed recording rules (#90360)
* Implement UI for grafana-managed recording rules * use undefined for the duration instead of null , for recording rules * Fix tests * add tests * Add pause functionality for grafana recording rules * update translations * remove obsolete snapshot * use createUrl instead of renderUrl * refactor * Add validation for grafana recording rule name * create util functions for rule types and add record field in mock function * add util isDatatSourceManagedRuleByType * refactor * Add metric field in alert rule form * fix alert name component * update width for alert name and metric * fix test * add validation back to cloud recording rules name * Alerting: Recording rules PR review (#90654) Update type helper methods * add slash in createUrl * fix baseurl in the returnTo * nits * Add metric on expanded row in the alert list view * nits Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com> * update snapshot --------- Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
parent
c0af387766
commit
8423d06988
@ -2280,9 +2280,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/rules/GrafanaRules.tsx:5381": [
|
"public/app/features/alerting/unified/components/rules/GrafanaRules.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/rules/RuleConfigStatus.tsx:5381": [
|
"public/app/features/alerting/unified/components/rules/RuleConfigStatus.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
@ -2328,10 +2326,7 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/rules/RuleState.tsx:5381": [
|
"public/app/features/alerting/unified/components/rules/RuleState.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [
|
"public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||||
|
@ -25,6 +25,7 @@ export const LogMessages = {
|
|||||||
cancelSavingAlertRule: 'user canceled alert rule creation',
|
cancelSavingAlertRule: 'user canceled alert rule creation',
|
||||||
successSavingAlertRule: 'alert rule saved successfully',
|
successSavingAlertRule: 'alert rule saved successfully',
|
||||||
unknownMessageFromError: 'unknown messageFromError',
|
unknownMessageFromError: 'unknown messageFromError',
|
||||||
|
grafanaRecording: 'creating Grafana recording rule from scratch',
|
||||||
loadedCentralAlertStateHistory: 'loaded central alert state history',
|
loadedCentralAlertStateHistory: 'loaded central alert state history',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,7 +17,10 @@ import { fetchRulesSourceBuildInfoAction } from './state/actions';
|
|||||||
import { useRulesAccess } from './utils/accessControlHooks';
|
import { useRulesAccess } from './utils/accessControlHooks';
|
||||||
import * as ruleId from './utils/rule-id';
|
import * as ruleId from './utils/rule-id';
|
||||||
|
|
||||||
type RuleEditorProps = GrafanaRouteComponentProps<{ id?: string; type?: 'recording' | 'alerting' }>;
|
type RuleEditorProps = GrafanaRouteComponentProps<{
|
||||||
|
id?: string;
|
||||||
|
type?: 'recording' | 'alerting' | 'grafana-recording';
|
||||||
|
}>;
|
||||||
|
|
||||||
const defaultPageNav: Partial<NavModelItem> = {
|
const defaultPageNav: Partial<NavModelItem> = {
|
||||||
icon: 'bell',
|
icon: 'bell',
|
||||||
@ -25,8 +28,8 @@ const defaultPageNav: Partial<NavModelItem> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// sadly we only get the "type" when a new rule is being created, when editing an existing recording rule we can't actually know it from the URL
|
// sadly we only get the "type" when a new rule is being created, when editing an existing recording rule we can't actually know it from the URL
|
||||||
const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting') => {
|
const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting' | 'grafana-recording') => {
|
||||||
if (type === 'recording') {
|
if (type === 'recording' || type === 'grafana-recording') {
|
||||||
if (identifier) {
|
if (identifier) {
|
||||||
// this branch should never trigger actually, the type param isn't used when editing rules
|
// this branch should never trigger actually, the type param isn't used when editing rules
|
||||||
return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit recording rule' };
|
return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit recording rule' };
|
||||||
|
@ -112,6 +112,6 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
|
|||||||
"refId": "C",
|
"refId": "C",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"type": "grafana",
|
"type": "grafana-alerting",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Field, Input, Text } from '@grafana/ui';
|
import { Field, Input, Stack, Text } from '@grafana/ui';
|
||||||
|
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
|
import { isCloudRecordingRuleByType, isGrafanaRecordingRuleByType, isRecordingRuleByType } from '../../utils/rules';
|
||||||
|
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
|
|
||||||
const recordingRuleNameValidationPattern = {
|
const recordingRuleNameValidationPattern = (type: RuleFormType) => ({
|
||||||
message:
|
message: isGrafanaRecordingRuleByType(type)
|
||||||
'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
|
? 'Recording rule metric must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.'
|
||||||
|
: 'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
|
||||||
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
|
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
|
||||||
};
|
});
|
||||||
|
|
||||||
export const AlertRuleNameInput = () => {
|
/**
|
||||||
|
* This component renders the input for the alert rule name.
|
||||||
|
* In case of recording rule, it also renders the input for the recording rule metric, and it validates this value.
|
||||||
|
*/
|
||||||
|
export const AlertRuleNameAndMetric = () => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
watch,
|
watch,
|
||||||
@ -21,8 +27,14 @@ export const AlertRuleNameInput = () => {
|
|||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const ruleFormType = watch('type');
|
const ruleFormType = watch('type');
|
||||||
const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
|
if (!ruleFormType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isRecording = isRecordingRuleByType(ruleFormType);
|
||||||
|
const isGrafanaRecordingRule = isGrafanaRecordingRuleByType(ruleFormType);
|
||||||
|
const isCloudRecordingRule = isCloudRecordingRuleByType(ruleFormType);
|
||||||
|
const recordingLabel = isGrafanaRecordingRule ? 'recording rule and metric' : 'recording rule';
|
||||||
|
const entityName = isRecording ? recordingLabel : 'alert rule';
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection
|
<RuleEditorSection
|
||||||
stepNo={1}
|
stepNo={1}
|
||||||
@ -33,19 +45,37 @@ export const AlertRuleNameInput = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Stack direction="column">
|
||||||
<Field label="Name" error={errors?.name?.message} invalid={!!errors.name?.message}>
|
<Field label="Name" error={errors?.name?.message} invalid={!!errors.name?.message}>
|
||||||
<Input
|
<Input
|
||||||
data-testid={selectors.components.AlertRules.ruleNameField}
|
data-testid={selectors.components.AlertRules.ruleNameField}
|
||||||
id="name"
|
id="name"
|
||||||
width={35}
|
width={38}
|
||||||
{...register('name', {
|
{...register('name', {
|
||||||
required: { value: true, message: 'Must enter a name' },
|
required: { value: true, message: 'Must enter a name' },
|
||||||
pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined,
|
pattern: isCloudRecordingRule
|
||||||
|
? recordingRuleNameValidationPattern(RuleFormType.cloudRecording)
|
||||||
|
: undefined,
|
||||||
})}
|
})}
|
||||||
aria-label="name"
|
aria-label="name"
|
||||||
placeholder={`Give your ${entityName} a name`}
|
placeholder={`Give your ${entityName} a name`}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
{isGrafanaRecordingRule && (
|
||||||
|
<Field label="Metric" error={errors?.metric?.message} invalid={!!errors.metric?.message}>
|
||||||
|
<Input
|
||||||
|
id="metric"
|
||||||
|
width={38}
|
||||||
|
{...register('metric', {
|
||||||
|
required: { value: true, message: 'Must enter a metric name' },
|
||||||
|
pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording),
|
||||||
|
})}
|
||||||
|
aria-label="metric"
|
||||||
|
placeholder={`Give your metric a name`}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</RuleEditorSection>
|
</RuleEditorSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import { Controller, RegisterOptions, useFormContext } from 'react-hook-form';
|
|||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Field, Icon, IconButton, Input, Label, Stack, Switch, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Field, Icon, IconButton, Input, Label, Stack, Switch, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { Trans, t } from 'app/core/internationalization';
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
|
import { isGrafanaAlertingRuleByType } from 'app/features/alerting/unified/utils/rules';
|
||||||
|
|
||||||
import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
||||||
import { LogMessages, logInfo } from '../../Analytics';
|
import { LogMessages, logInfo } from '../../Analytics';
|
||||||
@ -290,6 +291,9 @@ export function GrafanaEvaluationBehavior({
|
|||||||
const { watch, setValue } = useFormContext<RuleFormValues>();
|
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const isPaused = watch('isPaused');
|
const isPaused = watch('isPaused');
|
||||||
|
const type = watch('type');
|
||||||
|
|
||||||
|
const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// TODO remove "and alert condition" for recording rules
|
// TODO remove "and alert condition" for recording rules
|
||||||
@ -300,7 +304,8 @@ export function GrafanaEvaluationBehavior({
|
|||||||
evaluateEvery={evaluateEvery}
|
evaluateEvery={evaluateEvery}
|
||||||
enableProvisionedGroups={enableProvisionedGroups}
|
enableProvisionedGroups={enableProvisionedGroups}
|
||||||
/>
|
/>
|
||||||
<ForInput evaluateEvery={evaluateEvery} />
|
{/* Show the pending period input only for Grafana alerting rules */}
|
||||||
|
{isGrafanaAlertingRule && <ForInput evaluateEvery={evaluateEvery} />}
|
||||||
|
|
||||||
{existing && (
|
{existing && (
|
||||||
<Field htmlFor="pause-alert-switch">
|
<Field htmlFor="pause-alert-switch">
|
||||||
@ -327,6 +332,8 @@ export function GrafanaEvaluationBehavior({
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{isGrafanaAlertingRule && (
|
||||||
|
<>
|
||||||
<CollapseToggle
|
<CollapseToggle
|
||||||
isCollapsed={!showErrorHandling}
|
isCollapsed={!showErrorHandling}
|
||||||
onToggle={(collapsed) => setShowErrorHandling(!collapsed)}
|
onToggle={(collapsed) => setShowErrorHandling(!collapsed)}
|
||||||
@ -367,6 +374,8 @@ export function GrafanaEvaluationBehavior({
|
|||||||
</Field>
|
</Field>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</RuleEditorSection>
|
</RuleEditorSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
|||||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
|
import { isRecordingRuleByType } from '../../utils/rules';
|
||||||
|
|
||||||
import { NeedHelpInfo } from './NeedHelpInfo';
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
@ -62,11 +63,14 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
|||||||
}
|
}
|
||||||
setShowLabelsEditor(false);
|
setShowLabelsEditor(false);
|
||||||
}
|
}
|
||||||
|
if (!type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection
|
<RuleEditorSection
|
||||||
stepNo={4}
|
stepNo={4}
|
||||||
title={type === RuleFormType.cloudRecording ? 'Add labels' : 'Configure labels and notifications'}
|
title={isRecordingRuleByType(type) ? 'Add labels' : 'Configure labels and notifications'}
|
||||||
description={
|
description={
|
||||||
<Stack direction="row" gap={0.5} alignItems="center">
|
<Stack direction="row" gap={0.5} alignItems="center">
|
||||||
{type === RuleFormType.cloudRecording ? (
|
{type === RuleFormType.cloudRecording ? (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useMountedState } from 'react-use';
|
import { useMountedState } from 'react-use';
|
||||||
import { takeWhile } from 'rxjs/operators';
|
import { takeWhile } from 'rxjs/operators';
|
||||||
@ -13,6 +13,7 @@ import { previewAlertRule } from '../../api/preview';
|
|||||||
import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus';
|
import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus';
|
||||||
import { PreviewRuleRequest, PreviewRuleResponse } from '../../types/preview';
|
import { PreviewRuleRequest, PreviewRuleResponse } from '../../types/preview';
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
|
import { isDataSourceManagedRuleByType } from '../../utils/rules';
|
||||||
|
|
||||||
import { PreviewRuleResult } from './PreviewRuleResult';
|
import { PreviewRuleResult } from './PreviewRuleResult';
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ export function PreviewRule(): React.ReactElement | null {
|
|||||||
const [type, condition, queries] = watch(['type', 'condition', 'queries']);
|
const [type, condition, queries] = watch(['type', 'condition', 'queries']);
|
||||||
const { allDataSourcesAvailable } = useAlertQueriesStatus(queries);
|
const { allDataSourcesAvailable } = useAlertQueriesStatus(queries);
|
||||||
|
|
||||||
if (type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) {
|
if (!type || isDataSourceManagedRuleByType(type)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,10 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
|||||||
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
|
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
|
||||||
import {
|
import {
|
||||||
getRuleGroupLocationFromRuleWithLocation,
|
getRuleGroupLocationFromRuleWithLocation,
|
||||||
|
isGrafanaManagedRuleByType,
|
||||||
isGrafanaRulerRule,
|
isGrafanaRulerRule,
|
||||||
isGrafanaRulerRulePaused,
|
isGrafanaRulerRulePaused,
|
||||||
|
isRecordingRuleByType,
|
||||||
} from 'app/features/alerting/unified/utils/rules';
|
} from 'app/features/alerting/unified/utils/rules';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
@ -23,8 +25,8 @@ import { RuleWithLocation } from 'app/types/unified-alerting';
|
|||||||
import {
|
import {
|
||||||
LogMessages,
|
LogMessages,
|
||||||
logInfo,
|
logInfo,
|
||||||
trackAlertRuleFormError,
|
|
||||||
trackAlertRuleFormCancelled,
|
trackAlertRuleFormCancelled,
|
||||||
|
trackAlertRuleFormError,
|
||||||
trackAlertRuleFormSaved,
|
trackAlertRuleFormSaved,
|
||||||
} from '../../../Analytics';
|
} from '../../../Analytics';
|
||||||
import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||||
@ -33,8 +35,8 @@ import { 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 {
|
||||||
MANUAL_ROUTING_KEY,
|
|
||||||
DEFAULT_GROUP_EVALUATION_INTERVAL,
|
DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
|
MANUAL_ROUTING_KEY,
|
||||||
formValuesFromExistingRule,
|
formValuesFromExistingRule,
|
||||||
getDefaultFormValues,
|
getDefaultFormValues,
|
||||||
getDefaultQueries,
|
getDefaultQueries,
|
||||||
@ -42,7 +44,7 @@ import {
|
|||||||
normalizeDefaultAnnotations,
|
normalizeDefaultAnnotations,
|
||||||
} from '../../../utils/rule-form';
|
} from '../../../utils/rule-form';
|
||||||
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
|
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
|
||||||
import { AlertRuleNameInput } from '../AlertRuleNameInput';
|
import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
|
||||||
import AnnotationsStep from '../AnnotationsStep';
|
import AnnotationsStep from '../AnnotationsStep';
|
||||||
import { CloudEvaluationBehavior } from '../CloudEvaluationBehavior';
|
import { CloudEvaluationBehavior } from '../CloudEvaluationBehavior';
|
||||||
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
|
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
|
||||||
@ -106,7 +108,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
const type = watch('type');
|
const type = watch('type');
|
||||||
const dataSourceName = watch('dataSourceName');
|
const dataSourceName = watch('dataSourceName');
|
||||||
|
|
||||||
const showDataSourceDependantStep = Boolean(type && (type === RuleFormType.grafana || !!dataSourceName));
|
const showDataSourceDependantStep = Boolean(type && (isGrafanaManagedRuleByType(type) || !!dataSourceName));
|
||||||
|
|
||||||
const submitState = useUnifiedAlertingSelector((state) => state.ruleForm.saveRule) || initialAsyncRequestState;
|
const submitState = useUnifiedAlertingSelector((state) => state.ruleForm.saveRule) || initialAsyncRequestState;
|
||||||
useCleanup((state) => (state.unifiedAlerting.ruleForm.saveRule = initialAsyncRequestState));
|
useCleanup((state) => (state.unifiedAlerting.ruleForm.saveRule = initialAsyncRequestState));
|
||||||
@ -233,6 +235,9 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule);
|
const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule);
|
||||||
|
if (!type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
<AppChromeUpdate actions={actionButtons} />
|
<AppChromeUpdate actions={actionButtons} />
|
||||||
@ -242,14 +247,14 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||||
<Stack direction="column" gap={3}>
|
<Stack direction="column" gap={3}>
|
||||||
{/* Step 1 */}
|
{/* Step 1 */}
|
||||||
<AlertRuleNameInput />
|
<AlertRuleNameAndMetric />
|
||||||
{/* Step 2 */}
|
{/* Step 2 */}
|
||||||
<QueryAndExpressionsStep editingExistingRule={!!existing} onDataChange={checkAlertCondition} />
|
<QueryAndExpressionsStep editingExistingRule={!!existing} onDataChange={checkAlertCondition} />
|
||||||
{/* Step 3-4-5 */}
|
{/* Step 3-4-5 */}
|
||||||
{showDataSourceDependantStep && (
|
{showDataSourceDependantStep && (
|
||||||
<>
|
<>
|
||||||
{/* Step 3 */}
|
{/* Step 3 */}
|
||||||
{type === RuleFormType.grafana && (
|
{isGrafanaManagedRuleByType(type) && (
|
||||||
<GrafanaEvaluationBehavior
|
<GrafanaEvaluationBehavior
|
||||||
evaluateEvery={evaluateEvery}
|
evaluateEvery={evaluateEvery}
|
||||||
setEvaluateEvery={setEvaluateEvery}
|
setEvaluateEvery={setEvaluateEvery}
|
||||||
@ -266,7 +271,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
{/* Notifications step*/}
|
{/* Notifications step*/}
|
||||||
<NotificationsStep alertUid={uidFromParams} />
|
<NotificationsStep alertUid={uidFromParams} />
|
||||||
{/* Annotations only for cloud and Grafana */}
|
{/* Annotations only for cloud and Grafana */}
|
||||||
{type !== RuleFormType.cloudRecording && <AnnotationsStep />}
|
{!isRecordingRuleByType(type) && <AnnotationsStep />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -285,7 +290,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showEditYaml ? (
|
{showEditYaml ? (
|
||||||
type === RuleFormType.grafana ? (
|
isGrafanaManagedRuleByType(type) ? (
|
||||||
<GrafanaRuleExporter alertUid={uidFromParams} onClose={() => setShowEditYaml(false)} />
|
<GrafanaRuleExporter alertUid={uidFromParams} onClose={() => setShowEditYaml(false)} />
|
||||||
) : (
|
) : (
|
||||||
<RuleInspector onClose={() => setShowEditYaml(false)} />
|
<RuleInspector onClose={() => setShowEditYaml(false)} />
|
||||||
|
@ -21,8 +21,8 @@ import { DEFAULT_GROUP_EVALUATION_INTERVAL, formValuesToRulerGrafanaRuleDTO } fr
|
|||||||
import { isGrafanaRulerRule } from '../../../utils/rules';
|
import { isGrafanaRulerRule } from '../../../utils/rules';
|
||||||
import { FileExportPreview } from '../../export/FileExportPreview';
|
import { FileExportPreview } from '../../export/FileExportPreview';
|
||||||
import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
|
import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
|
||||||
import { allGrafanaExportProviders, ExportFormats } from '../../export/providers';
|
import { ExportFormats, allGrafanaExportProviders } from '../../export/providers';
|
||||||
import { AlertRuleNameInput } from '../AlertRuleNameInput';
|
import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
|
||||||
import AnnotationsStep from '../AnnotationsStep';
|
import AnnotationsStep from '../AnnotationsStep';
|
||||||
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
|
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
|
||||||
import { NotificationsStep } from '../NotificationsStep';
|
import { NotificationsStep } from '../NotificationsStep';
|
||||||
@ -88,7 +88,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
|
|||||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||||
<Stack direction="column" gap={3}>
|
<Stack direction="column" gap={3}>
|
||||||
{/* Step 1 */}
|
{/* Step 1 */}
|
||||||
<AlertRuleNameInput />
|
<AlertRuleNameAndMetric />
|
||||||
{/* Step 2 */}
|
{/* Step 2 */}
|
||||||
<QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} />
|
<QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} />
|
||||||
{/* Step 3-4-5 */}
|
{/* Step 3-4-5 */}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
import { RulerRuleDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { mockRulerGrafanaRule } from '../../../mocks';
|
import { mockRulerGrafanaRecordingRule, mockRulerGrafanaRule } from '../../../mocks';
|
||||||
import { RuleFormValues } from '../../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||||
import { Annotation } from '../../../utils/constants';
|
import { Annotation } from '../../../utils/constants';
|
||||||
import { getDefaultFormValues } from '../../../utils/rule-form';
|
import { getDefaultFormValues } from '../../../utils/rule-form';
|
||||||
|
|
||||||
@ -34,10 +34,19 @@ const rule3 = mockRulerGrafanaRule(
|
|||||||
{ uid: 'uid-rule-3', title: 'Rule3', data: [] }
|
{ uid: 'uid-rule-3', title: 'Rule3', data: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rule4 = mockRulerGrafanaRecordingRule(
|
||||||
|
{
|
||||||
|
labels: { severity: 'notcritical4', region: 'region4' },
|
||||||
|
annotations: { [Annotation.summary]: 'This grafana rule4' },
|
||||||
|
},
|
||||||
|
{ uid: 'uid-rule-4', title: 'Rule4', data: [] }
|
||||||
|
);
|
||||||
|
|
||||||
// Prepare the form values for rule2 updated
|
// Prepare the form values for rule2 updated
|
||||||
const defaultValues = getDefaultFormValues();
|
const defaultValues = getDefaultFormValues();
|
||||||
const formValuesForRule2Updated: RuleFormValues = {
|
const formValuesForRule2Updated: RuleFormValues = {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
|
type: RuleFormType.grafana,
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@ -56,6 +65,26 @@ const formValuesForRule2Updated: RuleFormValues = {
|
|||||||
labels: [{ key: 'newLabel', value: 'newLabel' }],
|
labels: [{ key: 'newLabel', value: 'newLabel' }],
|
||||||
annotations: [{ key: 'summary', value: 'This grafana rule2 updated' }],
|
annotations: [{ key: 'summary', value: 'This grafana rule2 updated' }],
|
||||||
};
|
};
|
||||||
|
const formValuesForRecordingRule4Updated: RuleFormValues = {
|
||||||
|
...defaultValues,
|
||||||
|
type: RuleFormType.grafanaRecording,
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
relativeTimeRange: { from: 900, to: 1000 },
|
||||||
|
datasourceUid: 'dsuid',
|
||||||
|
model: {
|
||||||
|
refId: 'A',
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
|
queryType: 'query',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
condition: 'A',
|
||||||
|
name: 'Rule4 updated',
|
||||||
|
labels: [{ key: 'newLabel', value: 'newLabel' }],
|
||||||
|
annotations: [{ key: 'summary', value: 'This grafana rule4 updated' }],
|
||||||
|
};
|
||||||
|
|
||||||
const expectedModifiedRule2 = (uid: string) => ({
|
const expectedModifiedRule2 = (uid: string) => ({
|
||||||
annotations: {
|
annotations: {
|
||||||
@ -90,24 +119,81 @@ const expectedModifiedRule2 = (uid: string) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expectedModifiedRule4 = (uid: string) => ({
|
||||||
|
annotations: {
|
||||||
|
summary: 'This grafana rule4 updated',
|
||||||
|
},
|
||||||
|
grafana_alert: {
|
||||||
|
condition: 'A',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
datasourceUid: 'dsuid',
|
||||||
|
model: {
|
||||||
|
refId: 'A',
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
|
queryType: 'query',
|
||||||
|
refId: 'A',
|
||||||
|
relativeTimeRange: {
|
||||||
|
from: 900,
|
||||||
|
to: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_paused: false,
|
||||||
|
notification_settings: undefined,
|
||||||
|
record: {
|
||||||
|
metric: 'Rule4 updated',
|
||||||
|
from: 'A',
|
||||||
|
},
|
||||||
|
title: 'Rule4 updated',
|
||||||
|
uid: uid,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
newLabel: 'newLabel',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('getPayloadFromDto', () => {
|
describe('getPayloadFromDto', () => {
|
||||||
const groupDto: RulerRuleGroupDTO<RulerRuleDTO> = {
|
const groupDto: RulerRuleGroupDTO<RulerRuleDTO> = {
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
rules: [rule1, rule2, rule3],
|
rules: [rule1, rule2, rule3, rule4],
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should return a ModifyExportPayload with the updated rule added to a group with this rule belongs, in the same position', () => {
|
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);
|
// for alerting rule
|
||||||
expect(result).toEqual({
|
const resultForAlerting = getPayloadToExport('uid-rule-2', formValuesForRule2Updated, groupDto);
|
||||||
|
expect(resultForAlerting).toEqual({
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
rules: [rule1, expectedModifiedRule2('uid-rule-2'), rule3],
|
rules: [rule1, expectedModifiedRule2('uid-rule-2'), rule3, rule4],
|
||||||
|
});
|
||||||
|
// for recording rule
|
||||||
|
const resultForRecording = getPayloadToExport(
|
||||||
|
'uid-rule-4',
|
||||||
|
{ ...formValuesForRecordingRule4Updated, type: RuleFormType.grafanaRecording },
|
||||||
|
groupDto
|
||||||
|
);
|
||||||
|
expect(resultForRecording).toEqual({
|
||||||
|
name: 'Test Group',
|
||||||
|
rules: [rule1, rule2, rule3, expectedModifiedRule4('uid-rule-4')],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
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', () => {
|
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', () => {
|
||||||
|
// for alerting rule
|
||||||
const result = getPayloadToExport('uid-rule-5', formValuesForRule2Updated, groupDto);
|
const result = getPayloadToExport('uid-rule-5', formValuesForRule2Updated, groupDto);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
rules: [rule1, rule2, rule3, expectedModifiedRule2('uid-rule-5')],
|
rules: [rule1, rule2, rule3, rule4, expectedModifiedRule2('uid-rule-5')],
|
||||||
|
});
|
||||||
|
// for recording rule
|
||||||
|
const resultForRecording = getPayloadToExport(
|
||||||
|
'uid-rule-5',
|
||||||
|
{ ...formValuesForRecordingRule4Updated, type: RuleFormType.grafanaRecording },
|
||||||
|
groupDto
|
||||||
|
);
|
||||||
|
expect(resultForRecording).toEqual({
|
||||||
|
name: 'Test Group',
|
||||||
|
rules: [rule1, rule2, rule3, rule4, expectedModifiedRule4('uid-rule-5')],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||||
import { useFormContext, Controller } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { getDefaultRelativeTimeRange, GrafanaTheme2 } from '@grafana/data';
|
import { getDefaultRelativeTimeRange, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@ -18,6 +18,13 @@ import { fetchAllPromBuildInfoAction } from '../../../state/actions';
|
|||||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||||
import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource';
|
import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource';
|
||||||
import { isPromOrLokiQuery, PromOrLokiQuery } from '../../../utils/rule-form';
|
import { isPromOrLokiQuery, PromOrLokiQuery } from '../../../utils/rule-form';
|
||||||
|
import {
|
||||||
|
isCloudAlertingRuleByType,
|
||||||
|
isCloudRecordingRuleByType,
|
||||||
|
isDataSourceManagedRuleByType,
|
||||||
|
isGrafanaAlertingRuleByType,
|
||||||
|
isGrafanaManagedRuleByType,
|
||||||
|
} from '../../../utils/rules';
|
||||||
import { ExpressionEditor } from '../ExpressionEditor';
|
import { ExpressionEditor } from '../ExpressionEditor';
|
||||||
import { ExpressionsEditor } from '../ExpressionsEditor';
|
import { ExpressionsEditor } from '../ExpressionsEditor';
|
||||||
import { NeedHelpInfo } from '../NeedHelpInfo';
|
import { NeedHelpInfo } from '../NeedHelpInfo';
|
||||||
@ -70,9 +77,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState);
|
const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState);
|
||||||
const [type, condition, dataSourceName] = watch(['type', 'condition', 'dataSourceName']);
|
const [type, condition, dataSourceName] = watch(['type', 'condition', 'dataSourceName']);
|
||||||
|
|
||||||
const isGrafanaManagedType = type === RuleFormType.grafana;
|
const isGrafanaAlertingType = isGrafanaAlertingRuleByType(type);
|
||||||
const isRecordingRuleType = type === RuleFormType.cloudRecording;
|
const isRecordingRuleType = isCloudRecordingRuleByType(type);
|
||||||
const isCloudAlertRuleType = type === RuleFormType.cloudAlerting;
|
const isCloudAlertRuleType = isCloudAlertingRuleByType(type);
|
||||||
|
|
||||||
const dispatchReduxAction = useDispatch();
|
const dispatchReduxAction = useDispatch();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -114,9 +121,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
const emptyQueries = queries.length === 0;
|
const emptyQueries = queries.length === 0;
|
||||||
|
|
||||||
// apply some validations and asserts to the results of the evaluation when creating or editing
|
// apply some validations and asserts to the results of the evaluation when creating or editing
|
||||||
// Grafana-managed alert rules
|
// Grafana-managed alert rules and Grafa-managed recording rules
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGrafanaManagedType) {
|
if (type && !isGrafanaManagedRuleByType(type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +140,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
const error = errorFromPreviewData(previewData) ?? errorFromCurrentCondition(previewData);
|
const error = errorFromPreviewData(previewData) ?? errorFromCurrentCondition(previewData);
|
||||||
|
|
||||||
onDataChange(error?.message || '');
|
onDataChange(error?.message || '');
|
||||||
}, [queryPreviewData, getValues, onDataChange, isGrafanaManagedType]);
|
}, [queryPreviewData, getValues, onDataChange, type]);
|
||||||
|
|
||||||
const handleSetCondition = useCallback(
|
const handleSetCondition = useCallback(
|
||||||
(refId: string | null) => {
|
(refId: string | null) => {
|
||||||
@ -361,7 +368,9 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { sectionTitle, helpLabel, helpContent, helpLink } = DESCRIPTIONS[type ?? RuleFormType.grafana];
|
const { sectionTitle, helpLabel, helpContent, helpLink } = DESCRIPTIONS[type ?? RuleFormType.grafana];
|
||||||
|
if (!type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection
|
<RuleEditorSection
|
||||||
stepNo={2}
|
stepNo={2}
|
||||||
@ -381,7 +390,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* This is the cloud data source selector */}
|
{/* This is the cloud data source selector */}
|
||||||
{(type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) && (
|
{isDataSourceManagedRuleByType(type) && (
|
||||||
<CloudDataSourceSelector onChangeCloudDatasource={onChangeCloudDatasource} disabled={editingExistingRule} />
|
<CloudDataSourceSelector onChangeCloudDatasource={onChangeCloudDatasource} disabled={editingExistingRule} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -429,8 +438,8 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* This is the editor for Grafana managed rules */}
|
{/* This is the editor for Grafana managed rules and Grafana managed recording rules */}
|
||||||
{isGrafanaManagedType && (
|
{isGrafanaManagedRuleByType(type) && (
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
{/* Data Queries */}
|
{/* Data Queries */}
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
@ -457,12 +466,15 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
|||||||
Add query
|
Add query
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{/* We only show Switch for Grafana managed alerts */}
|
||||||
|
{isGrafanaAlertingType && (
|
||||||
<SmartAlertTypeDetector
|
<SmartAlertTypeDetector
|
||||||
editingExistingRule={editingExistingRule}
|
editingExistingRule={editingExistingRule}
|
||||||
rulesSourcesWithRuler={rulesSourcesWithRuler}
|
rulesSourcesWithRuler={rulesSourcesWithRuler}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
onClickSwitch={onClickSwitch}
|
onClickSwitch={onClickSwitch}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{/* Expression Queries */}
|
{/* Expression Queries */}
|
||||||
<Stack direction="column" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<Text element="h5">Expressions</Text>
|
<Text element="h5">Expressions</Text>
|
||||||
|
@ -15,6 +15,13 @@ export const DESCRIPTIONS: Record<RuleFormType, FormDescriptions> = {
|
|||||||
'Pre-compute frequently needed or computationally expensive expressions and save their result as a new set of time series.',
|
'Pre-compute frequently needed or computationally expensive expressions and save their result as a new set of time series.',
|
||||||
helpLink: '',
|
helpLink: '',
|
||||||
},
|
},
|
||||||
|
[RuleFormType.grafanaRecording]: {
|
||||||
|
sectionTitle: 'Define recording rule',
|
||||||
|
helpLabel: 'Define your recording rule',
|
||||||
|
helpContent:
|
||||||
|
'Pre-compute frequently needed or computationally expensive expressions and save their result as a new set of time series.',
|
||||||
|
helpLink: '',
|
||||||
|
},
|
||||||
[RuleFormType.grafana]: {
|
[RuleFormType.grafana]: {
|
||||||
sectionTitle: 'Define query and alert condition',
|
sectionTitle: 'Define query and alert condition',
|
||||||
helpLabel: 'Define query and alert condition',
|
helpLabel: 'Define query and alert condition',
|
||||||
|
@ -327,6 +327,10 @@ export function translateRouteParamToRuleType(param = ''): RuleFormType {
|
|||||||
return RuleFormType.cloudRecording;
|
return RuleFormType.cloudRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (param === 'grafana-recording') {
|
||||||
|
return RuleFormType.grafanaRecording;
|
||||||
|
}
|
||||||
|
|
||||||
return RuleFormType.grafana;
|
return RuleFormType.grafana;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
getRulePluginOrigin,
|
getRulePluginOrigin,
|
||||||
isAlertingRule,
|
isAlertingRule,
|
||||||
isFederatedRuleGroup,
|
isFederatedRuleGroup,
|
||||||
|
isGrafanaRecordingRule,
|
||||||
isGrafanaRulerRule,
|
isGrafanaRulerRule,
|
||||||
isGrafanaRulerRulePaused,
|
isGrafanaRulerRulePaused,
|
||||||
isRecordingRule,
|
isRecordingRule,
|
||||||
@ -192,6 +193,13 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isGrafanaRecordingRule(rule.rulerRule)) {
|
||||||
|
const metric = rule.rulerRule?.grafana_alert.record?.metric ?? '';
|
||||||
|
metadata.push({
|
||||||
|
label: 'Metric name',
|
||||||
|
value: <Text color="primary">{metric}</Text>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (interval) {
|
if (interval) {
|
||||||
metadata.push({
|
metadata.push({
|
||||||
|
@ -3,7 +3,7 @@ import { formatDistanceToNowStrict } from 'date-fns';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Text, Stack, useStyles2, ClipboardButton, TextLink } from '@grafana/ui';
|
import { ClipboardButton, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
||||||
import { CombinedRule } from 'app/types/unified-alerting';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
import { Annotations } from 'app/types/unified-alerting-dto';
|
import { Annotations } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
@ -98,7 +98,10 @@ const Details = ({ rule }: DetailsProps) => {
|
|||||||
</MetaText>
|
</MetaText>
|
||||||
|
|
||||||
{/* nodata and execution error state mapping */}
|
{/* nodata and execution error state mapping */}
|
||||||
{isGrafanaRulerRule(rule.rulerRule) && (
|
{isGrafanaRulerRule(rule.rulerRule) &&
|
||||||
|
// grafana recording rules don't have these fields
|
||||||
|
rule.rulerRule.grafana_alert.no_data_state &&
|
||||||
|
rule.rulerRule.grafana_alert.exec_err_state && (
|
||||||
<>
|
<>
|
||||||
<MetaText direction="column">
|
<MetaText direction="column">
|
||||||
Alert state if no data or all values are null
|
Alert state if no data or all values are null
|
||||||
|
@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||||
import { LinkButton, LoadingPlaceholder, Pagination, Spinner, useStyles2, Text } from '@grafana/ui';
|
import { LinkButton, LoadingPlaceholder, Pagination, Spinner, Text, useStyles2 } from '@grafana/ui';
|
||||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||||
@ -136,6 +136,7 @@ export function CreateRecordingRuleButton() {
|
|||||||
href={urlUtil.renderUrl(`alerting/new/recording`, {
|
href={urlUtil.renderUrl(`alerting/new/recording`, {
|
||||||
returnTo: location.pathname + location.search,
|
returnTo: location.pathname + location.search,
|
||||||
})}
|
})}
|
||||||
|
tooltip="Create new Data source-managed recording rule"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
|
@ -4,24 +4,24 @@ import { useMemo } from 'react';
|
|||||||
import { FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form';
|
import { FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Badge, Button, Field, Input, Label, LinkButton, Modal, useStyles2, Stack, Alert } from '@grafana/ui';
|
import { Alert, Badge, Button, Field, Input, Label, LinkButton, Modal, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useUpdateRuleGroupConfiguration,
|
|
||||||
useRenameRuleGroup,
|
|
||||||
useMoveRuleGroup,
|
useMoveRuleGroup,
|
||||||
|
useRenameRuleGroup,
|
||||||
|
useUpdateRuleGroupConfiguration,
|
||||||
} from '../../hooks/ruleGroup/useUpdateRuleGroup';
|
} from '../../hooks/ruleGroup/useUpdateRuleGroup';
|
||||||
import { anyOfRequestState } from '../../hooks/useAsync';
|
import { anyOfRequestState } from '../../hooks/useAsync';
|
||||||
import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions';
|
import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions';
|
||||||
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
||||||
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource';
|
||||||
import { stringifyErrorLike } from '../../utils/misc';
|
import { stringifyErrorLike } from '../../utils/misc';
|
||||||
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
||||||
import { AlertInfo, getAlertInfo, isRecordingRulerRule } from '../../utils/rules';
|
import { AlertInfo, getAlertInfo, isGrafanaOrDataSourceRecordingRule } from '../../utils/rules';
|
||||||
import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusDuration } from '../../utils/time';
|
import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusDuration } from '../../utils/time';
|
||||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||||
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
||||||
@ -75,7 +75,8 @@ export const RulesForGroupTable = ({ rulesWithoutRecordingRules }: { rulesWithou
|
|||||||
}))
|
}))
|
||||||
.sort(
|
.sort(
|
||||||
(alert1, alert2) =>
|
(alert1, alert2) =>
|
||||||
safeParsePrometheusDuration(alert1.data.forDuration) - safeParsePrometheusDuration(alert2.data.forDuration)
|
safeParsePrometheusDuration(alert1.data.forDuration ?? '') -
|
||||||
|
safeParsePrometheusDuration(alert2.data.forDuration ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: AlertsWithForTableColumnProps[] = useMemo(() => {
|
const columns: AlertsWithForTableColumnProps[] = useMemo(() => {
|
||||||
@ -154,9 +155,11 @@ export const evaluateEveryValidationOptions = (rules: RulerRuleDTO[]): RegisterO
|
|||||||
} else {
|
} else {
|
||||||
const rulePendingPeriods = rules.map((rule) => {
|
const rulePendingPeriods = rules.map((rule) => {
|
||||||
const { forDuration } = getAlertInfo(rule, evaluateEvery);
|
const { forDuration } = getAlertInfo(rule, evaluateEvery);
|
||||||
return safeParsePrometheusDuration(forDuration);
|
return forDuration ? safeParsePrometheusDuration(forDuration) : null;
|
||||||
});
|
});
|
||||||
const largestPendingPeriod = Math.min(...rulePendingPeriods);
|
const largestPendingPeriod = Math.min(
|
||||||
|
...rulePendingPeriods.filter((period): period is number => period !== null)
|
||||||
|
);
|
||||||
return `Evaluation interval should be smaller or equal to "pending period" values for existing rules in this rule group. Choose a value smaller than or equal to "${formatPrometheusDuration(largestPendingPeriod)}".`;
|
return `Evaluation interval should be smaller or equal to "pending period" values for existing rules in this rule group. Choose a value smaller than or equal to "${formatPrometheusDuration(largestPendingPeriod)}".`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -262,7 +265,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rulesWithoutRecordingRules = compact(
|
const rulesWithoutRecordingRules = compact(
|
||||||
group.rules.map((r) => r.rulerRule).filter((rule) => !isRecordingRulerRule(rule))
|
group.rules.map((r) => r.rulerRule).filter((rule) => !isGrafanaOrDataSourceRecordingRule(rule))
|
||||||
);
|
);
|
||||||
const hasSomeNoRecordingRules = rulesWithoutRecordingRules.length > 0;
|
const hasSomeNoRecordingRules = rulesWithoutRecordingRules.length > 0;
|
||||||
const modalTitle =
|
const modalTitle =
|
||||||
|
@ -2,11 +2,14 @@ import { css } from '@emotion/css';
|
|||||||
import { useToggle } from 'react-use';
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, LoadingPlaceholder, Pagination, Spinner, useStyles2, Text } from '@grafana/ui';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Button, LinkButton, LoadingPlaceholder, Pagination, Spinner, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||||
|
import { LogMessages, logInfo } from '../../Analytics';
|
||||||
import { AlertingAction, useAlertingAbility } from '../../hooks/useAbilities';
|
import { AlertingAction, useAlertingAbility } from '../../hooks/useAbilities';
|
||||||
import { flattenGrafanaManagedRules } from '../../hooks/useCombinedRuleNamespaces';
|
import { flattenGrafanaManagedRules } from '../../hooks/useCombinedRuleNamespaces';
|
||||||
import { usePagination } from '../../hooks/usePagination';
|
import { usePagination } from '../../hooks/usePagination';
|
||||||
@ -14,6 +17,7 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect
|
|||||||
import { getPaginationStyles } from '../../styles/pagination';
|
import { getPaginationStyles } from '../../styles/pagination';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { initialAsyncRequestState } from '../../utils/redux';
|
import { initialAsyncRequestState } from '../../utils/redux';
|
||||||
|
import { createUrl } from '../../utils/url';
|
||||||
import { GrafanaRulesExporter } from '../export/GrafanaRulesExporter';
|
import { GrafanaRulesExporter } from '../export/GrafanaRulesExporter';
|
||||||
|
|
||||||
import { RulesGroup } from './RulesGroup';
|
import { RulesGroup } from './RulesGroup';
|
||||||
@ -53,14 +57,21 @@ export const GrafanaRules = ({ namespaces, expandAll }: Props) => {
|
|||||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||||
const hasGrafanaAlerts = namespaces.length > 0;
|
const hasGrafanaAlerts = namespaces.length > 0;
|
||||||
|
|
||||||
|
const grafanaRecordingRulesEnabled = config.featureToggles.grafanaManagedRecordingRules;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.wrapper}>
|
<section className={styles.wrapper}>
|
||||||
<div className={styles.sectionHeader}>
|
<div className={styles.sectionHeader}>
|
||||||
<div className={styles.headerRow}>
|
<div className={styles.headerRow}>
|
||||||
<Text element="h2" variant="h5">
|
<Text element="h2" variant="h5">
|
||||||
Grafana
|
<Trans i18nKey="alerting.grafana-rules.title">Grafana</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{loading ? <LoadingPlaceholder className={styles.loader} text="Loading..." /> : <div />}
|
{loading ? (
|
||||||
|
<LoadingPlaceholder className={styles.loader} text={t('alerting.grafana-rules.loading', 'Loading...')} />
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||||
{hasGrafanaAlerts && canExportRules && (
|
{hasGrafanaAlerts && canExportRules && (
|
||||||
<Button
|
<Button
|
||||||
aria-label="export all grafana rules"
|
aria-label="export all grafana rules"
|
||||||
@ -70,9 +81,23 @@ export const GrafanaRules = ({ namespaces, expandAll }: Props) => {
|
|||||||
onClick={toggleShowExportDrawer}
|
onClick={toggleShowExportDrawer}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
Export rules
|
<Trans i18nKey="alerting.grafana-rules.export-rules">Export rules</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{grafanaRecordingRulesEnabled && (
|
||||||
|
<LinkButton
|
||||||
|
href={createUrl('/alerting/new/grafana-recording', {
|
||||||
|
returnTo: '/alerting/list' + location.search,
|
||||||
|
})}
|
||||||
|
icon="plus"
|
||||||
|
variant="secondary"
|
||||||
|
tooltip="Create new Grafana-managed recording rule"
|
||||||
|
onClick={() => logInfo(LogMessages.grafanaRecording)}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="alerting.grafana-rules.new-recording-rule">New recording rule</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { useState } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { LinkButton, useStyles2, Stack } from '@grafana/ui';
|
import { LinkButton, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu';
|
import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu';
|
||||||
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
|
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
|
||||||
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
|
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
|
||||||
@ -18,7 +18,7 @@ import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
|||||||
import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datasource';
|
||||||
import { createViewLink } from '../../utils/misc';
|
import { createViewLink } from '../../utils/misc';
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { createUrl } from '../../utils/url';
|
import { createUrl } from '../../utils/url';
|
||||||
|
|
||||||
import { RedirectToCloneRule } from './CloneRule';
|
import { RedirectToCloneRule } from './CloneRule';
|
||||||
@ -138,7 +138,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, showCopyLinkButton
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{deleteModal}
|
{deleteModal}
|
||||||
{isGrafanaRulerRule(rule.rulerRule) && showSilenceDrawer && (
|
{isGrafanaAlertingRule(rule.rulerRule) && showSilenceDrawer && (
|
||||||
<AlertmanagerProvider accessType="instance">
|
<AlertmanagerProvider accessType="instance">
|
||||||
<SilenceGrafanaRuleDrawer rulerRule={rule.rulerRule} onClose={() => setShowSilenceDrawer(false)} />
|
<SilenceGrafanaRuleDrawer rulerRule={rule.rulerRule} onClose={() => setShowSilenceDrawer(false)} />
|
||||||
</AlertmanagerProvider>
|
</AlertmanagerProvider>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
|
import { GrafanaTheme2, dateTime, dateTimeFormat } from '@grafana/data';
|
||||||
import { useStyles2, Tooltip } from '@grafana/ui';
|
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { Time } from 'app/features/explore/Time';
|
import { Time } from 'app/features/explore/Time';
|
||||||
import { CombinedRule } from 'app/types/unified-alerting';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import { useCleanAnnotations } from '../../utils/annotations';
|
import { useCleanAnnotations } from '../../utils/annotations';
|
||||||
import { isRecordingRulerRule } from '../../utils/rules';
|
import { isGrafanaRecordingRule, isRecordingRulerRule } from '../../utils/rules';
|
||||||
import { isNullDate } from '../../utils/time';
|
import { isNullDate } from '../../utils/time';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
import { DetailsField } from '../DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
@ -68,6 +68,7 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
|
|||||||
let every = rule.group.interval;
|
let every = rule.group.interval;
|
||||||
let lastEvaluation = rule.promRule?.lastEvaluation;
|
let lastEvaluation = rule.promRule?.lastEvaluation;
|
||||||
let lastEvaluationDuration = rule.promRule?.evaluationTime;
|
let lastEvaluationDuration = rule.promRule?.evaluationTime;
|
||||||
|
const metric = isGrafanaRecordingRule(rule.rulerRule) ? rule.rulerRule?.grafana_alert.record?.metric : undefined;
|
||||||
|
|
||||||
// recording rules don't have a for duration
|
// recording rules don't have a for duration
|
||||||
if (!isRecordingRulerRule(rule.rulerRule)) {
|
if (!isRecordingRulerRule(rule.rulerRule)) {
|
||||||
@ -76,6 +77,11 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{metric && (
|
||||||
|
<DetailsField label="Metric" horizontal={true}>
|
||||||
|
{metric}
|
||||||
|
</DetailsField>
|
||||||
|
)}
|
||||||
{every && (
|
{every && (
|
||||||
<DetailsField label="Evaluate" horizontal={true}>
|
<DetailsField label="Evaluate" horizontal={true}>
|
||||||
Every {every}
|
Every {every}
|
||||||
|
@ -10,7 +10,7 @@ import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
|||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { isCloudRulesSource } from '../../utils/datasource';
|
import { isCloudRulesSource } from '../../utils/datasource';
|
||||||
import { createExploreLink } from '../../utils/misc';
|
import { createExploreLink } from '../../utils/misc';
|
||||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
import { isFederatedRuleGroup, isGrafanaAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rule: CombinedRule;
|
rule: CombinedRule;
|
||||||
@ -103,7 +103,7 @@ const RuleDetailsButtons = ({ rule, rulesSource }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGrafanaRulerRule(rule.rulerRule)) {
|
if (isGrafanaAlertingRule(rule.rulerRule)) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Fragment key="history">
|
<Fragment key="history">
|
||||||
<Button
|
<Button
|
||||||
|
@ -2,11 +2,13 @@ import { css } from '@emotion/css';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
|
||||||
import { Spinner, useStyles2, Stack } from '@grafana/ui';
|
import { Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
import { Trans } from 'app/core/internationalization';
|
||||||
import { CombinedRule } from 'app/types/unified-alerting';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { isAlertingRule, isRecordingRule, getFirstActiveAt } from '../../utils/rules';
|
import { getFirstActiveAt, isAlertingRule, isGrafanaRecordingRule, isRecordingRule } from '../../utils/rules';
|
||||||
|
import { StateTag } from '../StateTag';
|
||||||
|
|
||||||
import { AlertStateTag } from './AlertStateTag';
|
import { AlertStateTag } from './AlertStateTag';
|
||||||
|
|
||||||
@ -19,9 +21,22 @@ interface Props {
|
|||||||
|
|
||||||
export const RuleState = ({ rule, isDeleting, isCreating, isPaused }: Props) => {
|
export const RuleState = ({ rule, isDeleting, isCreating, isPaused }: Props) => {
|
||||||
const style = useStyles2(getStyle);
|
const style = useStyles2(getStyle);
|
||||||
const { promRule } = rule;
|
const { promRule, rulerRule } = rule;
|
||||||
|
|
||||||
// return how long the rule has been in its firing state, if any
|
// return how long the rule has been in its firing state, if any
|
||||||
|
const RecordingRuleState = () => {
|
||||||
|
if (isPaused && isGrafanaRecordingRule(rulerRule)) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={'Recording rule evaluation is currently paused'} placement="top">
|
||||||
|
<StateTag state="warning">
|
||||||
|
<Icon name="pause" size="xs" />
|
||||||
|
<Trans i18nKey="alerting.rule-state.paused">Paused</Trans>
|
||||||
|
</StateTag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Trans i18nKey="alerting.rule-state.recording-rule">Recording rule</Trans>;
|
||||||
|
}
|
||||||
|
};
|
||||||
const forTime = useMemo(() => {
|
const forTime = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
promRule &&
|
promRule &&
|
||||||
@ -55,14 +70,14 @@ export const RuleState = ({ rule, isDeleting, isCreating, isPaused }: Props) =>
|
|||||||
return (
|
return (
|
||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
Deleting
|
<Trans i18nKey="alerting.rule-state.deleting">Deleting</Trans>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else if (isCreating) {
|
} else if (isCreating) {
|
||||||
return (
|
return (
|
||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
Creating
|
<Trans i18nKey="alerting.rule-state.creating">Creating</Trans>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else if (promRule && isAlertingRule(promRule)) {
|
} else if (promRule && isAlertingRule(promRule)) {
|
||||||
@ -73,7 +88,7 @@ export const RuleState = ({ rule, isDeleting, isCreating, isPaused }: Props) =>
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else if (promRule && isRecordingRule(promRule)) {
|
} else if (promRule && isRecordingRule(promRule)) {
|
||||||
return <>Recording rule</>;
|
return <RecordingRuleState />;
|
||||||
}
|
}
|
||||||
return <>n/a</>;
|
return <>n/a</>;
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ import { useAlertmanager } from '../state/AlertmanagerContext';
|
|||||||
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
|
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
import { isAdmin } from '../utils/misc';
|
import { isAdmin } from '../utils/misc';
|
||||||
import { isFederatedRuleGroup, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules';
|
import { isFederatedRuleGroup, isGrafanaRecordingRule, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules';
|
||||||
|
|
||||||
import { useIsRuleEditable } from './useIsRuleEditable';
|
import { useIsRuleEditable } from './useIsRuleEditable';
|
||||||
|
|
||||||
@ -284,6 +284,7 @@ export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability
|
|||||||
function useCanSilence(rule: CombinedRule): [boolean, boolean] {
|
function useCanSilence(rule: CombinedRule): [boolean, boolean] {
|
||||||
const rulesSource = rule.namespace.rulesSource;
|
const rulesSource = rule.namespace.rulesSource;
|
||||||
const isGrafanaManagedRule = rulesSource === GRAFANA_RULES_SOURCE_NAME;
|
const isGrafanaManagedRule = rulesSource === GRAFANA_RULES_SOURCE_NAME;
|
||||||
|
const isGrafanaRecording = isGrafanaRecordingRule(rule.rulerRule);
|
||||||
|
|
||||||
const { currentData: amConfigStatus, isLoading } =
|
const { currentData: amConfigStatus, isLoading } =
|
||||||
alertmanagerApi.endpoints.getGrafanaAlertingConfigurationStatus.useQuery(undefined, {
|
alertmanagerApi.endpoints.getGrafanaAlertingConfigurationStatus.useQuery(undefined, {
|
||||||
@ -293,9 +294,9 @@ function useCanSilence(rule: CombinedRule): [boolean, boolean] {
|
|||||||
const folderUID = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.namespace_uid : undefined;
|
const folderUID = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.namespace_uid : undefined;
|
||||||
const { loading: folderIsLoading, folder } = useFolder(folderUID);
|
const { loading: folderIsLoading, folder } = useFolder(folderUID);
|
||||||
|
|
||||||
// we don't support silencing when the rule is not a Grafana managed rule
|
// we don't support silencing when the rule is not a Grafana managed alerting rule
|
||||||
// we simply don't know what Alertmanager the ruler is sending alerts to
|
// we simply don't know what Alertmanager the ruler is sending alerts to
|
||||||
if (!isGrafanaManagedRule || isLoading || folderIsLoading || !folder) {
|
if (!isGrafanaManagedRule || isGrafanaRecording || isLoading || folderIsLoading || !folder) {
|
||||||
return [false, false];
|
return [false, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +142,42 @@ export const mockRulerGrafanaRule = (
|
|||||||
...partial,
|
...partial,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
export const mockRulerGrafanaRecordingRule = (
|
||||||
|
partial: Partial<RulerGrafanaRuleDTO> = {},
|
||||||
|
partialDef: Partial<GrafanaRuleDefinition> = {}
|
||||||
|
): RulerGrafanaRuleDTO => {
|
||||||
|
return {
|
||||||
|
grafana_alert: {
|
||||||
|
uid: '123',
|
||||||
|
title: 'myalert',
|
||||||
|
namespace_uid: '123',
|
||||||
|
rule_group: 'my-group',
|
||||||
|
condition: 'A',
|
||||||
|
record: {
|
||||||
|
metric: 'myalert',
|
||||||
|
from: 'A',
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
datasourceUid: '123',
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'huh',
|
||||||
|
model: {
|
||||||
|
refId: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...partialDef,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
message: 'alert with severity "{{.warning}}}"',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
severity: 'warning',
|
||||||
|
},
|
||||||
|
...partial,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const mockRulerAlertingRule = (partial: Partial<RulerAlertingRuleDTO> = {}): RulerAlertingRuleDTO => ({
|
export const mockRulerAlertingRule = (partial: Partial<RulerAlertingRuleDTO> = {}): RulerAlertingRuleDTO => ({
|
||||||
alert: 'alert1',
|
alert: 'alert1',
|
||||||
|
@ -52,7 +52,7 @@ import { discoverFeatures } from '../api/buildInfo';
|
|||||||
import { fetchNotifiers } from '../api/grafana';
|
import { fetchNotifiers } from '../api/grafana';
|
||||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||||
import { FetchRulerRulesFilter, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from '../api/ruler';
|
import { FetchRulerRulesFilter, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from '../api/ruler';
|
||||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
import { RuleFormValues } from '../types/rule-form';
|
||||||
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
||||||
import {
|
import {
|
||||||
GRAFANA_RULES_SOURCE_NAME,
|
GRAFANA_RULES_SOURCE_NAME,
|
||||||
@ -64,7 +64,13 @@ import { makeAMLink } from '../utils/misc';
|
|||||||
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
|
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
|
||||||
import * as ruleId from '../utils/rule-id';
|
import * as ruleId from '../utils/rule-id';
|
||||||
import { getRulerClient } from '../utils/rulerClient';
|
import { getRulerClient } from '../utils/rulerClient';
|
||||||
import { getAlertInfo, isGrafanaRulerRule, isRulerNotSupportedResponse } from '../utils/rules';
|
import {
|
||||||
|
getAlertInfo,
|
||||||
|
isDataSourceManagedRuleByType,
|
||||||
|
isGrafanaManagedRuleByType,
|
||||||
|
isGrafanaRulerRule,
|
||||||
|
isRulerNotSupportedResponse,
|
||||||
|
} from '../utils/rules';
|
||||||
import { safeParsePrometheusDuration } from '../utils/time';
|
import { safeParsePrometheusDuration } from '../utils/time';
|
||||||
|
|
||||||
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
|
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
|
||||||
@ -357,13 +363,16 @@ export const saveRuleFormAction = createAsyncThunk(
|
|||||||
withSerializedError(
|
withSerializedError(
|
||||||
(async () => {
|
(async () => {
|
||||||
const { type } = values;
|
const { type } = values;
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO getRulerConfig should be smart enough to provide proper rulerClient implementation
|
// TODO getRulerConfig should be smart enough to provide proper rulerClient implementation
|
||||||
// For the dataSourceName specified
|
// For the dataSourceName specified
|
||||||
// in case of system (cortex/loki)
|
// in case of system (cortex/loki)
|
||||||
let identifier: RuleIdentifier;
|
let identifier: RuleIdentifier;
|
||||||
|
|
||||||
if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) {
|
if (isDataSourceManagedRuleByType(type)) {
|
||||||
if (!values.dataSourceName) {
|
if (!values.dataSourceName) {
|
||||||
throw new Error('The Data source has not been defined.');
|
throw new Error('The Data source has not been defined.');
|
||||||
}
|
}
|
||||||
@ -372,8 +381,8 @@ export const saveRuleFormAction = createAsyncThunk(
|
|||||||
const rulerClient = getRulerClient(rulerConfig);
|
const rulerClient = getRulerClient(rulerConfig);
|
||||||
identifier = await rulerClient.saveLotexRule(values, evaluateEvery, existing);
|
identifier = await rulerClient.saveLotexRule(values, evaluateEvery, existing);
|
||||||
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: values.dataSourceName }));
|
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: values.dataSourceName }));
|
||||||
// in case of grafana managed
|
// in case of grafana managed rules or grafana-managed recording rules
|
||||||
} else if (type === RuleFormType.grafana) {
|
} else if (isGrafanaManagedRuleByType(type)) {
|
||||||
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, GRAFANA_RULES_SOURCE_NAME);
|
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, GRAFANA_RULES_SOURCE_NAME);
|
||||||
const rulerClient = getRulerClient(rulerConfig);
|
const rulerClient = getRulerClient(rulerConfig);
|
||||||
identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing);
|
identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing);
|
||||||
@ -656,10 +665,10 @@ export const testReceiversAction = createAsyncThunk(
|
|||||||
export const rulesInSameGroupHaveInvalidFor = (rules: RulerRuleDTO[], everyDuration: string) => {
|
export const rulesInSameGroupHaveInvalidFor = (rules: RulerRuleDTO[], everyDuration: string) => {
|
||||||
return rules.filter((rule: RulerRuleDTO) => {
|
return rules.filter((rule: RulerRuleDTO) => {
|
||||||
const { forDuration } = getAlertInfo(rule, everyDuration);
|
const { forDuration } = getAlertInfo(rule, everyDuration);
|
||||||
const forNumber = safeParsePrometheusDuration(forDuration);
|
const forNumber = forDuration ? safeParsePrometheusDuration(forDuration) : null;
|
||||||
const everyNumber = safeParsePrometheusDuration(everyDuration);
|
const everyNumber = safeParsePrometheusDuration(everyDuration);
|
||||||
|
|
||||||
return forNumber !== 0 && forNumber < everyNumber;
|
return forNumber ? forNumber !== 0 && forNumber < everyNumber : false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ import { AlertQuery, GrafanaAlertStateDecision } from 'app/types/unified-alertin
|
|||||||
import { Folder } from '../components/rule-editor/RuleFolderPicker';
|
import { Folder } from '../components/rule-editor/RuleFolderPicker';
|
||||||
|
|
||||||
export enum RuleFormType {
|
export enum RuleFormType {
|
||||||
grafana = 'grafana',
|
grafana = 'grafana-alerting',
|
||||||
|
grafanaRecording = 'grafana-recording',
|
||||||
cloudAlerting = 'cloud-alerting',
|
cloudAlerting = 'cloud-alerting',
|
||||||
cloudRecording = 'cloud-recording',
|
cloudRecording = 'cloud-recording',
|
||||||
}
|
}
|
||||||
@ -45,6 +46,7 @@ export interface RuleFormValues {
|
|||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
manualRouting: boolean; // if true contactPoints are used. This field will not be used for saving the rule
|
manualRouting: boolean; // if true contactPoints are used. This field will not be used for saving the rule
|
||||||
contactPoints?: AlertManagerManualRouting;
|
contactPoints?: AlertManagerManualRouting;
|
||||||
|
metric?: string;
|
||||||
|
|
||||||
// cortex / loki rules
|
// cortex / loki rules
|
||||||
namespace: string;
|
namespace: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form values 1`] = `
|
exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form values for grafana alerting rule 1`] = `
|
||||||
{
|
{
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"description": "",
|
"description": "",
|
||||||
@ -23,6 +23,29 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form values for grafana recording rule 1`] = `
|
||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"description": "",
|
||||||
|
"runbook_url": "",
|
||||||
|
"summary": "",
|
||||||
|
},
|
||||||
|
"grafana_alert": {
|
||||||
|
"condition": "A",
|
||||||
|
"data": [],
|
||||||
|
"is_paused": false,
|
||||||
|
"record": {
|
||||||
|
"from": "A",
|
||||||
|
"metric": "",
|
||||||
|
},
|
||||||
|
"title": "",
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range type queries 1`] = `
|
exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range type queries 1`] = `
|
||||||
{
|
{
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
@ -20,7 +20,7 @@ export function alertRuleToQueries(combinedRule: CombinedRule | undefined | null
|
|||||||
|
|
||||||
if (isGrafanaRulerRule(rulerRule)) {
|
if (isGrafanaRulerRule(rulerRule)) {
|
||||||
const query = rulerRule.grafana_alert.data;
|
const query = rulerRule.grafana_alert.data;
|
||||||
return widenRelativeTimeRanges(query, rulerRule.for, combinedRule.group.interval);
|
return widenRelativeTimeRanges(query, rulerRule.for ?? '', combinedRule.group.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCloudRulesSource(rulesSource)) {
|
if (isCloudRulesSource(rulesSource)) {
|
||||||
|
@ -17,10 +17,21 @@ import {
|
|||||||
} from './rule-form';
|
} from './rule-form';
|
||||||
|
|
||||||
describe('formValuesToRulerGrafanaRuleDTO', () => {
|
describe('formValuesToRulerGrafanaRuleDTO', () => {
|
||||||
it('should correctly convert rule form values', () => {
|
it('should correctly convert rule form values for grafana alerting rule', () => {
|
||||||
const formValues: RuleFormValues = {
|
const formValues: RuleFormValues = {
|
||||||
...getDefaultFormValues(),
|
...getDefaultFormValues(),
|
||||||
condition: 'A',
|
condition: 'A',
|
||||||
|
type: RuleFormType.grafana,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(formValuesToRulerGrafanaRuleDTO(formValues)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly convert rule form values for grafana recording rule', () => {
|
||||||
|
const formValues: RuleFormValues = {
|
||||||
|
...getDefaultFormValues(),
|
||||||
|
condition: 'A',
|
||||||
|
type: RuleFormType.grafanaRecording,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(formValuesToRulerGrafanaRuleDTO(formValues)).toMatchSnapshot();
|
expect(formValuesToRulerGrafanaRuleDTO(formValues)).toMatchSnapshot();
|
||||||
@ -31,6 +42,7 @@ describe('formValuesToRulerGrafanaRuleDTO', () => {
|
|||||||
|
|
||||||
const values: RuleFormValues = {
|
const values: RuleFormValues = {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
|
type: RuleFormType.grafana,
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
|
@ -47,7 +47,13 @@ import { getRulesAccess } from './access-control';
|
|||||||
import { Annotation, defaultAnnotations } from './constants';
|
import { Annotation, defaultAnnotations } from './constants';
|
||||||
import { getDefaultOrFirstCompatibleDataSource, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource';
|
import { getDefaultOrFirstCompatibleDataSource, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource';
|
||||||
import { arrayToRecord, recordToArray } from './misc';
|
import { arrayToRecord, recordToArray } from './misc';
|
||||||
import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './rules';
|
import {
|
||||||
|
isAlertingRulerRule,
|
||||||
|
isGrafanaAlertingRuleByType,
|
||||||
|
isGrafanaRecordingRule,
|
||||||
|
isGrafanaRulerRule,
|
||||||
|
isRecordingRulerRule,
|
||||||
|
} from './rules';
|
||||||
import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } from './time';
|
import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } from './time';
|
||||||
|
|
||||||
export type PromOrLokiQuery = PromQuery | LokiQuery;
|
export type PromOrLokiQuery = PromQuery | LokiQuery;
|
||||||
@ -194,29 +200,62 @@ export function getNotificationSettingsForDTO(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO {
|
export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO {
|
||||||
const { name, condition, noDataState, execErrState, evaluateFor, queries, isPaused, contactPoints, manualRouting } =
|
const {
|
||||||
values;
|
name,
|
||||||
|
condition,
|
||||||
|
noDataState,
|
||||||
|
execErrState,
|
||||||
|
evaluateFor,
|
||||||
|
queries,
|
||||||
|
isPaused,
|
||||||
|
contactPoints,
|
||||||
|
manualRouting,
|
||||||
|
type,
|
||||||
|
metric,
|
||||||
|
} = values;
|
||||||
if (condition) {
|
if (condition) {
|
||||||
const notificationSettings: GrafanaNotificationSettings | undefined = getNotificationSettingsForDTO(
|
const notificationSettings: GrafanaNotificationSettings | undefined = getNotificationSettingsForDTO(
|
||||||
manualRouting,
|
manualRouting,
|
||||||
contactPoints
|
contactPoints
|
||||||
);
|
);
|
||||||
|
if (isGrafanaAlertingRuleByType(type)) {
|
||||||
return {
|
return {
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
title: name,
|
title: name,
|
||||||
condition,
|
condition,
|
||||||
no_data_state: noDataState,
|
|
||||||
exec_err_state: execErrState,
|
|
||||||
data: queries.map(fixBothInstantAndRangeQuery),
|
data: queries.map(fixBothInstantAndRangeQuery),
|
||||||
is_paused: Boolean(isPaused),
|
is_paused: Boolean(isPaused),
|
||||||
|
|
||||||
|
// Alerting rule specific
|
||||||
|
no_data_state: noDataState,
|
||||||
|
exec_err_state: execErrState,
|
||||||
notification_settings: notificationSettings,
|
notification_settings: notificationSettings,
|
||||||
},
|
},
|
||||||
|
annotations: arrayToRecord(values.annotations || []),
|
||||||
|
labels: arrayToRecord(values.labels || []),
|
||||||
|
|
||||||
|
// Alerting rule specific
|
||||||
for: evaluateFor,
|
for: evaluateFor,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
grafana_alert: {
|
||||||
|
title: name,
|
||||||
|
condition,
|
||||||
|
data: queries.map(fixBothInstantAndRangeQuery),
|
||||||
|
is_paused: Boolean(isPaused),
|
||||||
|
|
||||||
|
// Recording rule specific
|
||||||
|
record: {
|
||||||
|
metric: metric ?? name,
|
||||||
|
from: condition,
|
||||||
|
},
|
||||||
|
},
|
||||||
annotations: arrayToRecord(values.annotations || []),
|
annotations: arrayToRecord(values.annotations || []),
|
||||||
labels: arrayToRecord(values.labels || []),
|
labels: arrayToRecord(values.labels || []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throw new Error('Cannot create rule without specifying alert condition');
|
throw new Error('Cannot create rule without specifying alert condition');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,11 +290,29 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
|
|
||||||
const defaultFormValues = getDefaultFormValues();
|
const defaultFormValues = getDefaultFormValues();
|
||||||
if (isGrafanaRulesSource(ruleSourceName)) {
|
if (isGrafanaRulesSource(ruleSourceName)) {
|
||||||
if (isGrafanaRulerRule(rule)) {
|
// GRAFANA-MANAGED RULES
|
||||||
|
if (isGrafanaRecordingRule(rule)) {
|
||||||
|
// grafana recording rule
|
||||||
|
const ga = rule.grafana_alert;
|
||||||
|
return {
|
||||||
|
...defaultFormValues,
|
||||||
|
name: ga.title,
|
||||||
|
type: RuleFormType.grafanaRecording,
|
||||||
|
group: group.name,
|
||||||
|
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||||
|
queries: ga.data,
|
||||||
|
condition: ga.condition,
|
||||||
|
annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)),
|
||||||
|
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
||||||
|
folder: { title: namespace, uid: ga.namespace_uid },
|
||||||
|
isPaused: ga.is_paused,
|
||||||
|
metric: ga.record?.metric,
|
||||||
|
};
|
||||||
|
} else if (isGrafanaRulerRule(rule)) {
|
||||||
|
// grafana alerting rule
|
||||||
const ga = rule.grafana_alert;
|
const ga = rule.grafana_alert;
|
||||||
|
|
||||||
const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga);
|
const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga);
|
||||||
|
if (ga.no_data_state !== undefined && ga.exec_err_state !== undefined) {
|
||||||
return {
|
return {
|
||||||
...defaultFormValues,
|
...defaultFormValues,
|
||||||
name: ga.title,
|
name: ga.title,
|
||||||
@ -279,6 +336,10 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
throw new Error('Unexpected type of rule for grafana rules source');
|
throw new Error('Unexpected type of rule for grafana rules source');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
throw new Error('Unexpected type of rule for grafana rules source');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// DATASOURCE-MANAGED RULES
|
||||||
if (isAlertingRulerRule(rule)) {
|
if (isAlertingRulerRule(rule)) {
|
||||||
const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? '';
|
const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? '';
|
||||||
|
|
||||||
|
@ -10,19 +10,18 @@ import {
|
|||||||
CombinedRuleGroup,
|
CombinedRuleGroup,
|
||||||
CombinedRuleWithLocation,
|
CombinedRuleWithLocation,
|
||||||
GrafanaRuleIdentifier,
|
GrafanaRuleIdentifier,
|
||||||
PrometheusRuleIdentifier,
|
|
||||||
PromRuleWithLocation,
|
PromRuleWithLocation,
|
||||||
|
PrometheusRuleIdentifier,
|
||||||
RecordingRule,
|
RecordingRule,
|
||||||
Rule,
|
Rule,
|
||||||
RuleIdentifier,
|
|
||||||
RuleGroupIdentifier,
|
RuleGroupIdentifier,
|
||||||
|
RuleIdentifier,
|
||||||
RuleNamespace,
|
RuleNamespace,
|
||||||
RuleWithLocation,
|
RuleWithLocation,
|
||||||
} from 'app/types/unified-alerting';
|
} from 'app/types/unified-alerting';
|
||||||
import {
|
import {
|
||||||
GrafanaAlertState,
|
GrafanaAlertState,
|
||||||
GrafanaAlertStateWithReason,
|
GrafanaAlertStateWithReason,
|
||||||
mapStateWithReasonToBaseState,
|
|
||||||
PostableRuleDTO,
|
PostableRuleDTO,
|
||||||
PromAlertingRuleState,
|
PromAlertingRuleState,
|
||||||
PromRuleType,
|
PromRuleType,
|
||||||
@ -31,11 +30,13 @@ import {
|
|||||||
RulerGrafanaRuleDTO,
|
RulerGrafanaRuleDTO,
|
||||||
RulerRecordingRuleDTO,
|
RulerRecordingRuleDTO,
|
||||||
RulerRuleDTO,
|
RulerRuleDTO,
|
||||||
|
mapStateWithReasonToBaseState,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
|
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
|
||||||
import { State } from '../components/StateTag';
|
import { State } from '../components/StateTag';
|
||||||
import { RuleHealth } from '../search/rulesSearchParser';
|
import { RuleHealth } from '../search/rulesSearchParser';
|
||||||
|
import { RuleFormType } from '../types/rule-form';
|
||||||
|
|
||||||
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
||||||
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
|
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
|
||||||
@ -59,10 +60,29 @@ export function isRecordingRulerRule(rule?: RulerRuleDTO): rule is RulerRecordin
|
|||||||
return typeof rule === 'object' && 'record' in rule;
|
return typeof rule === 'object' && 'record' in rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGrafanaOrDataSourceRecordingRule(rule?: RulerRuleDTO) {
|
||||||
|
return (
|
||||||
|
(typeof rule === 'object' && isRecordingRulerRule(rule)) ||
|
||||||
|
(isGrafanaRulerRule(rule) && 'record' in rule.grafana_alert)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGrafanaRecordingRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||||
|
return typeof rule === 'object' && isGrafanaOrDataSourceRecordingRule(rule) && isGrafanaRulerRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGrafanaAlertingRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||||
|
return typeof rule === 'object' && isGrafanaRulerRule(rule) && !isGrafanaOrDataSourceRecordingRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
export function isGrafanaRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerGrafanaRuleDTO {
|
export function isGrafanaRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||||
return typeof rule === 'object' && 'grafana_alert' in rule;
|
return typeof rule === 'object' && 'grafana_alert' in rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGrafanaRecordingRulerRule(rule?: RulerRuleDTO) {
|
||||||
|
return typeof rule === 'object' && 'grafana_alert' in rule && 'record' in rule.grafana_alert;
|
||||||
|
}
|
||||||
|
|
||||||
export function isCloudRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerCloudRuleDTO {
|
export function isCloudRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerCloudRuleDTO {
|
||||||
return typeof rule === 'object' && !isGrafanaRulerRule(rule);
|
return typeof rule === 'object' && !isGrafanaRulerRule(rule);
|
||||||
}
|
}
|
||||||
@ -254,8 +274,8 @@ export function getRuleName(rule: RulerRuleDTO) {
|
|||||||
|
|
||||||
export interface AlertInfo {
|
export interface AlertInfo {
|
||||||
alertName: string;
|
alertName: string;
|
||||||
forDuration: string;
|
forDuration?: string;
|
||||||
evaluationsToFire: number;
|
evaluationsToFire: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAlertInfo = (alert: RulerRuleDTO, currentEvaluation: string): AlertInfo => {
|
export const getAlertInfo = (alert: RulerRuleDTO, currentEvaluation: string): AlertInfo => {
|
||||||
@ -268,7 +288,7 @@ export const getAlertInfo = (alert: RulerRuleDTO, currentEvaluation: string): Al
|
|||||||
return {
|
return {
|
||||||
alertName: alert.grafana_alert.title,
|
alertName: alert.grafana_alert.title,
|
||||||
forDuration: alert.for,
|
forDuration: alert.for,
|
||||||
evaluationsToFire: getNumberEvaluationsToStartAlerting(alert.for, currentEvaluation),
|
evaluationsToFire: alert.for ? getNumberEvaluationsToStartAlerting(alert.for, currentEvaluation) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isAlertingRulerRule(alert)) {
|
if (isAlertingRulerRule(alert)) {
|
||||||
@ -329,3 +349,31 @@ export function getRuleGroupLocationFromRuleWithLocation(rule: RuleWithLocation)
|
|||||||
groupName,
|
groupName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGrafanaAlertingRuleByType(type?: RuleFormType) {
|
||||||
|
return type === RuleFormType.grafana;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGrafanaRecordingRuleByType(type: RuleFormType) {
|
||||||
|
return type === RuleFormType.grafanaRecording;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCloudAlertingRuleByType(type?: RuleFormType) {
|
||||||
|
return type === RuleFormType.cloudAlerting;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCloudRecordingRuleByType(type?: RuleFormType) {
|
||||||
|
return type === RuleFormType.cloudRecording;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGrafanaManagedRuleByType(type: RuleFormType) {
|
||||||
|
return isGrafanaAlertingRuleByType(type) || isGrafanaRecordingRuleByType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRecordingRuleByType(type: RuleFormType) {
|
||||||
|
return isGrafanaRecordingRuleByType(type) || isCloudRecordingRuleByType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDataSourceManagedRuleByType(type: RuleFormType) {
|
||||||
|
return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type);
|
||||||
|
}
|
||||||
|
@ -108,6 +108,6 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
|
|||||||
"refId": "C",
|
"refId": "C",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"type": "grafana",
|
"type": "grafana-alerting",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -222,11 +222,15 @@ export interface PostableGrafanaRuleDefinition {
|
|||||||
uid?: string;
|
uid?: string;
|
||||||
title: string;
|
title: string;
|
||||||
condition: string;
|
condition: string;
|
||||||
no_data_state: GrafanaAlertStateDecision;
|
no_data_state?: GrafanaAlertStateDecision;
|
||||||
exec_err_state: GrafanaAlertStateDecision;
|
exec_err_state?: GrafanaAlertStateDecision;
|
||||||
data: AlertQuery[];
|
data: AlertQuery[];
|
||||||
is_paused?: boolean;
|
is_paused?: boolean;
|
||||||
notification_settings?: GrafanaNotificationSettings;
|
notification_settings?: GrafanaNotificationSettings;
|
||||||
|
record?: {
|
||||||
|
metric: string;
|
||||||
|
from: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -238,7 +242,7 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
|||||||
|
|
||||||
export interface RulerGrafanaRuleDTO<T = GrafanaRuleDefinition> {
|
export interface RulerGrafanaRuleDTO<T = GrafanaRuleDefinition> {
|
||||||
grafana_alert: T;
|
grafana_alert: T;
|
||||||
for: string;
|
for?: string;
|
||||||
annotations: Annotations;
|
annotations: Annotations;
|
||||||
labels: Labels;
|
labels: Labels;
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,12 @@
|
|||||||
"used-by_one": "Used by {{ count }} notification policy",
|
"used-by_one": "Used by {{ count }} notification policy",
|
||||||
"used-by_other": "Used by {{ count }} notification policies"
|
"used-by_other": "Used by {{ count }} notification policies"
|
||||||
},
|
},
|
||||||
|
"grafana-rules": {
|
||||||
|
"export-rules": "Export rules",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"new-recording-rule": "New recording rule",
|
||||||
|
"title": "Grafana"
|
||||||
|
},
|
||||||
"policies": {
|
"policies": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"timingOptions": {
|
"timingOptions": {
|
||||||
@ -144,6 +150,12 @@
|
|||||||
"success": "Successfully updated rule group"
|
"success": "Successfully updated rule group"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rule-state": {
|
||||||
|
"creating": "Creating",
|
||||||
|
"deleting": "Deleting",
|
||||||
|
"paused": "Paused",
|
||||||
|
"recording-rule": "Recording rule"
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"delete-rule": {
|
"delete-rule": {
|
||||||
"success": "Rule successfully deleted"
|
"success": "Rule successfully deleted"
|
||||||
|
@ -112,6 +112,12 @@
|
|||||||
"used-by_one": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčy",
|
"used-by_one": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčy",
|
||||||
"used-by_other": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčįęş"
|
"used-by_other": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčįęş"
|
||||||
},
|
},
|
||||||
|
"grafana-rules": {
|
||||||
|
"export-rules": "Ēχpőřŧ řūľęş",
|
||||||
|
"loading": "Ŀőäđįʼnģ...",
|
||||||
|
"new-recording-rule": "Ńęŵ řęčőřđįʼnģ řūľę",
|
||||||
|
"title": "Ğřäƒäʼnä"
|
||||||
|
},
|
||||||
"policies": {
|
"policies": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"timingOptions": {
|
"timingOptions": {
|
||||||
@ -144,6 +150,12 @@
|
|||||||
"success": "Ŝūččęşşƒūľľy ūpđäŧęđ řūľę ģřőūp"
|
"success": "Ŝūččęşşƒūľľy ūpđäŧęđ řūľę ģřőūp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rule-state": {
|
||||||
|
"creating": "Cřęäŧįʼnģ",
|
||||||
|
"deleting": "Đęľęŧįʼnģ",
|
||||||
|
"paused": "Päūşęđ",
|
||||||
|
"recording-rule": "Ŗęčőřđįʼnģ řūľę"
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"delete-rule": {
|
"delete-rule": {
|
||||||
"success": "Ŗūľę şūččęşşƒūľľy đęľęŧęđ"
|
"success": "Ŗūľę şūččęşşƒūľľy đęľęŧęđ"
|
||||||
|
Loading…
Reference in New Issue
Block a user