replace evaluation inputs with group dropdown

This commit is contained in:
laurenashleigh
2026-01-12 11:41:26 +00:00
parent 179753c608
commit 1430698a8b
4 changed files with 198 additions and 98 deletions
@@ -7,12 +7,10 @@ import { Trans, t } from '@grafana/i18n';
import {
Combobox,
ComboboxOption,
Field,
Icon,
InlineField,
InlineFieldRow,
Input,
Select,
Stack,
Text,
useStyles2,
@@ -24,29 +22,15 @@ 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';
// no custom pending/eval parsing here; use defaults from form
import { EvaluationGroupFieldRow } from './rule-editor/EvaluationGroupFieldRow';
export function RuleConditionSection({ type }: { type: RuleFormType }) {
const base = useStyles2(getStyles);
const { register, watch, setValue } = useFormContext<RuleFormValues>();
const evaluateEvery = watch('evaluateEvery') || '5m';
const { watch } = useFormContext<RuleFormValues>();
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');
}
watch('folder');
type LocalSimpleCondition = { whenField?: string; evaluator: { params: number[]; type: EvalFunction } };
const [simpleCondition, setSimpleCondition] = useState<LocalSimpleCondition>({
@@ -145,72 +129,7 @@ export function RuleConditionSection({ type }: { type: RuleFormType }) {
</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>
<EvaluationGroupFieldRow enableProvisionedGroups={false} />
{evaluateFor === '0s' && (
<Stack direction="row" gap={0.5} alignItems="center">
@@ -0,0 +1,191 @@
import { css } from '@emotion/css';
import { uniqueId } from 'lodash';
import { useMemo, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Box, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui';
import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
import { useFetchGroupsForFolder } from '../../hooks/useFetchGroupsForFolder';
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults';
import { RuleFormValues } from '../../types/rule-form';
import { isProvisionedRuleGroup } from '../../utils/rules';
import { ProvisioningBadge } from '../Provisioning';
import { EvaluationGroupCreationModal } from './GrafanaEvaluationBehavior';
export function EvaluationGroupFieldRow({ enableProvisionedGroups }: { enableProvisionedGroups: boolean }) {
const styles = useStyles2(getStyles);
const {
watch,
setValue,
getValues,
formState: { errors },
control,
} = useFormContext<RuleFormValues>();
const [group, folder] = watch(['group', 'folder']);
const { currentData: rulerNamespace, isLoading: loadingGroups } = useFetchGroupsForFolder(folder?.uid ?? '');
const collator = useMemo(() => new Intl.Collator(), []);
type GroupOption = SelectableValue<string> & { isProvisioned?: boolean };
const groupOptions = useMemo<GroupOption[]>(() => {
if (!rulerNamespace) {
return [];
}
const folderGroups = Object.values(rulerNamespace).flat();
return folderGroups
.map<GroupOption>((g: RulerRuleGroupDTO) => {
const provisioned = isProvisionedRuleGroup(g);
return {
label: g.name,
value: g.name,
description: g.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
isDisabled: !enableProvisionedGroups ? provisioned : false,
isProvisioned: provisioned,
};
})
.sort((a, b) => collator.compare(a.label ?? '', b.label ?? ''));
}, [collator, enableProvisionedGroups, rulerNamespace]);
const defaultGroupValue = group ? { value: group, label: group } : undefined;
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
const onOpenEvaluationGroupCreationModal = () => setIsCreatingEvaluationGroup(true);
const handleEvalGroupCreation = (groupName: string, evaluationInterval: string) => {
setValue('group', groupName);
setValue('evaluateEvery', evaluationInterval);
setIsCreatingEvaluationGroup(false);
};
const label = !folder?.uid
? t(
'alerting.rule-form.evaluation.select-folder-before',
'Select a folder before setting evaluation group and interval'
)
: t('alerting.rule-form.evaluation.evaluation-group-and-interval', 'Evaluation group and interval');
return (
<Stack alignItems="center">
<div style={{ width: 420 }}>
<Field
noMargin
label={label}
data-testid="group-picker"
className={styles.formInput}
error={errors.group?.message}
invalid={!!errors.group?.message}
htmlFor="group"
>
<Controller
render={({ field: { ref, ...field }, fieldState }) => (
<Select
disabled={!folder?.uid || loadingGroups}
inputId="group"
key={uniqueId()}
{...field}
onChange={(group) => {
field.onChange(group.label ?? '');
}}
isLoading={loadingGroups}
invalid={Boolean(folder?.uid) && !group && Boolean(fieldState.error)}
cacheOptions
loadingMessage={t(
'alerting.grafana-evaluation-behavior-step.loadingMessage-loading-groups',
'Loading groups...'
)}
defaultValue={defaultGroupValue}
options={groupOptions}
getOptionLabel={(option: GroupOption) => (
<div>
<span>{option.label}</span>
{option.isProvisioned && (
<>
{' '}
<ProvisioningBadge />
</>
)}
</div>
)}
placeholder={t(
'alerting.grafana-evaluation-behavior-step.placeholder-select-an-evaluation-group',
'Select an evaluation group...'
)}
/>
)}
name="group"
control={control}
rules={{
required: {
value: true,
message: t(
'alerting.grafana-evaluation-behavior-step.message.must-enter-a-group-name',
'Must enter a group name'
),
},
}}
/>
</Field>
</div>
<Box gap={1} display={'flex'} alignItems={'center'}>
<Text color="secondary">
<Trans i18nKey="alerting.grafana-evaluation-behavior-step.or">or</Trans>
</Text>
<Button
onClick={onOpenEvaluationGroupCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!folder?.uid}
data-testid={'new-evaluation-group-button'}
>
<Trans i18nKey="alerting.rule-form.evaluation.new-group">New evaluation group</Trans>
</Button>
</Box>
{isCreatingEvaluationGroup && (
<EvaluationGroupCreationModal
onCreate={handleEvalGroupCreation}
onClose={() => setIsCreatingEvaluationGroup(false)}
groupfoldersForGrafana={rulerNamespace}
/>
)}
{getValues('group') && getValues('evaluateEvery') && (
<div className={styles.evaluationContainer}>
<Stack direction="column" gap={0}>
<div className={styles.marginTop}>
<Stack direction="column" gap={1}>
<Trans
i18nKey="alerting.rule-form.evaluation.group-text"
values={{ evaluateEvery: getValues('evaluateEvery') }}
>
All rules in the selected group are evaluated every {{ evaluateEvery: getValues('evaluateEvery') }}.
</Trans>
</Stack>
</div>
</Stack>
</div>
)}
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
formInput: css({
flexGrow: 1,
}),
evaluationContainer: css({
color: theme.colors.text.secondary,
maxWidth: `${theme.breakpoints.values.sm}px`,
fontSize: theme.typography.size.sm,
}),
marginTop: css({
marginTop: theme.spacing(1),
}),
};
}
@@ -439,7 +439,7 @@ export function GrafanaEvaluationBehaviorStep({
);
}
function EvaluationGroupCreationModal({
export function EvaluationGroupCreationModal({
onClose,
onCreate,
groupfoldersForGrafana,
+1 -11
View File
@@ -1079,9 +1079,6 @@
"text-loading-notification-template": "Loading notification template",
"title-error-loading-notification-template": "Error loading notification template"
},
"duration": {
"immediately": "immediately"
},
"duration-quick-pick": {
"none": "None"
},
@@ -2824,14 +2821,7 @@
"continue-in-alerting": "Continue in Alerting",
"create": "Create",
"evaluation": {
"after-breached": "after the condition is initially breached.",
"custom-delay": "with custom delay of",
"custom-delay-invalid": "The delay must be a multiple of the evaluation frequency.",
"every": "Evaluate the rule every",
"fire": "Fire the alert rule",
"immediate-warning": "Immediate firing might lead to unnecessary alerts being sent for temporary issues",
"wait-prefix": "Wait",
"wait-suffix": "after the condition is breached before firing."
"immediate-warning": "Immediate firing might lead to unnecessary alerts being sent for temporary issues"
},
"notification": {
"contact-point": {