Alerting: allow any "evaluate for" value >=0 in the alert rule form (#35807)

pull/35995/head
Domas 4 years ago committed by GitHub
parent b05b5d5e3b
commit 781ab833bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/grafana-data/src/datetime/durationutil.ts
  2. 5
      public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx
  3. 62
      public/app/features/alerting/unified/components/rule-editor/CloudConditionsStep.tsx
  4. 178
      public/app/features/alerting/unified/components/rule-editor/ConditionsStep.tsx
  5. 32
      public/app/features/alerting/unified/components/rule-editor/GrafanaConditionEvalWarning.tsx
  6. 142
      public/app/features/alerting/unified/components/rule-editor/GrafanaConditionsStep.tsx
  7. 2
      public/app/features/alerting/unified/utils/rule-form.ts
  8. 13
      public/app/features/alerting/unified/utils/time.ts

@ -48,6 +48,11 @@ export function addDurationToDate(date: Date | number, duration: Duration): Date
return add(date, duration);
}
export function durationToMilliseconds(duration: Duration): number {
const now = new Date();
return addDurationToDate(now, duration).getTime() - now.getTime();
}
export function isValidDate(dateString: string) {
return !isNaN(Date.parse(dateString));
}

@ -4,7 +4,6 @@ import { PageToolbar, Button, useStyles2, CustomScrollbar, Spinner } from '@graf
import { css } from '@emotion/css';
import { AlertTypeStep } from './AlertTypeStep';
import { ConditionsStep } from './ConditionsStep';
import { DetailsStep } from './DetailsStep';
import { QueryStep } from './QueryStep';
import { useForm, FormProvider } from 'react-hook-form';
@ -21,6 +20,8 @@ import { Link } from 'react-router-dom';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { appEvents } from 'app/core/core';
import { CloudConditionsStep } from './CloudConditionsStep';
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
type Props = {
existing?: RuleWithLocation;
@ -120,7 +121,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
{showStep2 && (
<>
<QueryStep />
<ConditionsStep />
{type === RuleFormType.cloud ? <CloudConditionsStep /> : <GrafanaConditionsStep />}
<DetailsStep />
</>
)}

@ -0,0 +1,62 @@
import React, { FC } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
import { Field, Input, InputControl, Select, useStyles } from '@grafana/ui';
import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form';
import { timeOptions } from '../../utils/time';
import { RuleEditorSection } from './RuleEditorSection';
import { PreviewRule } from './PreviewRule';
export const CloudConditionsStep: FC = () => {
const styles = useStyles(getStyles);
const {
register,
control,
formState: { errors },
} = useFormContext<RuleFormValues>();
return (
<RuleEditorSection stepNo={3} title="Define alert conditions">
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
<div className={styles.flexRow}>
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
<Input
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a positive integer.' } })}
width={8}
/>
</Field>
<InputControl
name="forTimeUnit"
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
options={timeOptions}
onChange={(value) => onChange(value?.value)}
width={15}
className={styles.timeUnit}
/>
)}
control={control}
/>
</div>
</Field>
<PreviewRule />
</RuleEditorSection>
);
};
const getStyles = (theme: GrafanaTheme) => ({
inlineField: css`
margin-bottom: 0;
`,
flexRow: css`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
`,
timeUnit: css`
margin-left: ${theme.spacing.xs};
`,
});

