Add Condition section to panel

This commit is contained in:
laurenashleigh
2026-01-12 11:41:24 +00:00
parent 59e062d1aa
commit 563e437610
2 changed files with 280 additions and 1 deletions
@@ -1,12 +1,16 @@
import { css } from '@emotion/css';
import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { Drawer } from '@grafana/ui';
import { Drawer, useStyles2 } from '@grafana/ui';
import { RuleDefinitionSection } from 'app/features/alerting/unified/components/RuleDefinitionSection';
import { getDefaultFormValues } from '../rule-editor/formDefaults';
import { RuleFormType, RuleFormValues } from '../types/rule-form';
import { RuleConditionSection } from './RuleConditionSection';
export interface AlertRuleDrawerFormProps {
isOpen: boolean;
onClose: () => void;
@@ -15,6 +19,7 @@ export interface AlertRuleDrawerFormProps {
export function AlertRuleDrawerForm({ isOpen, onClose, title }: AlertRuleDrawerFormProps) {
const methods = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues(RuleFormType.grafana) });
const styles = useStyles2(getStyles);
if (!isOpen) {
return null;
@@ -27,7 +32,20 @@ export function AlertRuleDrawerForm({ isOpen, onClose, title }: AlertRuleDrawerF
>
<FormProvider {...methods}>
<RuleDefinitionSection type={RuleFormType.grafana} />
<div className={styles.divider} aria-hidden="true" />
<RuleConditionSection type={RuleFormType.grafana} />
<div className={styles.divider} aria-hidden="true" />
</FormProvider>
</Drawer>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
divider: css({
borderTop: `1px solid ${theme.colors.border.weak}`,
margin: `${theme.spacing(3)} 0`,
width: '100%',
}),
};
}
@@ -0,0 +1,261 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2, ReducerID, SelectableValue } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import {
Combobox,
ComboboxOption,
Field,
Icon,
InlineField,
InlineFieldRow,
Input,
Select,
Stack,
Text,
useStyles2,
} from '@grafana/ui';
import { EvalFunction } from 'app/features/alerting/state/alertDef';
import { ThresholdSelect } from 'app/features/expressions/components/ThresholdSelect';
import { ToLabel } from 'app/features/expressions/components/ToLabel';
import { reducerTypes, thresholdFunctions } from 'app/features/expressions/types';
import { isRangeEvaluator } from 'app/features/expressions/utils/expressionTypes';
import { RuleFormType, RuleFormValues } from '../types/rule-form';
import { safeParsePrometheusDuration } from '../utils/time';
import { getPendingPeriodQuickOptions } from './rule-editor/DurationQuickPick';
export function RuleConditionSection({ type }: { type: RuleFormType }) {
const base = useStyles2(getStyles);
const { register, watch, setValue } = useFormContext<RuleFormValues>();
const evaluateEvery = watch('evaluateEvery') || '5m';
const evaluateFor = watch('evaluateFor') || '0s';
const pendingOptions = getPendingPeriodQuickOptions(evaluateEvery);
const [customMode, setCustomMode] = useState(false);
const isCustomSelected = customMode;
const customDelay = isCustomSelected ? evaluateFor : '';
const evalMs = safeParsePrometheusDuration(evaluateEvery);
const customMs = safeParsePrometheusDuration(customDelay || '0s');
const isCustomInvalid = isCustomSelected && (evalMs <= 0 || customMs <= 0 || customMs % evalMs !== 0);
let fireSelectLabel: string = evaluateFor;
if (isCustomSelected) {
fireSelectLabel = customDelay || pendingOptions[1] || '1m';
} else if (evaluateFor === '0s') {
fireSelectLabel = t('alerting.duration.immediately', 'immediately');
}
type LocalSimpleCondition = { whenField?: string; evaluator: { params: number[]; type: EvalFunction } };
const [simpleCondition, setSimpleCondition] = useState<LocalSimpleCondition>({
whenField: ReducerID.last,
evaluator: { params: [0], type: EvalFunction.IsAbove },
});
const reducerOptions: Array<ComboboxOption<string>> = reducerTypes
.filter((o) => typeof o.value === 'string')
.map((o) => ({ value: o.value ?? '', label: o.label ?? String(o.value) }));
const onReducerTypeChange = (v: ComboboxOption<string> | null) => {
const value = v?.value ?? ReducerID.last;
setSimpleCondition((prev) => ({ ...prev, whenField: value }));
};
const isRange = isRangeEvaluator(simpleCondition.evaluator.type);
const thresholdFunction = thresholdFunctions.find((fn) => fn.value === simpleCondition.evaluator?.type);
const onEvalFunctionChange = (v: SelectableValue<EvalFunction>) => {
setSimpleCondition((prev) => ({
...prev,
evaluator: { ...prev.evaluator, type: v.value ?? EvalFunction.IsAbove },
}));
};
const onEvaluateValueChange = (e: React.FormEvent<HTMLInputElement>, index = 0) => {
const value = parseFloat(e.currentTarget.value) || 0;
setSimpleCondition((prev) => ({
...prev,
evaluator: {
...prev.evaluator,
params: index === 0 ? [value, prev.evaluator.params[1]] : [prev.evaluator.params[0], value],
},
}));
};
return (
<div className={base.section}>
<div className={base.sectionHeaderRow}>
<span className={base.stepBadge}>
<Trans i18nKey="alerting.simplified.step-number-two">2</Trans>
</span>
<div className={base.sectionHeader}>
<Trans i18nKey="alerting.simplified.condition.title">Condition</Trans>
</div>
</div>
<div className={base.contentIndented}>
<Stack direction="column" gap={2}>
<InlineFieldRow>
{simpleCondition.whenField && (
<InlineField label={t('alerting.simple-condition-editor.label-when', 'WHEN')}>
<Combobox
options={reducerOptions}
value={simpleCondition.whenField}
onChange={onReducerTypeChange}
width={20}
/>
</InlineField>
)}
<InlineField
label={
simpleCondition.whenField
? t('alerting.simple-condition-editor.label-of-query', 'OF QUERY')
: t('alerting.simple-condition-editor.label-when-query', 'WHEN QUERY')
}
>
<Stack direction="row" gap={1} alignItems="center">
<ThresholdSelect onChange={onEvalFunctionChange} value={thresholdFunction} />
{isRange ? (
<>
<Input
type="number"
width={10}
key={simpleCondition.evaluator.params[0]}
defaultValue={simpleCondition.evaluator.params[0] ?? ''}
onBlur={(event) => onEvaluateValueChange(event, 0)}
/>
<ToLabel />
<Input
type="number"
width={10}
key={simpleCondition.evaluator.params[1]}
defaultValue={simpleCondition.evaluator.params[1] ?? ''}
onBlur={(event) => onEvaluateValueChange(event, 1)}
/>
</>
) : (
<Input
type="number"
width={10}
key={simpleCondition.evaluator.params[0]}
defaultValue={simpleCondition.evaluator.params[0] ?? ''}
onBlur={(event) => onEvaluateValueChange(event, 0)}
/>
)}
</Stack>
</InlineField>
</InlineFieldRow>
<div className={base.paragraphRow}>
<Text>
<Trans i18nKey="alerting.simplified.evaluation.every">Evaluate the rule every</Trans>
</Text>
<Input width={8} {...register('evaluateEvery')} />
<Text>.</Text>
<Text>
<Trans i18nKey="alerting.simplified.evaluation.wait-prefix">Wait</Trans>
</Text>
<Input width={8} value={evaluateFor} onChange={(e) => setValue('evaluateFor', e.currentTarget.value)} />
{isCustomSelected && (
<Field
noMargin
className={base.inlineField}
invalid={isCustomInvalid || undefined}
error={
isCustomInvalid
? t(
'alerting.simplified.evaluation.custom-delay-invalid',
'The delay must be a multiple of the evaluation frequency.'
)
: undefined
}
>
<Input width={8} value={customDelay} onChange={(e) => setValue('evaluateFor', e.currentTarget.value)} />
</Field>
)}
<Text>
<Trans i18nKey="alerting.simplified.evaluation.wait-suffix">
after the condition is breached before firing.
</Trans>
</Text>
<Text>
<Trans i18nKey="alerting.simplified.evaluation.fire">Fire the alert rule</Trans>
</Text>
<Select
width={20}
value={{
value: isCustomSelected ? 'custom' : evaluateFor,
label: fireSelectLabel,
}}
onChange={(v: SelectableValue<string>) => {
const val = v?.value;
if (val === 'custom') {
setCustomMode(true);
const defCustom = pendingOptions[1] || evaluateEvery || '1m';
if (!evaluateFor || pendingOptions.includes(evaluateFor)) {
setValue('evaluateFor', defCustom);
}
return;
}
setCustomMode(false);
setValue('evaluateFor', val || '0s');
}}
options={[
{ value: '0s', label: t('alerting.duration.immediately', 'immediately') },
...pendingOptions.filter((d) => d !== '0s').map((d) => ({ value: d, label: d })),
{ value: 'custom', label: t('alerting.simplified.evaluation.custom-delay', 'with custom delay of') },
]}
/>
<Text>
<Trans i18nKey="alerting.simplified.evaluation.after-breached">
after the condition is initially breached.
</Trans>
</Text>
</div>
{evaluateFor === '0s' && (
<Stack direction="row" gap={0.5} alignItems="center">
<Icon name="exclamation-triangle" />
<Text variant="bodySmall" color="secondary">
<Trans i18nKey="alerting.simplified.evaluation.immediate-warning">
Immediate firing might lead to unnecessary alerts being sent for temporary issues
</Trans>
</Text>
</Stack>
)}
</Stack>
</div>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
section: css({ width: '100%' }),
sectionHeaderRow: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
marginBottom: theme.spacing(1),
}),
sectionHeader: css({
fontWeight: 600,
fontSize: theme.typography.h4.fontSize,
lineHeight: theme.typography.h4.lineHeight,
}),
stepBadge: css({
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
height: 20,
width: 20,
borderRadius: theme.shape.radius.circle,
background: theme.colors.primary.main,
color: theme.colors.text.maxContrast,
fontSize: theme.typography.bodySmall.fontSize,
fontWeight: 600,
}),
contentIndented: css({ marginLeft: `calc(20px + ${theme.spacing(1)})` }),
paragraphRow: css({ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: theme.spacing(1) }),
inlineField: css({ display: 'inline-flex' }),
};
}