mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Evaluation quick buttons (#85010)
Co-authored-by: Tom Ratcliffe <tomratcliffe@users.noreply.github.com>
This commit is contained in:
parent
b5c33c540c
commit
d3ee3c0a24
@ -142,7 +142,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid,
|
uid,
|
||||||
namespace_uid: 'abcd',
|
namespace_uid: 'abcd',
|
||||||
@ -208,7 +208,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary', custom: 'value' },
|
annotations: { description: 'some description', summary: 'some summary', custom: 'value' },
|
||||||
labels: { severity: 'warn', team: 'the a-team', custom: 'value' },
|
labels: { severity: 'warn', team: 'the a-team', custom: 'value' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid,
|
uid,
|
||||||
condition: 'B',
|
condition: 'B',
|
||||||
|
@ -111,7 +111,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid: '23',
|
uid: '23',
|
||||||
namespace_uid: 'abcd',
|
namespace_uid: 'abcd',
|
||||||
@ -133,7 +133,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid: '23',
|
uid: '23',
|
||||||
namespace_uid: 'b',
|
namespace_uid: 'b',
|
||||||
@ -209,7 +209,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description' },
|
annotations: { description: 'some description' },
|
||||||
labels: { severity: 'warn' },
|
labels: { severity: 'warn' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
condition: 'B',
|
condition: 'B',
|
||||||
data: getDefaultQueries(),
|
data: getDefaultQueries(),
|
||||||
|
@ -706,6 +706,7 @@ describe('RuleList', () => {
|
|||||||
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
|
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
|
||||||
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
|
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
|
||||||
|
|
||||||
|
await userEvent.clear(ui.editGroupModal.intervalInput.get());
|
||||||
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
||||||
|
|
||||||
// submit, check that appropriate calls were made
|
// submit, check that appropriate calls were made
|
||||||
@ -743,6 +744,8 @@ describe('RuleList', () => {
|
|||||||
// make changes to form
|
// make changes to form
|
||||||
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
|
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
|
||||||
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
|
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
|
||||||
|
|
||||||
|
await userEvent.clear(ui.editGroupModal.intervalInput.get());
|
||||||
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
||||||
|
|
||||||
// submit, check that appropriate calls were made
|
// submit, check that appropriate calls were made
|
||||||
@ -773,6 +776,7 @@ describe('RuleList', () => {
|
|||||||
|
|
||||||
testCase('edit lotex group eval interval, no renaming', async () => {
|
testCase('edit lotex group eval interval, no renaming', async () => {
|
||||||
// make changes to form
|
// make changes to form
|
||||||
|
await userEvent.clear(ui.editGroupModal.intervalInput.get());
|
||||||
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
||||||
|
|
||||||
// submit, check that appropriate calls were made
|
// submit, check that appropriate calls were made
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { screen } from '@testing-library/dom';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getEvaluationGroupOptions, EvaluationGroupQuickPick } from './EvaluationGroupQuickPick';
|
||||||
|
|
||||||
|
describe('EvaluationGroupQuickPick', () => {
|
||||||
|
it('should render the correct default preset, set active element and allow selecting another option', async () => {
|
||||||
|
const onSelect = jest.fn();
|
||||||
|
render(<EvaluationGroupQuickPick currentInterval={'10m'} onSelect={onSelect} />);
|
||||||
|
|
||||||
|
const shouldHaveButtons = ['10s', '30s', '1m', '5m', '10m', '15m', '30m', '1h'];
|
||||||
|
const shouldNotHaveButtons = ['0s', '2h'];
|
||||||
|
|
||||||
|
shouldHaveButtons.forEach((name) => {
|
||||||
|
expect(screen.getByRole('option', { name })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldNotHaveButtons.forEach((name) => {
|
||||||
|
expect(screen.queryByRole('option', { name })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByRole('option', { selected: true })).toHaveTextContent('10m');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('option', { name: '30m' }));
|
||||||
|
expect(onSelect).toHaveBeenCalledWith('30m');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEvaluationGroupOptions', () => {
|
||||||
|
it('should return the correct default options', () => {
|
||||||
|
const options = getEvaluationGroupOptions();
|
||||||
|
expect(options).toEqual(['10s', '30s', '1m', '5m', '10m', '15m', '30m', '1h']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct options when minInterval is set within set of defaults', () => {
|
||||||
|
const options = getEvaluationGroupOptions('1m0s');
|
||||||
|
expect(options).toEqual(['1m', '5m', '10m', '15m', '30m', '1h', '2h', '4h']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct options when minInterval is set outside set of defaults', () => {
|
||||||
|
const options = getEvaluationGroupOptions('12h');
|
||||||
|
expect(options).toEqual(['12h', '1d', '1d12h', '2d', '2d12h', '3d', '3d12h', '4d']);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
import { last, times } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Button, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusDuration } from '../../utils/time';
|
||||||
|
|
||||||
|
const MIN_INTERVAl = config.unifiedAlerting.minInterval ?? '10s';
|
||||||
|
export const getEvaluationGroupOptions = (minInterval = MIN_INTERVAl) => {
|
||||||
|
const MIN_OPTIONS_TO_SHOW = 8;
|
||||||
|
const DEFAULT_INTERVAL_OPTIONS: number[] = [
|
||||||
|
parsePrometheusDuration('10s'),
|
||||||
|
parsePrometheusDuration('30s'),
|
||||||
|
parsePrometheusDuration('1m'),
|
||||||
|
parsePrometheusDuration('5m'),
|
||||||
|
parsePrometheusDuration('10m'),
|
||||||
|
parsePrometheusDuration('15m'),
|
||||||
|
parsePrometheusDuration('30m'),
|
||||||
|
parsePrometheusDuration('1h'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 10s for OSS and 1m0s for Grafana Cloud
|
||||||
|
const minEvaluationIntervalMillis = safeParsePrometheusDuration(minInterval);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. make sure we always show at least 8 options to the user
|
||||||
|
* 2. find the default interval closest to the configured minInterval
|
||||||
|
* 3. if we have fewer than 8 options, we basically double the last interval until we have 8 options
|
||||||
|
*/
|
||||||
|
const head = DEFAULT_INTERVAL_OPTIONS.filter((millis) => minEvaluationIntervalMillis <= millis);
|
||||||
|
|
||||||
|
const tail = times(MIN_OPTIONS_TO_SHOW - head.length, (index: number) => {
|
||||||
|
const lastInterval = last(head) ?? minEvaluationIntervalMillis;
|
||||||
|
const multiplier = head.length === 0 ? 1 : 2; // if the head is empty we start with the min interval and multiply it only once :)
|
||||||
|
return lastInterval * multiplier * (index + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...head, ...tail].map(formatPrometheusDuration);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QUICK_PICK_OPTIONS = getEvaluationGroupOptions(MIN_INTERVAl);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentInterval: string;
|
||||||
|
onSelect: (interval: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow a quick selection of group evaluation intervals, based on the configured "unifiedAlerting.minInterval" value
|
||||||
|
* ie. [1m, 2m, 5m, 10m, 15m] etc.
|
||||||
|
*/
|
||||||
|
export const EvaluationGroupQuickPick = ({ currentInterval, onSelect }: Props) => (
|
||||||
|
<Stack direction="row" gap={0.5} role="listbox">
|
||||||
|
{QUICK_PICK_OPTIONS.map((interval) => {
|
||||||
|
const isActive = currentInterval === interval;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
role="option"
|
||||||
|
aria-selected={isActive}
|
||||||
|
key={interval}
|
||||||
|
variant={isActive ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onSelect(interval)}
|
||||||
|
>
|
||||||
|
{interval}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
);
|
@ -17,11 +17,12 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect
|
|||||||
import { fetchRulerRulesAction } from '../../state/actions';
|
import { fetchRulerRulesAction } from '../../state/actions';
|
||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { RuleFormValues } from '../../types/rule-form';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { MINUTE } from '../../utils/rule-form';
|
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { ProvisioningBadge } from '../Provisioning';
|
import { ProvisioningBadge } from '../Provisioning';
|
||||||
import { evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
|
import { evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
|
||||||
|
|
||||||
|
import { EvaluationGroupQuickPick } from './EvaluationGroupQuickPick';
|
||||||
import { containsSlashes, Folder, RuleFolderPicker } from './RuleFolderPicker';
|
import { containsSlashes, Folder, RuleFolderPicker } from './RuleFolderPicker';
|
||||||
import { checkForPathSeparator } from './util';
|
import { checkForPathSeparator } from './util';
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups
|
|||||||
return {
|
return {
|
||||||
label: group.name,
|
label: group.name,
|
||||||
value: group.name,
|
value: group.name,
|
||||||
description: group.interval ?? MINUTE,
|
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
// we include provisioned folders, but disable the option to select them
|
// we include provisioned folders, but disable the option to select them
|
||||||
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
||||||
isProvisioned: isProvisioned,
|
isProvisioned: isProvisioned,
|
||||||
@ -357,12 +358,17 @@ function EvaluationGroupCreationModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formAPI = useForm({
|
const formAPI = useForm({
|
||||||
defaultValues: { group: '', evaluateEvery: '' },
|
defaultValues: { group: '', evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL },
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
shouldFocusError: true,
|
shouldFocusError: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, handleSubmit, formState, getValues } = formAPI;
|
const { register, handleSubmit, formState, setValue, getValues, watch: watchGroupFormValues } = formAPI;
|
||||||
|
const evaluationInterval = watchGroupFormValues('evaluateEvery');
|
||||||
|
|
||||||
|
const setEvaluationInterval = (interval: string) => {
|
||||||
|
setValue('evaluateEvery', interval, { shouldValidate: true });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -379,7 +385,7 @@ function EvaluationGroupCreationModal({
|
|||||||
<Field
|
<Field
|
||||||
label={<Label htmlFor={'group'}>Evaluation group name</Label>}
|
label={<Label htmlFor={'group'}>Evaluation group name</Label>}
|
||||||
error={formState.errors.group?.message}
|
error={formState.errors.group?.message}
|
||||||
invalid={!!formState.errors.group}
|
invalid={Boolean(formState.errors.group)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
className={styles.formInput}
|
className={styles.formInput}
|
||||||
@ -392,22 +398,24 @@ function EvaluationGroupCreationModal({
|
|||||||
|
|
||||||
<Field
|
<Field
|
||||||
error={formState.errors.evaluateEvery?.message}
|
error={formState.errors.evaluateEvery?.message}
|
||||||
invalid={!!formState.errors.evaluateEvery}
|
invalid={Boolean(formState.errors.evaluateEvery) ? true : undefined}
|
||||||
label={
|
label={
|
||||||
<Label
|
<Label htmlFor={evaluateEveryId} description="How often all rules in the group are evaluated.">
|
||||||
htmlFor={evaluateEveryId}
|
|
||||||
description="How often is the rule evaluated. Applies to every rule within the group."
|
|
||||||
>
|
|
||||||
Evaluation interval
|
Evaluation interval
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Input
|
<Stack direction="column">
|
||||||
className={styles.formInput}
|
<Input
|
||||||
id={evaluateEveryId}
|
className={styles.formInput}
|
||||||
placeholder="e.g. 5m"
|
id={evaluateEveryId}
|
||||||
{...register('evaluateEvery', evaluateEveryValidationOptions(groupRules))}
|
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
|
||||||
/>
|
{...register('evaluateEvery', evaluateEveryValidationOptions(groupRules))}
|
||||||
|
/>
|
||||||
|
<Stack direction="row" alignItems="flex-end">
|
||||||
|
<EvaluationGroupQuickPick currentInterval={evaluationInterval} onSelect={setEvaluationInterval} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</Field>
|
</Field>
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button variant="secondary" type="button" onClick={onCancel}>
|
<Button variant="secondary" type="button" onClick={onCancel}>
|
||||||
|
@ -18,6 +18,7 @@ import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
|
|||||||
import { FolderAndGroup, useFolderGroupOptions } from './FolderAndGroup';
|
import { FolderAndGroup, useFolderGroupOptions } from './FolderAndGroup';
|
||||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||||
import { NeedHelpInfo } from './NeedHelpInfo';
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||||
|
import { PendingPeriodQuickPick } from './PendingPeriodQuickPick';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
|
|
||||||
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
||||||
@ -104,6 +105,11 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
setIsEditingGroup(false);
|
setIsEditingGroup(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// when the group evaluation interval changes, update the pending period to match
|
||||||
|
useEffect(() => {
|
||||||
|
setValue('evaluateFor', evaluateEvery);
|
||||||
|
}, [evaluateEvery, setValue]);
|
||||||
|
|
||||||
const onOpenEditGroupModal = () => setIsEditingGroup(true);
|
const onOpenEditGroupModal = () => setIsEditingGroup(true);
|
||||||
|
|
||||||
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !groupName;
|
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !groupName;
|
||||||
@ -163,9 +169,16 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const evaluateForId = 'eval-for-input';
|
const evaluateForId = 'eval-for-input';
|
||||||
|
const currentPendingPeriod = watch('evaluateFor');
|
||||||
|
|
||||||
|
const setPendingPeriod = (pendingPeriod: string) => {
|
||||||
|
setValue('evaluateFor', pendingPeriod);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" justify-content="flex-start" align-items="flex-start">
|
<Stack direction="row" justify-content="flex-start" align-items="flex-start">
|
||||||
@ -180,10 +193,17 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
|||||||
}
|
}
|
||||||
className={styles.inlineField}
|
className={styles.inlineField}
|
||||||
error={errors.evaluateFor?.message}
|
error={errors.evaluateFor?.message}
|
||||||
invalid={!!errors.evaluateFor?.message}
|
invalid={Boolean(errors.evaluateFor?.message) ? true : undefined}
|
||||||
validationMessageHorizontalOverflow={true}
|
validationMessageHorizontalOverflow={true}
|
||||||
>
|
>
|
||||||
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
|
<Stack direction="row" alignItems="center">
|
||||||
|
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
|
||||||
|
<PendingPeriodQuickPick
|
||||||
|
selectedPendingPeriod={currentPendingPeriod}
|
||||||
|
groupEvaluationInterval={evaluateEvery}
|
||||||
|
onSelect={setPendingPeriod}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
</Field>
|
</Field>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import { screen } from '@testing-library/dom';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { PendingPeriodQuickPick } from './PendingPeriodQuickPick';
|
||||||
|
|
||||||
|
describe('PendingPeriodQuickPick', () => {
|
||||||
|
it('should render the correct default preset, set active element and allow selecting other options', async () => {
|
||||||
|
const onSelect = jest.fn();
|
||||||
|
render(<PendingPeriodQuickPick onSelect={onSelect} groupEvaluationInterval={'1m'} selectedPendingPeriod={'2m'} />);
|
||||||
|
|
||||||
|
const shouldHaveButtons = ['None', '1m', '2m', '3m', '4m', '5m'];
|
||||||
|
const shouldNotHaveButtons = ['0s', '10s', '6m'];
|
||||||
|
|
||||||
|
shouldHaveButtons.forEach((name) => {
|
||||||
|
expect(screen.getByRole('option', { name })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldNotHaveButtons.forEach((name) => {
|
||||||
|
expect(screen.queryByRole('option', { name })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByRole('option', { selected: true })).toHaveTextContent('2m');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('option', { name: '3m' }));
|
||||||
|
expect(onSelect).toHaveBeenCalledWith('3m');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('option', { name: 'None' }));
|
||||||
|
expect(onSelect).toHaveBeenCalledWith('0s');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { formatPrometheusDuration, safeParsePrometheusDuration } from '../../utils/time';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedPendingPeriod: string;
|
||||||
|
groupEvaluationInterval: string;
|
||||||
|
onSelect: (interval: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPendingPeriodQuickOptions(groupEvaluationInterval: string): string[] {
|
||||||
|
const groupEvaluationIntervalMillis = safeParsePrometheusDuration(groupEvaluationInterval);
|
||||||
|
|
||||||
|
// we generate the quick selection based on the group's evaluation interval
|
||||||
|
const options: number[] = [
|
||||||
|
0,
|
||||||
|
groupEvaluationIntervalMillis * 1,
|
||||||
|
groupEvaluationIntervalMillis * 2,
|
||||||
|
groupEvaluationIntervalMillis * 3,
|
||||||
|
groupEvaluationIntervalMillis * 4,
|
||||||
|
groupEvaluationIntervalMillis * 5,
|
||||||
|
];
|
||||||
|
|
||||||
|
return options.map(formatPrometheusDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PendingPeriodQuickPick({ selectedPendingPeriod, groupEvaluationInterval, onSelect }: Props) {
|
||||||
|
const isQuickSelectionActive = (duration: string) => selectedPendingPeriod === duration;
|
||||||
|
|
||||||
|
const options = getPendingPeriodQuickOptions(groupEvaluationInterval);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row" gap={0.5} role="listbox">
|
||||||
|
{options.map((duration) => (
|
||||||
|
<Button
|
||||||
|
role="option"
|
||||||
|
aria-selected={isQuickSelectionActive(duration)}
|
||||||
|
key={duration}
|
||||||
|
variant={isQuickSelectionActive(duration) ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(duration);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{duration === '0s' ? 'None' : duration}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -29,7 +29,7 @@ import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
|||||||
import { initialAsyncRequestState } from '../../../utils/redux';
|
import { initialAsyncRequestState } from '../../../utils/redux';
|
||||||
import {
|
import {
|
||||||
MANUAL_ROUTING_KEY,
|
MANUAL_ROUTING_KEY,
|
||||||
MINUTE,
|
DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
formValuesFromExistingRule,
|
formValuesFromExistingRule,
|
||||||
getDefaultFormValues,
|
getDefaultFormValues,
|
||||||
getDefaultQueries,
|
getDefaultQueries,
|
||||||
@ -59,7 +59,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
const notifyApp = useAppNotification();
|
const notifyApp = useAppNotification();
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
const [showEditYaml, setShowEditYaml] = useState(false);
|
const [showEditYaml, setShowEditYaml] = useState(false);
|
||||||
const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? MINUTE);
|
const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
|
||||||
|
|
||||||
const routeParams = useParams<{ type: string; id: string }>();
|
const routeParams = useParams<{ type: string; id: string }>();
|
||||||
const ruleType = translateRouteParamToRuleType(routeParams.type);
|
const ruleType = translateRouteParamToRuleType(routeParams.type);
|
||||||
@ -319,7 +319,7 @@ function formValuesFromQueryParams(ruleDefinition: string, type: RuleFormType):
|
|||||||
annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []),
|
annotations: normalizeDefaultAnnotations(ruleFromQueryParams.annotations ?? []),
|
||||||
queries: ruleFromQueryParams.queries ?? getDefaultQueries(),
|
queries: ruleFromQueryParams.queries ?? getDefaultQueries(),
|
||||||
type: type || RuleFormType.grafana,
|
type: type || RuleFormType.grafana,
|
||||||
evaluateEvery: MINUTE,
|
evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { fetchRulerRulesGroup } from '../../../api/ruler';
|
|||||||
import { useDataSourceFeatures } from '../../../hooks/useCombinedRule';
|
import { useDataSourceFeatures } from '../../../hooks/useCombinedRule';
|
||||||
import { RuleFormValues } from '../../../types/rule-form';
|
import { RuleFormValues } from '../../../types/rule-form';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||||
import { formValuesToRulerGrafanaRuleDTO, MINUTE } from '../../../utils/rule-form';
|
import { DEFAULT_GROUP_EVALUATION_INTERVAL, formValuesToRulerGrafanaRuleDTO } from '../../../utils/rule-form';
|
||||||
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';
|
||||||
@ -44,7 +44,7 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
|
|||||||
const [exportData, setExportData] = useState<RuleFormValues | undefined>(undefined);
|
const [exportData, setExportData] = useState<RuleFormValues | undefined>(undefined);
|
||||||
|
|
||||||
const [conditionErrorMsg, setConditionErrorMsg] = useState('');
|
const [conditionErrorMsg, setConditionErrorMsg] = useState('');
|
||||||
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? MINUTE);
|
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
|
||||||
|
|
||||||
const onInvalid = (): void => {
|
const onInvalid = (): void => {
|
||||||
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
||||||
|
@ -61,7 +61,7 @@ const expectedModifiedRule2 = (uid: string) => ({
|
|||||||
annotations: {
|
annotations: {
|
||||||
summary: 'This grafana rule2 updated',
|
summary: 'This grafana rule2 updated',
|
||||||
},
|
},
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
condition: 'A',
|
condition: 'A',
|
||||||
data: [
|
data: [
|
||||||
|
@ -176,7 +176,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid: '23',
|
uid: '23',
|
||||||
namespace_uid: 'abcd',
|
namespace_uid: 'abcd',
|
||||||
@ -198,7 +198,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid: '23',
|
uid: '23',
|
||||||
namespace_uid: 'b',
|
namespace_uid: 'b',
|
||||||
@ -302,7 +302,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid: '23',
|
uid: '23',
|
||||||
namespace_uid: 'abcd',
|
namespace_uid: 'abcd',
|
||||||
@ -324,7 +324,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary' },
|
annotations: { description: 'some description', summary: 'some summary' },
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
labels: { severity: 'warn', team: 'the a-team' },
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
uid: '23',
|
uid: '23',
|
||||||
namespace_uid: 'b',
|
namespace_uid: 'b',
|
||||||
@ -396,7 +396,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
{
|
{
|
||||||
annotations: {},
|
annotations: {},
|
||||||
labels: {},
|
labels: {},
|
||||||
for: '5m',
|
for: '1m',
|
||||||
grafana_alert: {
|
grafana_alert: {
|
||||||
condition: 'B',
|
condition: 'B',
|
||||||
data: getDefaultQueries(),
|
data: getDefaultQueries(),
|
||||||
|
@ -16,11 +16,13 @@ import { rulesInSameGroupHaveInvalidFor, updateLotexNamespaceAndGroupAction } fr
|
|||||||
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
||||||
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { initialAsyncRequestState } from '../../utils/redux';
|
import { initialAsyncRequestState } from '../../utils/redux';
|
||||||
|
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
||||||
import { AlertInfo, getAlertInfo, isRecordingRulerRule } from '../../utils/rules';
|
import { AlertInfo, getAlertInfo, isRecordingRulerRule } from '../../utils/rules';
|
||||||
import { parsePrometheusDuration, safeParseDurationstr } 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';
|
||||||
import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util';
|
import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util';
|
||||||
|
import { EvaluationGroupQuickPick } from '../rule-editor/EvaluationGroupQuickPick';
|
||||||
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
|
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 10;
|
const ITEMS_PER_PAGE = 10;
|
||||||
@ -68,7 +70,8 @@ export const RulesForGroupTable = ({ rulesWithoutRecordingRules }: { rulesWithou
|
|||||||
data: getAlertInfo(rule, currentInterval),
|
data: getAlertInfo(rule, currentInterval),
|
||||||
}))
|
}))
|
||||||
.sort(
|
.sort(
|
||||||
(alert1, alert2) => safeParseDurationstr(alert1.data.forDuration) - safeParseDurationstr(alert2.data.forDuration)
|
(alert1, alert2) =>
|
||||||
|
safeParsePrometheusDuration(alert1.data.forDuration) - safeParsePrometheusDuration(alert2.data.forDuration)
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: AlertsWithForTableColumnProps[] = useMemo(() => {
|
const columns: AlertsWithForTableColumnProps[] = useMemo(() => {
|
||||||
@ -145,7 +148,12 @@ export const evaluateEveryValidationOptions = (rules: RulerRuleDTO[]): RegisterO
|
|||||||
if (rulesInSameGroupHaveInvalidFor(rules, evaluateEvery).length === 0) {
|
if (rulesInSameGroupHaveInvalidFor(rules, evaluateEvery).length === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
|
const rulePendingPeriods = rules.map((rule) => {
|
||||||
|
const { forDuration } = getAlertInfo(rule, evaluateEvery);
|
||||||
|
return safeParsePrometheusDuration(forDuration);
|
||||||
|
});
|
||||||
|
const largestPendingPeriod = Math.min(...rulePendingPeriods);
|
||||||
|
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) {
|
||||||
return error instanceof Error ? error.message : 'Failed to parse duration';
|
return error instanceof Error ? error.message : 'Failed to parse duration';
|
||||||
@ -176,9 +184,9 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
(): FormValues => ({
|
(): FormValues => ({
|
||||||
namespaceName: decodeGrafanaNamespace(namespace).name,
|
namespaceName: decodeGrafanaNamespace(namespace).name,
|
||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
groupInterval: group.interval ?? '',
|
groupInterval: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
}),
|
}),
|
||||||
[namespace, group]
|
[namespace, group.name, group.interval]
|
||||||
);
|
);
|
||||||
|
|
||||||
const rulesSourceName = getRulesSourceName(namespace.rulesSource);
|
const rulesSourceName = getRulesSourceName(namespace.rulesSource);
|
||||||
@ -227,6 +235,8 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
register,
|
register,
|
||||||
watch,
|
watch,
|
||||||
formState: { isDirty, errors },
|
formState: { isDirty, errors },
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
} = formAPI;
|
} = formAPI;
|
||||||
|
|
||||||
const onInvalid = () => {
|
const onInvalid = () => {
|
||||||
@ -260,7 +270,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
{nameSpaceLabel}
|
{nameSpaceLabel}
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
invalid={!!errors.namespaceName}
|
invalid={Boolean(errors.namespaceName) ? true : undefined}
|
||||||
error={errors.namespaceName?.message}
|
error={errors.namespaceName?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@ -305,14 +315,20 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
<Stack gap={0.5}>Evaluation interval</Stack>
|
<Stack gap={0.5}>Evaluation interval</Stack>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
invalid={!!errors.groupInterval}
|
invalid={Boolean(errors.groupInterval) ? true : undefined}
|
||||||
error={errors.groupInterval?.message}
|
error={errors.groupInterval?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Stack direction="column">
|
||||||
id="groupInterval"
|
<Input
|
||||||
placeholder="1m"
|
id="groupInterval"
|
||||||
{...register('groupInterval', evaluateEveryValidationOptions(rulesWithoutRecordingRules))}
|
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
|
||||||
/>
|
{...register('groupInterval', evaluateEveryValidationOptions(rulesWithoutRecordingRules))}
|
||||||
|
/>
|
||||||
|
<EvaluationGroupQuickPick
|
||||||
|
currentInterval={getValues('groupInterval')}
|
||||||
|
onSelect={(value) => setValue('groupInterval', value, { shouldValidate: true })}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && (
|
{checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && (
|
||||||
|
@ -82,7 +82,7 @@ import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../uti
|
|||||||
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, isGrafanaRulerRule, isRulerNotSupportedResponse } from '../utils/rules';
|
||||||
import { safeParseDurationstr } from '../utils/time';
|
import { safeParsePrometheusDuration } from '../utils/time';
|
||||||
|
|
||||||
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
|
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
|
||||||
const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
|
const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
|
||||||
@ -782,8 +782,8 @@ interface UpdateNamespaceAndGroupOptions {
|
|||||||
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 = safeParseDurationstr(forDuration);
|
const forNumber = safeParsePrometheusDuration(forDuration);
|
||||||
const everyNumber = safeParseDurationstr(everyDuration);
|
const everyNumber = safeParsePrometheusDuration(everyDuration);
|
||||||
|
|
||||||
return forNumber !== 0 && forNumber < everyNumber;
|
return forNumber !== 0 && forNumber < everyNumber;
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu
|
|||||||
"runbook_url": "",
|
"runbook_url": "",
|
||||||
"summary": "",
|
"summary": "",
|
||||||
},
|
},
|
||||||
"for": "5m",
|
"for": "1m",
|
||||||
"grafana_alert": {
|
"grafana_alert": {
|
||||||
"condition": "A",
|
"condition": "A",
|
||||||
"data": [],
|
"data": [],
|
||||||
@ -30,7 +30,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range
|
|||||||
"runbook_url": "",
|
"runbook_url": "",
|
||||||
"summary": "",
|
"summary": "",
|
||||||
},
|
},
|
||||||
"for": "5m",
|
"for": "1m",
|
||||||
"grafana_alert": {
|
"grafana_alert": {
|
||||||
"condition": "A",
|
"condition": "A",
|
||||||
"data": [
|
"data": [
|
||||||
|
@ -10,7 +10,7 @@ import { matcherToMatcherField } from './alertmanager';
|
|||||||
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
||||||
import { normalizeMatchers, parseMatcherToArray, quoteWithEscape, unquoteWithUnescape } from './matchers';
|
import { normalizeMatchers, parseMatcherToArray, quoteWithEscape, unquoteWithUnescape } from './matchers';
|
||||||
import { findExistingRoute } from './routeTree';
|
import { findExistingRoute } from './routeTree';
|
||||||
import { isValidPrometheusDuration, safeParseDurationstr } from './time';
|
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
|
||||||
|
|
||||||
const matchersToArrayFieldMatchers = (
|
const matchersToArrayFieldMatchers = (
|
||||||
matchers: Record<string, string> | undefined,
|
matchers: Record<string, string> | undefined,
|
||||||
@ -268,8 +268,8 @@ export const repeatIntervalValidator = (repeatInterval: string, groupInterval =
|
|||||||
return validGroupInterval;
|
return validGroupInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
const repeatDuration = safeParseDurationstr(repeatInterval);
|
const repeatDuration = safeParsePrometheusDuration(repeatInterval);
|
||||||
const groupDuration = safeParseDurationstr(groupInterval);
|
const groupDuration = safeParsePrometheusDuration(groupInterval);
|
||||||
|
|
||||||
const isRepeatLowerThanGroupDuration = groupDuration !== 0 && repeatDuration < groupDuration;
|
const isRepeatLowerThanGroupDuration = groupDuration !== 0 && repeatDuration < groupDuration;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { isValidPrometheusDuration, parsePrometheusDuration } from './time';
|
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
|
||||||
|
|
||||||
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
||||||
return Object.values(config.datasources);
|
return Object.values(config.datasources);
|
||||||
@ -14,13 +14,13 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
|
|||||||
return { globalLimit: 0, exceedsLimit: false };
|
return { globalLimit: 0, exceedsLimit: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluateEveryGlobalLimitMs = parsePrometheusDuration(config.unifiedAlerting.minInterval);
|
const evaluateEveryGlobalLimitMs = safeParsePrometheusDuration(config.unifiedAlerting.minInterval);
|
||||||
|
|
||||||
if (!alertGroupEvaluateEvery || !isValidPrometheusDuration(alertGroupEvaluateEvery)) {
|
if (!alertGroupEvaluateEvery || !isValidPrometheusDuration(alertGroupEvaluateEvery)) {
|
||||||
return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit: false };
|
return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluateEveryMs = parsePrometheusDuration(alertGroupEvaluateEvery);
|
const evaluateEveryMs = safeParsePrometheusDuration(alertGroupEvaluateEvery);
|
||||||
|
|
||||||
const exceedsLimit = evaluateEveryGlobalLimitMs > evaluateEveryMs && evaluateEveryMs > 0;
|
const exceedsLimit = evaluateEveryGlobalLimitMs > evaluateEveryMs && evaluateEveryMs > 0;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { AlertQuery } from 'app/types/unified-alerting-dto';
|
|||||||
|
|
||||||
import { isCloudRulesSource } from './datasource';
|
import { isCloudRulesSource } from './datasource';
|
||||||
import { isGrafanaRulerRule } from './rules';
|
import { isGrafanaRulerRule } from './rules';
|
||||||
import { safeParseDurationstr } from './time';
|
import { safeParsePrometheusDuration } from './time';
|
||||||
|
|
||||||
export function alertRuleToQueries(combinedRule: CombinedRule | undefined | null): AlertQuery[] {
|
export function alertRuleToQueries(combinedRule: CombinedRule | undefined | null): AlertQuery[] {
|
||||||
if (!combinedRule) {
|
if (!combinedRule) {
|
||||||
@ -45,7 +45,7 @@ export function alertRuleToQueries(combinedRule: CombinedRule | undefined | null
|
|||||||
export function widenRelativeTimeRanges(queries: AlertQuery[], pendingPeriod: string, groupInterval?: string) {
|
export function widenRelativeTimeRanges(queries: AlertQuery[], pendingPeriod: string, groupInterval?: string) {
|
||||||
// if pending period is zero that means inherit from group interval, if that is empty then assume 1m
|
// if pending period is zero that means inherit from group interval, if that is empty then assume 1m
|
||||||
const pendingPeriodDurationMillis =
|
const pendingPeriodDurationMillis =
|
||||||
safeParseDurationstr(pendingPeriod) ?? safeParseDurationstr(groupInterval ?? '1m');
|
safeParsePrometheusDuration(pendingPeriod) ?? safeParsePrometheusDuration(groupInterval ?? '1m');
|
||||||
const pendingPeriodDuration = Math.floor(pendingPeriodDurationMillis / 1000);
|
const pendingPeriodDuration = Math.floor(pendingPeriodDurationMillis / 1000);
|
||||||
|
|
||||||
return queries.map((query) =>
|
return queries.map((query) =>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { omit } from 'lodash';
|
import { clamp, omit } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
@ -48,14 +48,21 @@ 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, isGrafanaRulerRule, isRecordingRulerRule } from './rules';
|
||||||
import { parseInterval } from './time';
|
import { formatPrometheusDuration, parseInterval, safeParsePrometheusDuration } from './time';
|
||||||
|
|
||||||
export type PromOrLokiQuery = PromQuery | LokiQuery;
|
export type PromOrLokiQuery = PromQuery | LokiQuery;
|
||||||
|
|
||||||
export const MINUTE = '1m';
|
|
||||||
|
|
||||||
export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting';
|
export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting';
|
||||||
|
|
||||||
|
// even if the min interval is < 1m we should default to 1m, but allow arbitrary values for minInterval > 1m
|
||||||
|
const GROUP_EVALUATION_MIN_INTERVAL_MS = safeParsePrometheusDuration(config.unifiedAlerting?.minInterval ?? '10s');
|
||||||
|
const GROUP_EVALUATION_INTERVAL_LOWER_BOUND = safeParsePrometheusDuration('1m');
|
||||||
|
const GROUP_EVALUATION_INTERVAL_UPPER_BOUND = Infinity;
|
||||||
|
|
||||||
|
export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration(
|
||||||
|
clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND)
|
||||||
|
);
|
||||||
|
|
||||||
export const getDefaultFormValues = (): RuleFormValues => {
|
export const getDefaultFormValues = (): RuleFormValues => {
|
||||||
const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess();
|
const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess();
|
||||||
|
|
||||||
@ -75,8 +82,8 @@ export const getDefaultFormValues = (): RuleFormValues => {
|
|||||||
condition: '',
|
condition: '',
|
||||||
noDataState: GrafanaAlertStateDecision.NoData,
|
noDataState: GrafanaAlertStateDecision.NoData,
|
||||||
execErrState: GrafanaAlertStateDecision.Error,
|
execErrState: GrafanaAlertStateDecision.Error,
|
||||||
evaluateFor: '5m',
|
evaluateFor: DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
evaluateEvery: MINUTE,
|
evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false
|
manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false
|
||||||
contactPoints: {},
|
contactPoints: {},
|
||||||
overrideGrouping: false,
|
overrideGrouping: false,
|
||||||
|
@ -34,7 +34,7 @@ import { RuleHealth } from '../search/rulesSearchParser';
|
|||||||
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
||||||
import { getRulesSourceName } from './datasource';
|
import { getRulesSourceName } from './datasource';
|
||||||
import { AsyncRequestState } from './redux';
|
import { AsyncRequestState } from './redux';
|
||||||
import { safeParseDurationstr } from './time';
|
import { safeParsePrometheusDuration } from './time';
|
||||||
|
|
||||||
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
|
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
|
||||||
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
|
return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
|
||||||
@ -236,8 +236,8 @@ export const getAlertInfo = (alert: RulerRuleDTO, currentEvaluation: string): Al
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getNumberEvaluationsToStartAlerting = (forDuration: string, currentEvaluation: string) => {
|
export const getNumberEvaluationsToStartAlerting = (forDuration: string, currentEvaluation: string) => {
|
||||||
const evalNumberMs = safeParseDurationstr(currentEvaluation);
|
const evalNumberMs = safeParsePrometheusDuration(currentEvaluation);
|
||||||
const forNumber = safeParseDurationstr(forDuration);
|
const forNumber = safeParsePrometheusDuration(forDuration);
|
||||||
if (forNumber === 0 && evalNumberMs !== 0) {
|
if (forNumber === 0 && evalNumberMs !== 0) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { isValidPrometheusDuration } from './time';
|
import { formatPrometheusDuration, isValidPrometheusDuration, parsePrometheusDuration } from './time';
|
||||||
|
|
||||||
describe('isValidPrometheusDuration', () => {
|
describe('isValidPrometheusDuration', () => {
|
||||||
const validDurations = ['20h30m10s45ms', '1m30s', '20s4h', '90s', '10s', '20h20h', '2d4h20m'];
|
const validDurations = ['20h30m10s45ms', '1m30s', '20s4h', '90s', '10s', '20h20h', '2d4h20m'];
|
||||||
@ -13,3 +13,44 @@ describe('isValidPrometheusDuration', () => {
|
|||||||
expect(isValidPrometheusDuration(duration)).toBe(false);
|
expect(isValidPrometheusDuration(duration)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parsePrometheusDuration', () => {
|
||||||
|
const tests: Array<[string, number]> = [
|
||||||
|
['1ms', 1],
|
||||||
|
['1s', 1000],
|
||||||
|
['1m', 1000 * 60],
|
||||||
|
['1h', 1000 * 60 * 60],
|
||||||
|
['1d', 1000 * 60 * 60 * 24],
|
||||||
|
['1w', 1000 * 60 * 60 * 24 * 7],
|
||||||
|
['1y', 1000 * 60 * 60 * 24 * 365],
|
||||||
|
['1d10h17m36s789ms', 123456789],
|
||||||
|
['1w4d10h20m54s321ms', 987654321],
|
||||||
|
['1y1w1d1h1m1s1ms', 32230861001],
|
||||||
|
];
|
||||||
|
test.each(tests)('.parsePrometheusDuration(%s)', (input, expected) => {
|
||||||
|
expect(parsePrometheusDuration(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatPrometheusDuration', () => {
|
||||||
|
it('should return "0s" for 0 milliseconds', () => {
|
||||||
|
const result = formatPrometheusDuration(0);
|
||||||
|
expect(result).toBe('0s');
|
||||||
|
});
|
||||||
|
|
||||||
|
const tests: Array<[number, string]> = [
|
||||||
|
[0, '0s'],
|
||||||
|
[1000, '1s'],
|
||||||
|
[60000, '1m'],
|
||||||
|
[3600000, '1h'],
|
||||||
|
[86400000, '1d'],
|
||||||
|
[604800000, '1w'],
|
||||||
|
[31536000000, '1y'],
|
||||||
|
[123456789, '1d10h17m36s789ms'],
|
||||||
|
[987654321, '1w4d10h20m54s321ms'],
|
||||||
|
[32230861001, '1y1w1d1h1m1s1ms'],
|
||||||
|
];
|
||||||
|
test.each(tests)('.formatPrometheusDuration(%s)', (input, expected) => {
|
||||||
|
expect(formatPrometheusDuration(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -95,7 +95,46 @@ export function parsePrometheusDuration(duration: string): number {
|
|||||||
return totalDuration;
|
return totalDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const safeParseDurationstr = (duration: string): number => {
|
/**
|
||||||
|
* Formats the given duration in milliseconds into a human-readable string representation.
|
||||||
|
*
|
||||||
|
* @param milliseconds - The duration in milliseconds.
|
||||||
|
* @returns The formatted duration string.
|
||||||
|
*/
|
||||||
|
export function formatPrometheusDuration(milliseconds: number): string {
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const weeks = Math.floor(days / 7);
|
||||||
|
const years = Math.floor(days / 365);
|
||||||
|
|
||||||
|
// we'll make an exception here for 0, 0ms seems a bit weird
|
||||||
|
if (milliseconds === 0) {
|
||||||
|
return '0s';
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeUnits: Array<[number, string]> = [
|
||||||
|
[years, 'y'],
|
||||||
|
[weeks % 52, 'w'],
|
||||||
|
[(days % 365) - 7 * (weeks % 52), 'd'],
|
||||||
|
[hours % 24, 'h'],
|
||||||
|
[minutes % 60, 'm'],
|
||||||
|
[seconds % 60, 's'],
|
||||||
|
[milliseconds % 1000, 'ms'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
timeUnits
|
||||||
|
// remove all 0 values
|
||||||
|
.filter(([time]) => time > 0)
|
||||||
|
// join time and unit
|
||||||
|
.map(([time, unit]) => time + unit)
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const safeParsePrometheusDuration = (duration: string): number => {
|
||||||
try {
|
try {
|
||||||
return parsePrometheusDuration(duration);
|
return parsePrometheusDuration(duration);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Loading…
Reference in New Issue
Block a user