@ -1,178 +0,0 @@
import React, { FC, useState } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme, parseDuration, addDurationToDate } from '@grafana/data';
import { Field, InlineLabel, Input, InputControl, Select, Switch, useStyles } from '@grafana/ui';
import { useFormContext, RegisterOptions } from 'react-hook-form';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { timeOptions, timeValidationPattern } from '../../utils/time';
import { ConditionField } from './ConditionField';
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
import { RuleEditorSection } from './RuleEditorSection';
import { PreviewRule } from './PreviewRule';
const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
const timeRangeValidationOptions: RegisterOptions = {
required: {
value: true,
message: 'Required.',
},
pattern: timeValidationPattern,
validate: (value: string) => {
const duration = parseDuration(value);
if (Object.keys(duration).length) {
const from = new Date();
const to = addDurationToDate(from, duration);
const diff = to.getTime() - from.getTime();
if (diff < MIN_TIME_RANGE_STEP_S * 1000) {
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
}
if (diff % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
}
}
return true;
},
};
export const ConditionsStep: FC = () => {
const styles = useStyles(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false);
const {
register,
control,
watch,
formState: { errors },
} = useFormContext<RuleFormValues>();
const type = watch('type');
return (
<RuleEditorSection stepNo={3} title="Define alert conditions">
{type === RuleFormType.grafana && (
<>
<ConditionField />
<Field label="Evaluate">
<div className={styles.flexRow}>
<InlineLabel 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?.message}
validationMessageHorizontalOverflow={true}
>
<Input width={8} {...register('evaluateEvery', timeRangeValidationOptions)} />
</Field>
<InlineLabel
width={7}
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
>
for
</InlineLabel>
<Field
className={styles.inlineField}
error={errors.evaluateFor?.message}
invalid={!!errors.evaluateFor?.message}
validationMessageHorizontalOverflow={true}
>
<Input width={8} {...register('evaluateFor', timeRangeValidationOptions)} />
</Field>
</div>
</Field>
<Field label="Configure no data and error handling" horizontal={true} className={styles.switchField}>
<Switch value={showErrorHandling} onChange={() => setShowErrorHandling(!showErrorHandling)} />
</Field>
{showErrorHandling && (
<>
<Field label="Alert state if no data or all values are null">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
width={42}
includeNoData={true}
onChange={(value) => onChange(value?.value)}
/>
)}
name="noDataState"
/>
</Field>
<Field label="Alert state if execution error or timeout">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
width={42}
includeNoData={false}
onChange={(value) => onChange(value?.value)}
/>
)}
name="execErrState"
/>
</Field>
</>
)}
</>
)}
{type === RuleFormType.cloud && (
<>
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
<div className={styles.flexRow}>
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
<Input
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a postive integer.' } })}
width={8}
/>
</Field>
<InputControl
name="forTimeUnit"
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
options={timeOptions}
onChange={(value) => onChange(value?.value)}
width={15}
className={styles.timeUnit}
/>
)}
control={control}
/>
</div>
</Field>
</>
)}
<PreviewRule />
</RuleEditorSection>
);
};
const getStyles = (theme: GrafanaTheme) => ({
inlineField: css`
margin-bottom: 0;
`,
flexRow: css`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
`,
numberInput: css`
width: 200px;
& + & {
margin-left: ${theme.spacing.sm};
}
`,
timeUnit: css`
margin-left: ${theme.spacing.xs};
`,
switchField: css`
display: inline-flex;
flex-direction: row-reverse;
margin-top: ${theme.spacing.md};
& > div:first-child {
margin-left: ${theme.spacing.sm};
}
`,
});

@ -0,0 +1,32 @@
import { durationToMilliseconds, parseDuration } from '@grafana/data';
import { Alert } from '@grafana/ui';
import { isEmpty } from 'lodash';
import React, { FC } from 'react';
import { useFormContext } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form';
// a warning that will be shown if a problematic yet technically valid combination of "evaluate every" and "evaluate for" is enetered
export const GrafanaConditionEvalWarning: FC = () => {
const { watch } = useFormContext<RuleFormValues>();
const evaluateFor = watch('evaluateFor');
const evaluateEvery = watch('evaluateEvery');
if (evaluateFor === '0') {
return null;
}
const durationFor = parseDuration(evaluateFor);
const durationEvery = parseDuration(evaluateEvery);
if (isEmpty(durationFor) || isEmpty(durationEvery)) {
return null;
}
const millisFor = durationToMilliseconds(durationFor);
const millisEvery = durationToMilliseconds(durationEvery);
if (millisFor && millisEvery && millisFor <= millisEvery) {
return (
<Alert severity="warning" title="">
Setting a &quot;for&quot; duration that is less than or equal to the evaluation interval will result in the
evaluation interval being used to calculate when an alert that has stopped receiving data will be closed.
</Alert>
);
}
return null;
};

