mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Enable editing evaluation interval in alert form when creating a new group (#60083)
* Enable editing evaluation interval in alert form when creating a new group * Disable group when no folder selected and focus on group when clicking add new * Improve group selector showing interval as a description for each option * Fix evaluate every input and label aligment * Fix columns width in EditRuleGroupModal rules table * Reduce amount of space in the evaluation behaviour section
This commit is contained in:
parent
79ffb699ee
commit
171cd60480
@ -11297,6 +11297,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"BacktestConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"annotations": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/AlertQuery"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"for": {
|
||||||
|
"$ref": "#/definitions/Duration"
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"$ref": "#/definitions/Duration"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"no_data_state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Alerting",
|
||||||
|
"NoData",
|
||||||
|
"OK"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BacktestResult": {
|
||||||
|
"$ref": "#/definitions/Frame"
|
||||||
|
},
|
||||||
"BasicAuth": {
|
"BasicAuth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "BasicAuth contains basic HTTP authentication credentials.",
|
"title": "BasicAuth contains basic HTTP authentication credentials.",
|
||||||
|
@ -128,7 +128,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
await clickSelectOption(folderInput, 'Folder A');
|
await clickSelectOption(folderInput, 'Folder A');
|
||||||
const groupInput = await ui.inputs.group.find();
|
const groupInput = await ui.inputs.group.find();
|
||||||
await userEvent.click(byRole('combobox').get(groupInput));
|
await userEvent.click(byRole('combobox').get(groupInput));
|
||||||
await clickSelectOption(groupInput, 'group1 (1m)');
|
await clickSelectOption(groupInput, 'group1');
|
||||||
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
|
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
|
||||||
|
|
||||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { FC, useMemo, useState } from 'react';
|
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||||
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
|
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@ -180,6 +180,8 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const evaluateEveryInForm = watch('evaluateEvery');
|
||||||
|
useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
|
@ -40,8 +40,16 @@ const useGetGroups = (groupfoldersForGrafana: RulerRulesConfigDTO | null | undef
|
|||||||
return groupOptions;
|
return groupOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapGroupsToOptions(groups: string[]): Array<SelectableValue<string>> {
|
function mapGroupsToOptions(
|
||||||
return groups.map((group) => ({ label: group, value: group }));
|
groupsForFolder: RulerRulesConfigDTO | null | undefined,
|
||||||
|
groups: string[],
|
||||||
|
folderTitle: string
|
||||||
|
): Array<SelectableValue<string>> {
|
||||||
|
return groups.map((group) => ({
|
||||||
|
label: group,
|
||||||
|
value: group,
|
||||||
|
description: `${getIntervalForGroup(groupsForFolder, group, folderTitle)}`,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
interface FolderAndGroupProps {
|
interface FolderAndGroupProps {
|
||||||
initialFolder: RuleForm | null;
|
initialFolder: RuleForm | null;
|
||||||
@ -52,11 +60,14 @@ export const useGetGroupOptionsFromFolder = (folderTitle: string) => {
|
|||||||
|
|
||||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
||||||
|
|
||||||
const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions(
|
|
||||||
useGetGroups(groupfoldersForGrafana?.result, folderTitle)
|
|
||||||
);
|
|
||||||
const groupsForFolder = groupfoldersForGrafana?.result;
|
const groupsForFolder = groupfoldersForGrafana?.result;
|
||||||
return { groupOptions, groupsForFolder, loading: groupfoldersForGrafana?.loading };
|
|
||||||
|
const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions(
|
||||||
|
groupsForFolder,
|
||||||
|
useGetGroups(groupfoldersForGrafana?.result, folderTitle),
|
||||||
|
folderTitle
|
||||||
|
);
|
||||||
|
return { groupOptions, loading: groupfoldersForGrafana?.loading };
|
||||||
};
|
};
|
||||||
|
|
||||||
const useRuleFolderFilter = (existingRuleForm: RuleForm | null) => {
|
const useRuleFolderFilter = (existingRuleForm: RuleForm | null) => {
|
||||||
@ -93,6 +104,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
|
setValue,
|
||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -105,7 +117,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
const [selectedGroup, setSelectedGroup] = useState(group);
|
const [selectedGroup, setSelectedGroup] = useState(group);
|
||||||
const initialRender = useRef(true);
|
const initialRender = useRef(true);
|
||||||
|
|
||||||
const { groupOptions, groupsForFolder, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
||||||
|
|
||||||
useEffect(() => setSelectedGroup(group), [group, setSelectedGroup]);
|
useEffect(() => setSelectedGroup(group), [group, setSelectedGroup]);
|
||||||
|
|
||||||
@ -120,6 +132,10 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
initialRender.current = false;
|
initialRender.current = false;
|
||||||
}, [group, folder?.title]);
|
}, [group, folder?.title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue('group', selectedGroup);
|
||||||
|
}, [selectedGroup, setValue]);
|
||||||
|
|
||||||
const groupIsInGroupOptions = useCallback(
|
const groupIsInGroupOptions = useCallback(
|
||||||
(group_: string) => {
|
(group_: string) => {
|
||||||
return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_);
|
return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_);
|
||||||
@ -189,16 +205,15 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
<LoadingPlaceholder text="Loading..." />
|
<LoadingPlaceholder text="Loading..." />
|
||||||
) : (
|
) : (
|
||||||
<SelectWithAdd
|
<SelectWithAdd
|
||||||
|
disabled={!folder}
|
||||||
key={`my_unique_select_key__${folder?.title ?? ''}`}
|
key={`my_unique_select_key__${folder?.title ?? ''}`}
|
||||||
{...field}
|
{...field}
|
||||||
options={groupOptions}
|
options={groupOptions}
|
||||||
getOptionLabel={(option: SelectableValue<string>) =>
|
getOptionLabel={(option: SelectableValue<string>) => `${option.label}`}
|
||||||
`${option.label} (${getIntervalForGroup(groupsForFolder, option.label ?? '', folder?.title ?? '')})`
|
|
||||||
}
|
|
||||||
value={selectedGroup}
|
value={selectedGroup}
|
||||||
custom={isAddingGroup}
|
custom={isAddingGroup}
|
||||||
onCustomChange={(custom: boolean) => setIsAddingGroup(custom)}
|
onCustomChange={(custom: boolean) => setIsAddingGroup(custom)}
|
||||||
placeholder="Evaluation group name"
|
placeholder={isAddingGroup ? 'New evaluation group name' : 'Evaluation group name'}
|
||||||
onChange={(value: string) => {
|
onChange={(value: string) => {
|
||||||
field.onChange(value);
|
field.onChange(value);
|
||||||
setSelectedGroup(value);
|
setSelectedGroup(value);
|
||||||
|
@ -4,7 +4,7 @@ import { RegisterOptions, useFormContext } from 'react-hook-form';
|
|||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Button, Card, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
|
import { Button, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
|
||||||
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { logInfo, LogMessages } from '../../Analytics';
|
import { logInfo, LogMessages } from '../../Analytics';
|
||||||
@ -13,7 +13,7 @@ import { RuleForm, RuleFormValues } from '../../types/rule-form';
|
|||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { parsePrometheusDuration } from '../../utils/time';
|
import { parsePrometheusDuration } from '../../utils/time';
|
||||||
import { CollapseToggle } from '../CollapseToggle';
|
import { CollapseToggle } from '../CollapseToggle';
|
||||||
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
|
import { EditCloudGroupModal, evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
|
||||||
|
|
||||||
import { MINUTE } from './AlertRuleForm';
|
import { MINUTE } from './AlertRuleForm';
|
||||||
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
||||||
@ -79,6 +79,49 @@ const useIsNewGroup = (folder: string, group: string) => {
|
|||||||
return !groupIsInGroupOptions(group);
|
return !groupIsInGroupOptions(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EvaluateEveryNewGroup = ({ rules }: { rules: RulerRulesConfigDTO | null | undefined }) => {
|
||||||
|
const {
|
||||||
|
watch,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<RuleFormValues>();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const evaluateEveryId = 'eval-every-input';
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
label="Evaluation interval"
|
||||||
|
description="Applies to every rule within a group. It can overwrite the interval of an existing alert rule."
|
||||||
|
>
|
||||||
|
<div className={styles.alignInterval}>
|
||||||
|
<Stack direction="row" justify-content="left" align-items="baseline" gap={0}>
|
||||||
|
<InlineLabel
|
||||||
|
htmlFor={evaluateEveryId}
|
||||||
|
width={16}
|
||||||
|
tooltip="How often the alert will be evaluated to see if it fires"
|
||||||
|
>
|
||||||
|
Evaluate every
|
||||||
|
</InlineLabel>
|
||||||
|
<Field
|
||||||
|
className={styles.inlineField}
|
||||||
|
error={errors.evaluateEvery?.message}
|
||||||
|
invalid={!!errors.evaluateEvery}
|
||||||
|
validationMessageHorizontalOverflow={true}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id={evaluateEveryId}
|
||||||
|
width={8}
|
||||||
|
{...register(
|
||||||
|
'evaluateEvery',
|
||||||
|
evaluateEveryValidationOptions(rules, watch('group'), watch('folder.title'))
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function FolderGroupAndEvaluationInterval({
|
function FolderGroupAndEvaluationInterval({
|
||||||
initialFolder,
|
initialFolder,
|
||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
@ -89,7 +132,7 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
setEvaluateEvery: (value: string) => void;
|
setEvaluateEvery: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { watch } = useFormContext<RuleFormValues>();
|
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||||
|
|
||||||
const group = watch('group');
|
const group = watch('group');
|
||||||
@ -101,10 +144,15 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
const isNewGroup = useIsNewGroup(folder?.title ?? '', group);
|
const isNewGroup = useIsNewGroup(folder?.title ?? '', group);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
group &&
|
if (!isNewGroup) {
|
||||||
folder &&
|
group &&
|
||||||
setEvaluateEvery(getIntervalForGroup(groupfoldersForGrafana?.result, group, folder?.title ?? ''));
|
folder &&
|
||||||
}, [group, folder, groupfoldersForGrafana?.result, setEvaluateEvery]);
|
setEvaluateEvery(getIntervalForGroup(groupfoldersForGrafana?.result, group, folder?.title ?? ''));
|
||||||
|
} else {
|
||||||
|
setEvaluateEvery(MINUTE);
|
||||||
|
setValue('evaluateEvery', MINUTE);
|
||||||
|
}
|
||||||
|
}, [group, folder, groupfoldersForGrafana?.result, setEvaluateEvery, isNewGroup, setValue]);
|
||||||
|
|
||||||
const closeEditGroupModal = (saved = false) => {
|
const closeEditGroupModal = (saved = false) => {
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
@ -130,43 +178,42 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{folder && group && (
|
{folder && group && (
|
||||||
<Card className={styles.cardContainer}>
|
<div className={styles.evaluationContainer}>
|
||||||
<Card.Heading>Evaluation behavior</Card.Heading>
|
<Stack direction="column" gap={0}>
|
||||||
<Card.Meta>
|
<div className={styles.marginTop}>
|
||||||
<Stack direction="column">
|
{isNewGroup && group ? (
|
||||||
<div className={styles.evaluateLabel}>
|
<EvaluateEveryNewGroup rules={groupfoldersForGrafana?.result} />
|
||||||
{`Alert rules in the `} <span className={styles.bold}>{group}</span> group are evaluated every{' '}
|
) : (
|
||||||
<span className={styles.bold}>{evaluateEvery}</span>.
|
<Stack direction="column" gap={1}>
|
||||||
</div>
|
<div className={styles.evaluateLabel}>
|
||||||
|
{`Alert rules in the `} <span className={styles.bold}>{group}</span> group are evaluated every{' '}
|
||||||
<br />
|
<span className={styles.bold}>{evaluateEvery}</span>.
|
||||||
{!isNewGroup && (
|
</div>
|
||||||
<div>
|
{!isNewGroup && (
|
||||||
{`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
|
<div>
|
||||||
</div>
|
{`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<br />
|
</div>
|
||||||
</Stack>
|
|
||||||
</Card.Meta>
|
|
||||||
<Card.Actions>
|
|
||||||
<Stack direction="row" justify-content="right" align-items="center">
|
<Stack direction="row" justify-content="right" align-items="center">
|
||||||
{isNewGroup && (
|
{!isNewGroup && (
|
||||||
<div className={styles.warningMessage}>
|
<div className={styles.marginTop}>
|
||||||
{`To edit the evaluation group interval, save the alert rule.`}
|
<Button
|
||||||
|
icon={'edit'}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={editGroupDisabled}
|
||||||
|
onClick={onOpenEditGroupModal}
|
||||||
|
>
|
||||||
|
<span>{'Edit evaluation group'}</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
icon={'edit'}
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={editGroupDisabled}
|
|
||||||
onClick={onOpenEditGroupModal}
|
|
||||||
>
|
|
||||||
<span>{'Edit evaluation group'}</span>
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card.Actions>
|
</Stack>
|
||||||
</Card>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -280,8 +327,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
align-self: left;
|
align-self: left;
|
||||||
margin-right: ${theme.spacing(1)};
|
margin-right: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
cardContainer: css`
|
evaluationContainer: css`
|
||||||
|
background-color: ${theme.colors.background.secondary};
|
||||||
|
padding: ${theme.spacing(2)};
|
||||||
max-width: ${theme.breakpoints.values.sm}px;
|
max-width: ${theme.breakpoints.values.sm}px;
|
||||||
|
font-size: ${theme.typography.size.sm};
|
||||||
`,
|
`,
|
||||||
intervalChangedLabel: css`
|
intervalChangedLabel: css`
|
||||||
margin-bottom: ${theme.spacing(1)};
|
margin-bottom: ${theme.spacing(1)};
|
||||||
@ -297,4 +347,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
bold: css`
|
bold: css`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`,
|
`,
|
||||||
|
alignInterval: css`
|
||||||
|
margin-top: ${theme.spacing(1)};
|
||||||
|
margin-left: -${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
marginTop: css`
|
||||||
|
margin-top: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Input, Select } from '@grafana/ui';
|
import { Input, Select } from '@grafana/ui';
|
||||||
@ -43,6 +43,14 @@ export const SelectWithAdd: FC<Props> = ({
|
|||||||
[options, addLabel]
|
[options, addLabel]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && isCustom) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isCustom]);
|
||||||
|
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
@ -53,6 +61,7 @@ export const SelectWithAdd: FC<Props> = ({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={className}
|
className={className}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
ref={inputRef}
|
||||||
onChange={(e) => onChange(e.currentTarget.value)}
|
onChange={(e) => onChange(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -151,7 +151,7 @@ export const RulesForGroupTable = ({
|
|||||||
renderCell: ({ data: { alertName } }) => {
|
renderCell: ({ data: { alertName } }) => {
|
||||||
return <>{alertName}</>;
|
return <>{alertName}</>;
|
||||||
},
|
},
|
||||||
size: 0.6,
|
size: '330px',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'for',
|
id: 'for',
|
||||||
@ -174,7 +174,7 @@ export const RulesForGroupTable = ({
|
|||||||
return <>{numberEvaluations}</>;
|
return <>{numberEvaluations}</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
size: 0.2,
|
size: 0.6,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [currentInterval]);
|
}, [currentInterval]);
|
||||||
@ -208,6 +208,37 @@ interface FormValues {
|
|||||||
groupInterval: string;
|
groupInterval: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const evaluateEveryValidationOptions = (
|
||||||
|
rules: RulerRulesConfigDTO | null | undefined,
|
||||||
|
groupName: string,
|
||||||
|
nameSpaceName: string
|
||||||
|
): RegisterOptions => ({
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: 'Required.',
|
||||||
|
},
|
||||||
|
validate: (value: string) => {
|
||||||
|
try {
|
||||||
|
const duration = parsePrometheusDuration(value);
|
||||||
|
|
||||||
|
if (duration < MIN_TIME_RANGE_STEP_S * 1000) {
|
||||||
|
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
||||||
|
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||||
|
}
|
||||||
|
if (rulesInSameGroupHaveInvalidFor(rules, groupName, nameSpaceName, value).length === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return error instanceof Error ? error.message : 'Failed to parse duration';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
nameSpaceAndGroup: { namespace, group },
|
nameSpaceAndGroup: { namespace, group },
|
||||||
@ -285,35 +316,6 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||||
const groupfoldersForSource = rulerRuleRequests[sourceName];
|
const groupfoldersForSource = rulerRuleRequests[sourceName];
|
||||||
|
|
||||||
const evaluateEveryValidationOptions: RegisterOptions = {
|
|
||||||
required: {
|
|
||||||
value: true,
|
|
||||||
message: 'Required.',
|
|
||||||
},
|
|
||||||
validate: (value: string) => {
|
|
||||||
try {
|
|
||||||
const duration = parsePrometheusDuration(value);
|
|
||||||
|
|
||||||
if (duration < MIN_TIME_RANGE_STEP_S * 1000) {
|
|
||||||
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
|
||||||
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
rulesInSameGroupHaveInvalidFor(groupfoldersForSource.result, groupName, nameSpaceName, value).length === 0
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return error instanceof Error ? error.message : 'Failed to parse duration';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={styles.modal}
|
className={styles.modal}
|
||||||
@ -387,7 +389,10 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
<Input
|
<Input
|
||||||
id="groupInterval"
|
id="groupInterval"
|
||||||
placeholder="1m"
|
placeholder="1m"
|
||||||
{...register('groupInterval', evaluateEveryValidationOptions)}
|
{...register(
|
||||||
|
'groupInterval',
|
||||||
|
evaluateEveryValidationOptions(groupfoldersForSource?.result, groupName, nameSpaceName)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ export interface RuleFormValues {
|
|||||||
noDataState: GrafanaAlertStateDecision;
|
noDataState: GrafanaAlertStateDecision;
|
||||||
execErrState: GrafanaAlertStateDecision;
|
execErrState: GrafanaAlertStateDecision;
|
||||||
folder: RuleForm | null;
|
folder: RuleForm | null;
|
||||||
|
evaluateEvery: string;
|
||||||
evaluateFor: string;
|
evaluateFor: string;
|
||||||
|
|
||||||
// cortex / loki rules
|
// cortex / loki rules
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { EvalFunction } from '../../state/alertDef';
|
import { EvalFunction } from '../../state/alertDef';
|
||||||
|
import { MINUTE } from '../components/rule-editor/AlertRuleForm';
|
||||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||||
|
|
||||||
import { getRulesAccess } from './access-control';
|
import { getRulesAccess } from './access-control';
|
||||||
@ -60,6 +61,7 @@ export const getDefaultFormValues = (): RuleFormValues => {
|
|||||||
noDataState: GrafanaAlertStateDecision.NoData,
|
noDataState: GrafanaAlertStateDecision.NoData,
|
||||||
execErrState: GrafanaAlertStateDecision.Error,
|
execErrState: GrafanaAlertStateDecision.Error,
|
||||||
evaluateFor: '5m',
|
evaluateFor: '5m',
|
||||||
|
evaluateEvery: MINUTE,
|
||||||
|
|
||||||
// cortex / loki
|
// cortex / loki
|
||||||
namespace: '',
|
namespace: '',
|
||||||
@ -124,6 +126,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
name: ga.title,
|
name: ga.title,
|
||||||
type: RuleFormType.grafana,
|
type: RuleFormType.grafana,
|
||||||
group: group.name,
|
group: group.name,
|
||||||
|
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||||
evaluateFor: rule.for || '0',
|
evaluateFor: rule.for || '0',
|
||||||
noDataState: ga.no_data_state,
|
noDataState: ga.no_data_state,
|
||||||
execErrState: ga.exec_err_state,
|
execErrState: ga.exec_err_state,
|
||||||
|
Loading…
Reference in New Issue
Block a user