mirror of https://github.com/grafana/grafana
Alerting: allow any "evaluate for" value >=0 in the alert rule form (#35807)
parent
b05b5d5e3b
commit
781ab833bd
@ -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 "for" 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}; |
||||
} |
||||
`,
|
||||
}); |
Loading…
Reference in new issue