@ -0,0 +1,142 @@
import React, { FC, useState } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme, parseDuration, durationToMilliseconds } from '@grafana/data';
import { Field, InlineLabel, Input, InputControl, Switch, useStyles } from '@grafana/ui';
import { useFormContext, RegisterOptions } from 'react-hook-form';
import { RuleFormValues } from '../../types/rule-form';
import { positiveDurationValidationPattern, durationValidationPattern } from '../../utils/time';
import { ConditionField } from './ConditionField';
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
import { RuleEditorSection } from './RuleEditorSection';
import { PreviewRule } from './PreviewRule';
import { GrafanaConditionEvalWarning } from './GrafanaConditionEvalWarning';
const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
const forValidationOptions: RegisterOptions = {
required: {
value: true,
message: 'Required.',
},
pattern: durationValidationPattern,
};
const evaluateEveryValidationOptions: RegisterOptions = {
required: {
value: true,
message: 'Required.',
},
pattern: positiveDurationValidationPattern,
validate: (value: string) => {
const duration = parseDuration(value);
if (Object.keys(duration).length) {
const diff = durationToMilliseconds(duration);
if (diff < MIN_TIME_RANGE_STEP_S * 1000) {
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
}
if (diff % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
}
}
return true;
},
};
export const GrafanaConditionsStep: FC = () => {
const styles = useStyles(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false);
const {
register,
formState: { errors },
} = useFormContext<RuleFormValues>();
return (
<RuleEditorSection stepNo={3} title="Define alert conditions">
<ConditionField />
<Field label="Evaluate">
<div className={styles.flexRow}>
<InlineLabel 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?.message}
validationMessageHorizontalOverflow={true}
>
<Input width={8} {...register('evaluateEvery', evaluateEveryValidationOptions)} />
</Field>
<InlineLabel
width={7}
tooltip='Once condition is breached, alert will go into pending state. If it is pending for longer than the "for" value, it will become a firing alert.'
>
for
</InlineLabel>
<Field
className={styles.inlineField}
error={errors.evaluateFor?.message}
invalid={!!errors.evaluateFor?.message}
validationMessageHorizontalOverflow={true}
>
<Input width={8} {...register('evaluateFor', forValidationOptions)} />
</Field>
</div>
</Field>
<GrafanaConditionEvalWarning />
<Field label="Configure no data and error handling" horizontal={true} className={styles.switchField}>
<Switch value={showErrorHandling} onChange={() => setShowErrorHandling(!showErrorHandling)} />
</Field>
{showErrorHandling && (
<>
<Field label="Alert state if no data or all values are null">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
width={42}
includeNoData={true}
onChange={(value) => onChange(value?.value)}
/>
)}
name="noDataState"
/>
</Field>
<Field label="Alert state if execution error or timeout">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
width={42}
includeNoData={false}
onChange={(value) => onChange(value?.value)}
/>
)}
name="execErrState"
/>
</Field>
</>
)}
<PreviewRule />
</RuleEditorSection>
);
};
const getStyles = (theme: GrafanaTheme) => ({
inlineField: css`
margin-bottom: 0;
`,
flexRow: css`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
`,
switchField: css`
display: inline-flex;
flex-direction: row-reverse;
margin-top: ${theme.spacing.md};
& > div:first-child {
margin-left: ${theme.spacing.sm};
}
`,
});

@ -98,7 +98,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
...defaultFormValues,
name: ga.title,
type: RuleFormType.grafana,
evaluateFor: rule.for,
evaluateFor: rule.for || '0',
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
noDataState: ga.no_data_state,
execErrState: ga.exec_err_state,

@ -19,9 +19,18 @@ export const timeOptions = Object.entries(TimeOptions).map(([key, value]) => ({
value: value,
}));
export const timeValidationPattern = {
// 1h, 10m and such
export const positiveDurationValidationPattern = {
value: new RegExp(`^\\d+(${Object.values(TimeOptions).join('|')})$`),
message: `Must be of format "(number)(unit)", for example "1m". Available units: ${Object.values(TimeOptions).join(
message: `Must be of format "(number)(unit)" , for example "1m". Available units: ${Object.values(TimeOptions).join(
', '
)}`,
};
// 1h, 10m or 0 (without units)
export const durationValidationPattern = {
value: new RegExp(`^\\d+(${Object.values(TimeOptions).join('|')})|0$`),
message: `Must be of format "(number)(unit)", for example "1m", or just "0". Available units: ${Object.values(
TimeOptions
).join(', ')}`,
};

Loading…
Cancel
Save