Alerting: Move evaluation outside folder section, and move labels in instead (#95121)

* Move evaluation outside folder section, and move labels in instead

* rename file

* update translations

* refactor

* rename file and component

* refactor

* fix test

* refactor

* rename files and components

* update translations

* fix style

* update translations

* Use useAppNotification for toasts

* update label when group can not be selected yet

* update translations

* update some texts and add comment

* update translations

* remove duplicated code

* fix typo

* update texts and translations

* rename FolderWithoutGroup to FolderSelector

* restore wrong updates

* restore wrong updates

* translations and remove GroupAndFolder component

* address review comments

* remove container

* address review comments

* address review comments

* prettier

* prettier
pull/96719/head
Sonia Aguilar 6 months ago committed by GitHub
parent d3a000e7da
commit 1b0cc60d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      .betterer.results
  2. 6
      public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx
  3. 489
      public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx
  4. 186
      public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx
  5. 686
      public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx
  6. 73
      public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx
  7. 45
      public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx
  8. 22
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  9. 9
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
  10. 9
      public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx
  11. 9
      public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx
  12. 7
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx
  13. 10
      public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx
  14. 2
      public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx
  15. 6
      public/app/features/alerting/unified/components/rules/RulesGroup.tsx
  16. 3
      public/app/features/alerting/unified/types/rule-form.ts
  17. 3
      public/app/features/alerting/unified/utils/rule-form.ts
  18. 8
      public/app/features/alerting/unified/utils/rules.ts
  19. 88
      public/locales/en-US/grafana.json
  20. 88
      public/locales/pseudo-LOCALE/grafana.json

@ -1615,21 +1615,8 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"]
],
"public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"]
"public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]

@ -11,6 +11,7 @@ import { t, Trans } from 'app/core/internationalization';
import { DashboardModel } from '../../../../dashboard/state';
import { RuleFormValues } from '../../types/rule-form';
import { Annotation, annotationLabels } from '../../utils/constants';
import { isGrafanaManagedRuleByType } from '../../utils/rules';
import AnnotationHeaderField from './AnnotationHeaderField';
import DashboardAnnotationField from './DashboardAnnotationField';
@ -31,6 +32,7 @@ const AnnotationsStep = () => {
setValue,
} = useFormContext<RuleFormValues>();
const annotations = watch('annotations');
const type = watch('type');
const { fields, append, remove } = useFieldArray({ control, name: 'annotations' });
@ -104,10 +106,12 @@ const AnnotationsStep = () => {
</Stack>
);
}
// when using Grafana managed rules, the annotations step is the 6th step, as we have an additional step for the configure labels and notifications
const step = isGrafanaManagedRuleByType(type) ? 6 : 5;
return (
<RuleEditorSection
stepNo={5}
stepNo={step}
title={t('alerting.annotations.title', 'Configure notification message')}
description={getAnnotationsSectionDescription()}
fullWidth

@ -1,489 +0,0 @@
import { css } from '@emotion/css';
import { debounce, take, uniqueId } from 'lodash';
import * as React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Controller, FormProvider, useForm, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { AsyncSelect, Box, Button, Field, Input, Label, Modal, Stack, Text, useStyles2 } from '@grafana/ui';
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
import { useAppNotification } from 'app/core/copy/appNotification';
import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv';
import { useNewFolderMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { AccessControlAction } from 'app/types';
import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { alertRuleApi } from '../../api/alertRuleApi';
import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi';
import { Folder, RuleFormValues } from '../../types/rule-form';
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
import { isGrafanaRecordingRuleByType, isGrafanaRulerRule } from '../../utils/rules';
import { ProvisioningBadge } from '../Provisioning';
import { evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
import { EvaluationGroupQuickPick } from './EvaluationGroupQuickPick';
export const MAX_GROUP_RESULTS = 1000;
export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => {
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
// for our folders
const { isLoading: isLoadingRulerNamespace, currentData: rulerNamespace } =
alertRuleApi.endpoints.rulerNamespace.useQuery(
{
namespace: folderUid,
rulerConfig: GRAFANA_RULER_CONFIG,
},
{
skip: !folderUid,
refetchOnMountOrArgChange: true,
}
);
// There should be only one entry in the rulerNamespace object
// However it uses folder name as key, so to avoid fetching folder name, we use Object.values
const groupOptions = useMemo(() => {
if (!rulerNamespace) {
// still waiting for namespace information to be fetched
return [];
}
const folderGroups = Object.values(rulerNamespace).flat() ?? [];
return folderGroups
.map<SelectableValue<string>>((group) => {
const isProvisioned = isProvisionedGroup(group);
return {
label: group.name,
value: group.name,
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
// we include provisioned folders, but disable the option to select them
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
isProvisioned: isProvisioned,
};
})
.sort(sortByLabel);
}, [rulerNamespace, enableProvisionedGroups]);
return { groupOptions, loading: isLoadingRulerNamespace };
};
const isProvisionedGroup = (group: RulerRuleGroupDTO) => {
return group.rules.some((rule) => isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance) === true);
};
const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) => {
return a.label?.localeCompare(b.label ?? '') || 0;
};
const findGroupMatchingLabel = (group: SelectableValue<string>, query: string) => {
return group.label?.toLowerCase().includes(query.toLowerCase());
};
export function FolderAndGroup({
groupfoldersForGrafana,
enableProvisionedGroups,
}: {
groupfoldersForGrafana?: RulerRulesConfigDTO | null;
enableProvisionedGroups: boolean;
}) {
const {
formState: { errors },
watch,
setValue,
control,
} = useFormContext<RuleFormValues>();
const styles = useStyles2(getStyles);
const [folder, group, type] = watch(['folder', 'group', 'type']);
const isGrafanaRecordingRule = type ? isGrafanaRecordingRuleByType(type) : false;
const { groupOptions, loading } = useFolderGroupOptions(folder?.uid ?? '', enableProvisionedGroups);
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
const onOpenFolderCreationModal = () => setIsCreatingFolder(true);
const onOpenEvaluationGroupCreationModal = () => setIsCreatingEvaluationGroup(true);
const handleFolderCreation = (folder: Folder) => {
resetGroup();
setValue('folder', folder);
setIsCreatingFolder(false);
};
const handleEvalGroupCreation = (groupName: string, evaluationInterval: string) => {
setValue('group', groupName);
setValue('evaluateEvery', evaluationInterval);
setIsCreatingEvaluationGroup(false);
};
const resetGroup = useCallback(() => {
setValue('group', '');
}, [setValue]);
const getOptions = useCallback(
async (query: string) => {
const results = query ? groupOptions.filter((group) => findGroupMatchingLabel(group, query)) : groupOptions;
return take(results, MAX_GROUP_RESULTS);
},
[groupOptions]
);
const debouncedSearch = useMemo(() => {
return debounce(getOptions, 300, { leading: true });
}, [getOptions]);
const defaultGroupValue = group ? { value: group, label: group } : undefined;
const evaluationDesc = isGrafanaRecordingRule
? t('alerting.folderAndGroup.evaluation.text.recording', 'Define how often the recording rule is evaluated.')
: t('alerting.folderAndGroup.evaluation.text.alerting', 'Define how often the alert rule is evaluated.');
return (
<div className={styles.container}>
<Stack alignItems="center">
{
<Field
label={
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
Folder
</Label>
}
className={styles.formInput}
error={errors.folder?.message}
data-testid="folder-picker"
>
<Stack direction="row" alignItems="center">
{(!isCreatingFolder && (
<>
<Controller
render={({ field: { ref, ...field } }) => (
<div style={{ width: 420 }}>
<NestedFolderPicker
showRootFolder={false}
invalid={!!errors.folder?.message}
{...field}
value={folder?.uid}
onChange={(uid, title) => {
if (uid && title) {
setValue('folder', { title, uid });
} else {
setValue('folder', undefined);
}
resetGroup();
}}
/>
</div>
)}
name="folder"
rules={{
required: { value: true, message: 'Select a folder' },
}}
/>
<Text color="secondary">or</Text>
<Button
onClick={onOpenFolderCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
data-testid={selectors.components.AlertRules.newFolderButton}
>
New folder
</Button>
</>
)) || <div>Creating new folder...</div>}
</Stack>
</Field>
}
{isCreatingFolder && (
<FolderCreationModal onCreate={handleFolderCreation} onClose={() => setIsCreatingFolder(false)} />
)}
</Stack>
<Stack alignItems="center">
<div style={{ width: 420 }}>
<Field
label="Evaluation group and interval"
data-testid="group-picker"
description={evaluationDesc}
className={styles.formInput}
error={errors.group?.message}
invalid={!!errors.group?.message}
htmlFor="group"
>
<Controller
render={({ field: { ref, ...field }, fieldState }) => (
<AsyncSelect
disabled={!folder || loading}
inputId="group"
key={uniqueId()}
{...field}
onChange={(group) => {
field.onChange(group.label ?? '');
}}
isLoading={loading}
invalid={Boolean(folder) && !group && Boolean(fieldState.error)}
loadOptions={debouncedSearch}
cacheOptions
loadingMessage={'Loading groups...'}
defaultValue={defaultGroupValue}
defaultOptions={groupOptions}
getOptionLabel={(option: SelectableValue<string>) => (
<div>
<span>{option.label}</span>
{option.isProvisioned && (
<>
{' '}
<ProvisioningBadge />
</>
)}
</div>
)}
placeholder={'Select an evaluation group...'}
/>
)}
name="group"
control={control}
rules={{
required: { value: true, message: 'Must enter a group name' },
}}
/>
</Field>
</div>
<Box marginTop={4} gap={1} display={'flex'} alignItems={'center'}>
<Text color="secondary">or</Text>
<Button
onClick={onOpenEvaluationGroupCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!folder}
data-testid={selectors.components.AlertRules.newEvaluationGroupButton}
>
New evaluation group
</Button>
</Box>
{isCreatingEvaluationGroup && (
<EvaluationGroupCreationModal
onCreate={handleEvalGroupCreation}
onClose={() => setIsCreatingEvaluationGroup(false)}
groupfoldersForGrafana={groupfoldersForGrafana}
/>
)}
</Stack>
</div>
);
}
function FolderCreationModal({
onClose,
onCreate,
}: {
onClose: () => void;
onCreate: (folder: Folder) => void;
}): React.ReactElement {
const styles = useStyles2(getStyles);
const notifyApp = useAppNotification();
const [title, setTitle] = useState('');
const [createFolder] = useNewFolderMutation();
const onSubmit = async () => {
const { data, error } = await createFolder({ title });
if (error) {
notifyApp.error('Failed to create folder');
} else if (data) {
onCreate({ title: data.title, uid: data.uid });
notifyApp.success('Folder created');
}
};
return (
<Modal className={styles.modal} isOpen={true} title={'New folder'} onDismiss={onClose} onClickBackdrop={onClose}>
<div className={styles.modalTitle}>Create a new folder to store your rule</div>
<form onSubmit={onSubmit}>
<Field label={<Label htmlFor="folder">Folder name</Label>}>
<Input
data-testid={selectors.components.AlertRules.newFolderNameField}
autoFocus={true}
id="folderName"
placeholder="Enter a name"
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
className={styles.formInput}
/>
</Field>
<Modal.ButtonRow>
<Button variant="secondary" type="button" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
disabled={!title}
data-testid={selectors.components.AlertRules.newFolderNameCreateButton}
>
Create
</Button>
</Modal.ButtonRow>
</form>
</Modal>
);
}
function EvaluationGroupCreationModal({
onClose,
onCreate,
groupfoldersForGrafana,
}: {
onClose: () => void;
onCreate: (group: string, evaluationInterval: string) => void;
groupfoldersForGrafana?: RulerRulesConfigDTO | null;
}): React.ReactElement {
const styles = useStyles2(getStyles);
const onSubmit = () => {
onCreate(getValues('group'), getValues('evaluateEvery'));
};
const { watch } = useFormContext<RuleFormValues>();
const evaluateEveryId = 'eval-every-input';
const evaluationGroupNameId = 'new-eval-group-name';
const [groupName, folderName, type] = watch(['group', 'folder.title', 'type']);
const isGrafanaRecordingRule = type ? isGrafanaRecordingRuleByType(type) : false;
const groupRules =
(groupfoldersForGrafana && groupfoldersForGrafana[folderName]?.find((g) => g.name === groupName)?.rules) ?? [];
const onCancel = () => {
onClose();
};
const formAPI = useForm({
defaultValues: { group: '', evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL },
mode: 'onChange',
shouldFocusError: true,
});
const { register, handleSubmit, formState, setValue, getValues, watch: watchGroupFormValues } = formAPI;
const evaluationInterval = watchGroupFormValues('evaluateEvery');
const setEvaluationInterval = (interval: string) => {
setValue('evaluateEvery', interval, { shouldValidate: true });
};
const modalTitle = isGrafanaRecordingRule
? t(
'alerting.folderAndGroup.evaluation.modal.text.recording',
'Create a new evaluation group to use for this recording rule.'
)
: t(
'alerting.folderAndGroup.evaluation.modal.text.alerting',
'Create a new evaluation group to use for this alert rule.'
);
return (
<Modal
className={styles.modal}
isOpen={true}
title={'New evaluation group'}
onDismiss={onCancel}
onClickBackdrop={onCancel}
>
<div className={styles.modalTitle}>{modalTitle}</div>
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit(() => onSubmit())}>
<Field
label={
<Label
htmlFor={evaluationGroupNameId}
description="A group evaluates all its rules over the same evaluation interval."
>
Evaluation group name
</Label>
}
error={formState.errors.group?.message}
invalid={Boolean(formState.errors.group)}
>
<Input
data-testid={selectors.components.AlertRules.newEvaluationGroupName}
className={styles.formInput}
autoFocus={true}
id={evaluationGroupNameId}
placeholder="Enter a name"
{...register('group', { required: { value: true, message: 'Required.' } })}
/>
</Field>
<Field
error={formState.errors.evaluateEvery?.message}
label={
<Label htmlFor={evaluateEveryId} description="How often all rules in the group are evaluated.">
Evaluation interval
</Label>
}
invalid={Boolean(formState.errors.evaluateEvery)}
>
<Input
data-testid={selectors.components.AlertRules.newEvaluationGroupInterval}
className={styles.formInput}
id={evaluateEveryId}
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
{...register(
'evaluateEvery',
evaluateEveryValidationOptions<{ group: string; evaluateEvery: string }>(groupRules)
)}
/>
</Field>
<EvaluationGroupQuickPick currentInterval={evaluationInterval} onSelect={setEvaluationInterval} />
<Modal.ButtonRow>
<Button variant="secondary" type="button" onClick={onCancel}>
Cancel
</Button>
<Button
type="submit"
disabled={!formState.isValid}
data-testid={selectors.components.AlertRules.newEvaluationGroupCreate}
>
Create
</Button>
</Modal.ButtonRow>
</form>
</FormProvider>
</Modal>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
display: 'flex',
flexDirection: 'column',
alignItems: 'baseline',
maxWidth: `${theme.breakpoints.values.lg}px`,
justifyContent: 'space-between',
}),
formInput: css({
flexGrow: 1,
}),
modal: css({
width: `${theme.breakpoints.values.sm}px`,
}),
modalTitle: css({
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
});

@ -0,0 +1,186 @@
import { css } from '@emotion/css';
import * as React from 'react';
import { useCallback, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Button, Field, Input, Label, Modal, Stack, Text, useStyles2 } from '@grafana/ui';
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/services/context_srv';
import { useNewFolderMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { AccessControlAction } from 'app/types';
import { Trans } from '../../../../../core/internationalization/index';
import { Folder, RuleFormValues } from '../../types/rule-form';
export function FolderSelector() {
const {
formState: { errors },
setValue,
watch,
} = useFormContext<RuleFormValues>();
const resetGroup = useCallback(() => {
setValue('group', '');
}, [setValue]);
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
const folder = watch('folder');
const onOpenFolderCreationModal = () => setIsCreatingFolder(true);
const handleFolderCreation = (folder: Folder) => {
resetGroup();
setValue('folder', folder);
setIsCreatingFolder(false);
};
return (
<>
<Stack alignItems="center">
{
<Field
label={
<Label htmlFor="folder" description={'Select a folder to store your rule in.'}>
<Trans i18nKey="alerting.rule-form.folder.label">Folder</Trans>
</Label>
}
error={errors.folder?.message}
data-testid="folder-picker"
>
<Stack direction="row" alignItems="center">
{(!isCreatingFolder && (
<>
<Controller
render={({ field: { ref, ...field } }) => (
<div style={{ width: 420 }}>
<NestedFolderPicker
showRootFolder={false}
invalid={!!errors.folder?.message}
{...field}
value={folder?.uid}
onChange={(uid, title) => {
if (uid && title) {
setValue('folder', { title, uid });
} else {
setValue('folder', undefined);
}
resetGroup();
}}
/>
</div>
)}
name="folder"
rules={{
required: { value: true, message: 'Select a folder' },
}}
/>
<Text color="secondary">
<Trans i18nKey="alerting.rule-form.folder.new-folder-or">or</Trans>
</Text>
<Button
onClick={onOpenFolderCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
data-testid={selectors.components.AlertRules.newFolderButton}
>
<Trans i18nKey="alerting.rule-form.folder.new-folder">New folder</Trans>
</Button>
</>
)) || (
<div>
<Trans i18nKey="alerting.rule-form.folder.creating-new-folder">Creating new folder</Trans>
{'...'}
</div>
)}
</Stack>
</Field>
}
</Stack>
{isCreatingFolder && (
<FolderCreationModal onCreate={handleFolderCreation} onClose={() => setIsCreatingFolder(false)} />
)}
</>
);
}
function FolderCreationModal({
onClose,
onCreate,
}: {
onClose: () => void;
onCreate: (folder: Folder) => void;
}): React.ReactElement {
const styles = useStyles2(getStyles);
const notifyApp = useAppNotification();
const [title, setTitle] = useState('');
const [createFolder] = useNewFolderMutation();
const onSubmit = async () => {
const { data, error } = await createFolder({ title });
if (error) {
notifyApp.error('Failed to create folder');
} else if (data) {
onCreate({ title: data.title, uid: data.uid });
notifyApp.success('Folder created');
}
};
return (
<Modal className={styles.modal} isOpen={true} title={'New folder'} onDismiss={onClose} onClickBackdrop={onClose}>
<Stack direction="column" gap={2}>
<Text color="secondary">
<Trans i18nKey="alerting.rule-form.folder.create-folder">
Create a new folder to store your alert rule in.
</Trans>
</Text>
<form onSubmit={onSubmit}>
<Field
label={
<Label htmlFor="folder">
<Trans i18nKey="alerting.rule-form.folder.name">Folder name</Trans>
</Label>
}
>
<Input
data-testid={selectors.components.AlertRules.newFolderNameField}
autoFocus={true}
id="folderName"
placeholder="Enter a name"
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
/>
</Field>
<Modal.ButtonRow>
<Button variant="secondary" type="button" onClick={onClose}>
<Trans i18nKey="alerting.rule-form.folder.cancel">Cancel</Trans>
</Button>
<Button
type="submit"
disabled={!title}
data-testid={selectors.components.AlertRules.newFolderNameCreateButton}
>
<Trans i18nKey="alerting.rule-form.folder.create">Create</Trans>
</Button>
</Modal.ButtonRow>
</form>
</Stack>
</Modal>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
modal: css({
width: `${theme.breakpoints.values.sm}px`,
}),
});

@ -1,29 +1,113 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useState } from 'react';
import { Controller, RegisterOptions, useFormContext } from 'react-hook-form';
import { debounce, take, uniqueId } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Field, Icon, IconButton, Input, Label, Stack, Switch, Text, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { isGrafanaAlertingRuleByType, isGrafanaRecordingRuleByType } from 'app/features/alerting/unified/utils/rules';
import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
import { LogMessages, logInfo } from '../../Analytics';
import { selectors } from '@grafana/e2e-selectors';
import {
AsyncSelect,
Box,
Button,
Field,
Icon,
IconButton,
Input,
Label,
Modal,
Stack,
Switch,
Text,
Tooltip,
useStyles2,
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { logInfo, LogMessages } from '../../Analytics';
import { alertRuleApi } from '../../api/alertRuleApi';
import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi';
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { RuleFormValues } from '../../types/rule-form';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
import {
isGrafanaAlertingRuleByType,
isGrafanaManagedRuleByType,
isGrafanaRecordingRuleByType,
isGrafanaRulerRule,
} from '../../utils/rules';
import { parsePrometheusDuration } from '../../utils/time';
import { CollapseToggle } from '../CollapseToggle';
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
import { ProvisioningBadge } from '../Provisioning';
import { EditRuleGroupModal, evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
import { FolderAndGroup, useFolderGroupOptions } from './FolderAndGroup';
import { EvaluationGroupQuickPick } from './EvaluationGroupQuickPick';
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
import { NeedHelpInfo } from './NeedHelpInfo';
import { PendingPeriodQuickPick } from './PendingPeriodQuickPick';
import { RuleEditorSection } from './RuleEditorSection';
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
export const MAX_GROUP_RESULTS = 1000;
export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => {
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
// for our folders
const { isLoading: isLoadingRulerNamespace, currentData: rulerNamespace } =
alertRuleApi.endpoints.rulerNamespace.useQuery(
{
namespace: folderUid,
rulerConfig: GRAFANA_RULER_CONFIG,
},
{
skip: !folderUid,
refetchOnMountOrArgChange: true,
}
);
// There should be only one entry in the rulerNamespace object
// However it uses folder name as key, so to avoid fetching folder name, we use Object.values
const groupOptions = useMemo(() => {
if (!rulerNamespace) {
// still waiting for namespace information to be fetched
return [];
}
const folderGroups = Object.values(rulerNamespace).flat() ?? [];
return folderGroups
.map<SelectableValue<string>>((group) => {
const isProvisioned = isProvisionedGroup(group);
return {
label: group.name,
value: group.name,
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
// we include provisioned folders, but disable the option to select them
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
isProvisioned: isProvisioned,
};
})
.sort(sortByLabel);
}, [rulerNamespace, enableProvisionedGroups]);
return { groupOptions, loading: isLoadingRulerNamespace };
};
const isProvisionedGroup = (group: RulerRuleGroupDTO) => {
return group.rules.some((rule) => isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance) === true);
};
const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) => {
return a.label?.localeCompare(b.label ?? '') || 0;
};
const findGroupMatchingLabel = (group: SelectableValue<string>, query: string) => {
return group.label?.toLowerCase().includes(query.toLowerCase());
};
const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({
required: {
@ -49,7 +133,7 @@ const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluate
return millisFor >= millisEvery
? true
: t(
'alert-rule-form.evaluation-behaviour-for.validation',
'alerting.rule-form.evaluation-behaviour-for.validation',
'Pending period must be greater than or equal to the evaluation interval.'
);
} catch (err) {
@ -60,7 +144,7 @@ const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluate
} catch (error) {
return error instanceof Error
? error.message
: t('alert-rule-form.evaluation-behaviour-for.error-parsing', 'Failed to parse duration');
: t('alerting.rule-form.evaluation-behaviour-for.error-parsing', 'Failed to parse duration');
}
},
});
@ -75,29 +159,50 @@ const useIsNewGroup = (folder: string, group: string) => {
return !groupIsInGroupOptions(group);
};
function FolderGroupAndEvaluationInterval({
export function GrafanaEvaluationBehaviorStep({
evaluateEvery,
setEvaluateEvery,
existing,
enableProvisionedGroups,
}: {
evaluateEvery: string;
setEvaluateEvery: (value: string) => void;
existing: boolean;
enableProvisionedGroups: boolean;
}) {
const styles = useStyles2(getStyles);
const { watch, setValue, getValues } = useFormContext<RuleFormValues>();
const [isEditingGroup, setIsEditingGroup] = useState(false);
const [showErrorHandling, setShowErrorHandling] = useState(false);
const [groupName, folderUid, folderName] = watch(['group', 'folder.uid', 'folder.title']);
const {
watch,
setValue,
getValues,
formState: { errors },
control,
} = useFormContext<RuleFormValues>();
const [folder, group, type, isPaused, folderUid, folderName] = watch([
'folder',
'group',
'type',
'isPaused',
'folder.uid',
'folder.title',
]);
const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type);
const isGrafanaRecordingRule = isGrafanaRecordingRuleByType(type);
const { groupOptions, loading } = useFolderGroupOptions(folder?.uid ?? '', enableProvisionedGroups);
const [isEditingGroup, setIsEditingGroup] = useState(false);
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
const grafanaNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
const existingNamespace = grafanaNamespaces.find((ns) => ns.uid === folderUid);
const existingGroup = existingNamespace?.groups.find((g) => g.name === groupName);
const existingGroup = existingNamespace?.groups.find((g) => g.name === group);
const isNewGroup = useIsNewGroup(folderUid ?? '', groupName);
const isNewGroup = useIsNewGroup(folderUid ?? '', group);
useEffect(() => {
if (!isNewGroup && existingGroup?.interval) {
@ -114,60 +219,369 @@ function FolderGroupAndEvaluationInterval({
const onOpenEditGroupModal = () => setIsEditingGroup(true);
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !groupName;
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !group;
const emptyNamespace: CombinedRuleNamespace = {
name: folderName,
rulesSource: GRAFANA_RULES_SOURCE_NAME,
groups: [],
};
const emptyGroup: CombinedRuleGroup = { name: groupName, interval: evaluateEvery, rules: [], totals: {} };
const emptyGroup: CombinedRuleGroup = { name: group, interval: evaluateEvery, rules: [], totals: {} };
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
const handleEvalGroupCreation = (groupName: string, evaluationInterval: string) => {
setValue('group', groupName);
setValue('evaluateEvery', evaluationInterval);
setIsCreatingEvaluationGroup(false);
};
const getOptions = useCallback(
async (query: string) => {
const results = query ? groupOptions.filter((group) => findGroupMatchingLabel(group, query)) : groupOptions;
return take(results, MAX_GROUP_RESULTS);
},
[groupOptions]
);
const debouncedSearch = useMemo(() => {
return debounce(getOptions, 300, { leading: true });
}, [getOptions]);
const defaultGroupValue = group ? { value: group, label: group } : undefined;
const pauseContentText = isGrafanaRecordingRule
? t('alerting.rule-form.evaluation.pause.recording', 'Turn on to pause evaluation for this recording rule.')
: t('alerting.rule-form.evaluation.pause.alerting', 'Turn on to pause evaluation for this alert rule.');
const onOpenEvaluationGroupCreationModal = () => setIsCreatingEvaluationGroup(true);
const step = isGrafanaManagedRuleByType(type) ? 4 : 3;
const label =
isGrafanaManagedRuleByType(type) && !folder
? 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 (
<div>
<FolderAndGroup
groupfoldersForGrafana={groupfoldersForGrafana?.result}
enableProvisionedGroups={enableProvisionedGroups}
/>
{folderName && isEditingGroup && (
<EditCloudGroupModal
namespace={existingNamespace ?? emptyNamespace}
group={existingGroup ?? emptyGroup}
folderUid={folderUid}
onClose={() => closeEditGroupModal()}
intervalEditOnly
hideFolder={true}
/>
)}
{folderName && groupName && (
<div className={styles.evaluationContainer}>
<Stack direction="column" gap={0}>
<div className={styles.marginTop}>
<Stack direction="column" gap={1}>
{getValues('group') && getValues('evaluateEvery') && (
<span>
<Trans i18nKey="alert-rule-form.evaluation-behaviour-group.text" values={{ evaluateEvery }}>
All rules in the selected group are evaluated every {{ evaluateEvery }}.
</Trans>
{!isNewGroup && (
<IconButton
name="pen"
aria-label="Edit"
disabled={editGroupDisabled}
onClick={onOpenEditGroupModal}
/>
// TODO remove "and alert condition" for recording rules
<RuleEditorSection
stepNo={step}
title="Set evaluation behavior"
description={getDescription(isGrafanaRecordingRule)}
>
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
<Stack alignItems="center">
<div style={{ width: 420 }}>
<Field
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 }) => (
<AsyncSelect
disabled={!folder || loading}
inputId="group"
key={uniqueId()}
{...field}
onChange={(group) => {
field.onChange(group.label ?? '');
}}
isLoading={loading}
invalid={Boolean(folder) && !group && Boolean(fieldState.error)}
loadOptions={debouncedSearch}
cacheOptions
loadingMessage={'Loading groups...'}
defaultValue={defaultGroupValue}
defaultOptions={groupOptions}
getOptionLabel={(option: SelectableValue<string>) => (
<div>
<span>{option.label}</span>
{option.isProvisioned && (
<>
{' '}
<ProvisioningBadge />
</>
)}
</div>
)}
</span>
placeholder={'Select an evaluation group...'}
/>
)}
</Stack>
</div>
</Stack>
</div>
name="group"
control={control}
rules={{
required: { value: true, message: 'Must enter a group name' },
}}
/>
</Field>
</div>
<Box gap={1} display={'flex'} alignItems={'center'}>
<Text color="secondary">or</Text>
<Button
onClick={onOpenEvaluationGroupCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!folder}
data-testid={selectors.components.AlertRules.newEvaluationGroupButton}
>
<Trans i18nKey="alerting.rule-form.evaluation.new-group">New evaluation group</Trans>
</Button>
</Box>
{isCreatingEvaluationGroup && (
<EvaluationGroupCreationModal
onCreate={handleEvalGroupCreation}
onClose={() => setIsCreatingEvaluationGroup(false)}
groupfoldersForGrafana={groupfoldersForGrafana?.result}
/>
)}
</Stack>
{folderName && isEditingGroup && (
<EditRuleGroupModal
namespace={existingNamespace ?? emptyNamespace}
group={existingGroup ?? emptyGroup}
folderUid={folderUid}
onClose={() => closeEditGroupModal()}
intervalEditOnly
hideFolder={true}
/>
)}
{folderName && group && (
<div className={styles.evaluationContainer}>
<Stack direction="column" gap={0}>
<div className={styles.marginTop}>
<Stack direction="column" gap={1}>
{getValues('group') && getValues('evaluateEvery') && (
<Stack direction="row" gap={1} alignItems="center">
<Trans i18nKey="alerting.rule-form.evaluation.group-text" values={{ evaluateEvery }}>
All rules in the selected group are evaluated every {{ evaluateEvery }}.
</Trans>
{!isNewGroup && (
<IconButton
name="pen"
aria-label="Edit"
disabled={editGroupDisabled}
onClick={onOpenEditGroupModal}
/>
)}
</Stack>
)}
</Stack>
</div>
</Stack>
</div>
)}
{/* Show the pending period input only for Grafana alerting rules */}
{isGrafanaAlertingRule && <ForInput evaluateEvery={evaluateEvery} />}
{existing && (
<Field htmlFor="pause-alert-switch">
<Controller
render={() => (
<Stack gap={1} direction="row" alignItems="center">
<Switch
id="pause-alert"
onChange={(value) => {
setValue('isPaused', value.currentTarget.checked);
}}
value={Boolean(isPaused)}
/>
<label htmlFor="pause-alert" className={styles.switchLabel}>
<Trans i18nKey="alerting.rule-form.pause.label">Pause evaluation</Trans>
<Tooltip placement="top" content={pauseContentText} theme={'info'}>
<Icon tabIndex={0} name="info-circle" size="sm" className={styles.infoIcon} />
</Tooltip>
</label>
</Stack>
)}
name="isPaused"
/>
</Field>
)}
</Stack>
{isGrafanaAlertingRule && (
<>
<CollapseToggle
isCollapsed={!showErrorHandling}
onToggle={(collapsed) => setShowErrorHandling(!collapsed)}
text="Configure no data and error handling"
/>
{showErrorHandling && (
<>
<NeedHelpInfoForConfigureNoDataError />
<Field htmlFor="no-data-state-input" label="Alert state if no data or all values are null">
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
inputId="no-data-state-input"
width={42}
includeNoData={true}
includeError={false}
onChange={(value) => onChange(value?.value)}
/>
)}
name="noDataState"
/>
</Field>
<Field htmlFor="exec-err-state-input" label="Alert state if execution error or timeout">
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
inputId="exec-err-state-input"
width={42}
includeNoData={false}
includeError={true}
onChange={(value) => onChange(value?.value)}
/>
)}
name="execErrState"
/>
</Field>
</>
)}
</>
)}
</div>
</RuleEditorSection>
);
}
function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
function EvaluationGroupCreationModal({
onClose,
onCreate,
groupfoldersForGrafana,
}: {
onClose: () => void;
onCreate: (group: string, evaluationInterval: string) => void;
groupfoldersForGrafana?: RulerRulesConfigDTO | null;
}): React.ReactElement {
const styles = useStyles2(getStyles);
const { watch } = useFormContext<RuleFormValues>();
const evaluateEveryId = 'eval-every-input';
const evaluationGroupNameId = 'new-eval-group-name';
const [groupName, folderName, type] = watch(['group', 'folder.title', 'type']);
const isGrafanaRecordingRule = type ? isGrafanaRecordingRuleByType(type) : false;
const formAPI = useForm({
defaultValues: { group: '', evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL },
mode: 'onChange',
shouldFocusError: true,
});
const { register, handleSubmit, formState, setValue, getValues, watch: watchGroupFormValues } = formAPI;
const evaluationInterval = watchGroupFormValues('evaluateEvery');
const groupRules =
(groupfoldersForGrafana && groupfoldersForGrafana[folderName]?.find((g) => g.name === groupName)?.rules) ?? [];
const onSubmit = () => {
onCreate(getValues('group'), getValues('evaluateEvery'));
};
const onCancel = () => {
onClose();
};
const setEvaluationInterval = (interval: string) => {
setValue('evaluateEvery', interval, { shouldValidate: true });
};
const modalTitle = isGrafanaRecordingRule
? t(
'alerting.folderAndGroup.evaluation.modal.text.recording',
'Create a new evaluation group to use for this recording rule.'
)
: t(
'alerting.folderAndGroup.evaluation.modal.text.alerting',
'Create a new evaluation group to use for this alert rule.'
);
return (
<Modal
className={styles.modal}
isOpen={true}
title={'New evaluation group'}
onDismiss={onCancel}
onClickBackdrop={onCancel}
>
<div className={styles.modalTitle}>{modalTitle}</div>
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit(() => onSubmit())}>
<Field
label={
<Label
htmlFor={evaluationGroupNameId}
description="A group evaluates all its rules over the same evaluation interval."
>
<Trans i18nKey="alerting.rule-form.evaluation.group-name">Evaluation group name</Trans>
</Label>
}
error={formState.errors.group?.message}
invalid={Boolean(formState.errors.group)}
>
<Input
data-testid={selectors.components.AlertRules.newEvaluationGroupName}
className={styles.formInput}
autoFocus={true}
id={evaluationGroupNameId}
placeholder="Enter a name"
{...register('group', { required: { value: true, message: 'Required.' } })}
/>
</Field>
<Field
error={formState.errors.evaluateEvery?.message}
label={
<Label htmlFor={evaluateEveryId} description="How often all rules in the group are evaluated.">
<Trans i18nKey="alerting.rule-form.evaluation.group.interval">Evaluation interval</Trans>
</Label>
}
invalid={Boolean(formState.errors.evaluateEvery)}
>
<Input
data-testid={selectors.components.AlertRules.newEvaluationGroupInterval}
className={styles.formInput}
id={evaluateEveryId}
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
{...register(
'evaluateEvery',
evaluateEveryValidationOptions<{ group: string; evaluateEvery: string }>(groupRules)
)}
/>
</Field>
<EvaluationGroupQuickPick currentInterval={evaluationInterval} onSelect={setEvaluationInterval} />
<Modal.ButtonRow>
<Button variant="secondary" type="button" onClick={onCancel}>
<Trans i18nKey="alerting.rule-form.evaluation.group.cancel">Cancel</Trans>
</Button>
<Button
type="submit"
disabled={!formState.isValid}
data-testid={selectors.components.AlertRules.newEvaluationGroupCreate}
>
<Trans i18nKey="alerting.rule-form.evaluation.group.create">Create</Trans>
</Button>
</Modal.ButtonRow>
</form>
</FormProvider>
</Modal>
);
}
export function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
const styles = useStyles2(getStyles);
const {
register,
@ -191,7 +605,7 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
htmlFor={evaluateForId}
description='Period the threshold condition must be met to trigger the alert. Selecting "None" triggers the alert immediately once the condition is met.'
>
<Trans i18nKey="alert-rule-form.evaluation-behaviour.pending-period">Pending period</Trans>
<Trans i18nKey="alerting.rule-form.evaluation-behaviour.pending-period">Pending period</Trans>
</Label>
}
className={styles.inlineField}
@ -217,7 +631,7 @@ function NeedHelpInfoForConfigureNoDataError() {
return (
<Stack direction="row" gap={0.5} alignItems="center">
<Text variant="bodySmall" color="secondary">
<Trans i18nKey="alert-rule-form.evaluation-behaviour.info-help.text">
<Trans i18nKey="alerting.rule-form.evaluation-behaviour.info-help.text">
Define the alert behavior when the evaluation fails or the query returns no data.
</Trans>
</Text>
@ -242,7 +656,7 @@ function getDescription(isGrafanaRecordingRule: boolean) {
Define how the recording rule is evaluated.
</Trans>
) : (
<Trans i18nKey="alerting.alert-rule-form.evaluation-behaviour.description.text">
<Trans i18nKey="alerting.rule-form.evaluation-behaviour.description.text">
Define how the alert rule is evaluated.
</Trans>
)}
@ -251,18 +665,18 @@ function getDescription(isGrafanaRecordingRule: boolean) {
contentText={
<>
<p>
<Trans i18nKey="alert-rule-form.evaluation-behaviour-description1">
<Trans i18nKey="alerting.rule-form.evaluation-behaviour-description1">
Evaluation groups are containers for evaluating alert and recording rules.
</Trans>
</p>
<p>
<Trans i18nKey="alert-rule-form.evaluation-behaviour-description2">
<Trans i18nKey="alerting.rule-form.evaluation-behaviour-description2">
An evaluation group defines an evaluation interval - how often a rule is evaluated. Alert rules within
the same evaluation group are evaluated over the same evaluation interval.
</Trans>
</p>
<p>
<Trans i18nKey="alert-rule-form.evaluation-behaviour-description3">
<Trans i18nKey="alerting.rule-form.evaluation-behaviour-description3">
Pending period specifies how long the threshold condition must be met before the alert starts firing.
This option helps prevent alerts from being triggered by temporary issues.
</Trans>
@ -277,150 +691,18 @@ function getDescription(isGrafanaRecordingRule: boolean) {
);
}
export function GrafanaEvaluationBehavior({
evaluateEvery,
setEvaluateEvery,
existing,
enableProvisionedGroups,
}: {
evaluateEvery: string;
setEvaluateEvery: (value: string) => void;
existing: boolean;
enableProvisionedGroups: boolean;
}) {
const styles = useStyles2(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false);
const { watch, setValue } = useFormContext<RuleFormValues>();
const isPaused = watch('isPaused');
const type = watch('type');
const isGrafanaAlertingRule = isGrafanaAlertingRuleByType(type);
const isGrafanaRecordingRule = type ? isGrafanaRecordingRuleByType(type) : false;
const pauseContentText = isGrafanaRecordingRule
? t('alert-rule-form.pause.recording', 'Turn on to pause evaluation for this recording rule.')
: t('alert-rule-form.pause.alerting', 'Turn on to pause evaluation for this alert rule.');
return (
// TODO remove "and alert condition" for recording rules
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription(isGrafanaRecordingRule)}>
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
<FolderGroupAndEvaluationInterval
setEvaluateEvery={setEvaluateEvery}
evaluateEvery={evaluateEvery}
enableProvisionedGroups={enableProvisionedGroups}
/>
{/* Show the pending period input only for Grafana alerting rules */}
{isGrafanaAlertingRule && <ForInput evaluateEvery={evaluateEvery} />}
{existing && (
<Field htmlFor="pause-alert-switch">
<Controller
render={() => (
<Stack gap={1} direction="row" alignItems="center">
<Switch
id="pause-alert"
onChange={(value) => {
setValue('isPaused', value.currentTarget.checked);
}}
value={Boolean(isPaused)}
/>
<label htmlFor="pause-alert" className={styles.switchLabel}>
<Trans i18nKey="alert-rule-form.pause.label">Pause evaluation</Trans>
<Tooltip placement="top" content={pauseContentText} theme={'info'}>
<Icon tabIndex={0} name="info-circle" size="sm" className={styles.infoIcon} />
</Tooltip>
</label>
</Stack>
)}
name="isPaused"
/>
</Field>
)}
</Stack>
{isGrafanaAlertingRule && (
<>
<CollapseToggle
isCollapsed={!showErrorHandling}
onToggle={(collapsed) => setShowErrorHandling(!collapsed)}
text="Configure no data and error handling"
/>
{showErrorHandling && (
<>
<NeedHelpInfoForConfigureNoDataError />
<Field htmlFor="no-data-state-input" label="Alert state if no data or all values are null">
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
inputId="no-data-state-input"
width={42}
includeNoData={true}
includeError={false}
onChange={(value) => onChange(value?.value)}
/>
)}
name="noDataState"
/>
</Field>
<Field htmlFor="exec-err-state-input" label="Alert state if execution error or timeout">
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<GrafanaAlertStatePicker
{...field}
inputId="exec-err-state-input"
width={42}
includeNoData={false}
includeError={true}
onChange={(value) => onChange(value?.value)}
/>
)}
name="execErrState"
/>
</Field>
</>
)}
</>
)}
</RuleEditorSection>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
inlineField: css({
marginBottom: 0,
}),
evaluateLabel: css({
marginRight: theme.spacing(1),
}),
evaluationContainer: css({
color: theme.colors.text.secondary,
maxWidth: `${theme.breakpoints.values.sm}px`,
fontSize: theme.typography.size.sm,
}),
intervalChangedLabel: css({
marginBottom: theme.spacing(1),
}),
warningIcon: css({
justifySelf: 'center',
marginRight: theme.spacing(1),
color: theme.colors.warning.text,
}),
infoIcon: css({
marginLeft: '10px',
}),
warningMessage: css({
color: theme.colors.warning.text,
}),
bold: css({
fontWeight: 'bold',
}),
alignInterval: css({
marginTop: theme.spacing(1),
marginLeft: `-${theme.spacing(1)}`,
}),
marginTop: css({
marginTop: theme.spacing(1),
}),
@ -429,4 +711,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
cursor: 'pointer',
fontSize: theme.typography.bodySmall.fontSize,
}),
formInput: css({
flexGrow: 1,
}),
modal: css({
width: `${theme.breakpoints.values.sm}px`,
}),
modalTitle: css({
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
});

@ -0,0 +1,73 @@
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { Stack, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { KBObjectArray, RuleFormValues } from '../../types/rule-form';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { FolderSelector } from './FolderSelector';
import { NeedHelpInfo } from './NeedHelpInfo';
import { RuleEditorSection } from './RuleEditorSection';
import { LabelsEditorModal } from './labels/LabelsEditorModal';
import { LabelsFieldInForm } from './labels/LabelsFieldInForm';
/** Precondition: rule is Grafana managed.
*/
export function GrafanaFolderAndLabelsStep() {
const { setValue, getValues } = useFormContext<RuleFormValues>();
const [showLabelsEditor, setShowLabelsEditor] = useState(false);
function onCloseLabelsEditor(labelsToUpdate?: KBObjectArray) {
if (labelsToUpdate) {
setValue('labels', labelsToUpdate);
}
setShowLabelsEditor(false);
}
function SectionDescription() {
return (
<Stack direction="row" gap={0.5} alignItems="center">
<Text variant="bodySmall" color="secondary">
<Trans i18nKey="alerting.rule-form.folder-and-labels">
Organize your alert rule with a folder and set of labels.
</Trans>
</Text>
<NeedHelpInfo
contentText={
<>
<p>
{t(
'alerting.rule-form.folders.help-info',
'Folders are used for storing alert rules. You can extend the access provided by a role to alert rules and assign permissions to individual folders.'
)}
</p>
<p>
{t(
'alerting.rule-form.labels.help-info',
'Labels are used to differentiate an alert from all other alerts.You can use them for searching, silencing, and routing notifications.'
)}
</p>
</>
}
/>
</Stack>
);
}
return (
<RuleEditorSection stepNo={3} title="Add folder and labels" description={<SectionDescription />}>
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
<FolderSelector />
<LabelsFieldInForm onEditClick={() => setShowLabelsEditor(true)} />
<LabelsEditorModal
isOpen={showLabelsEditor}
onClose={onCloseLabelsEditor}
dataSourceName={GRAFANA_RULES_SOURCE_NAME}
initialLabels={getValues('labels')}
/>
</Stack>
</RuleEditorSection>
);
}

@ -8,9 +8,9 @@ import { Icon, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { KBObjectArray, RuleFormType, RuleFormValues } from '../../types/rule-form';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { isRecordingRuleByType } from '../../utils/rules';
import { isGrafanaManagedRuleByType, isGrafanaRecordingRuleByType, isRecordingRuleByType } from '../../utils/rules';
import { NeedHelpInfo } from './NeedHelpInfo';
import { RuleEditorSection } from './RuleEditorSection';
@ -45,6 +45,7 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
const [showLabelsEditor, setShowLabelsEditor] = useState(false);
const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME;
const isGrafanaManaged = isGrafanaManagedRuleByType(type);
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
const shouldRenderpreview = type === RuleFormType.grafana;
const hasInternalAlertmanagerEnabled = useHasInternalAlertmanagerEnabled();
@ -52,25 +53,29 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
const shouldAllowSimplifiedRouting =
type === RuleFormType.grafana && simplifiedRoutingToggleEnabled && hasInternalAlertmanagerEnabled;
function onCloseLabelsEditor(
labelsToUpdate?: Array<{
key: string;
value: string;
}>
) {
function onCloseLabelsEditor(labelsToUpdate?: KBObjectArray) {
if (labelsToUpdate) {
setValue('labels', labelsToUpdate);
}
setShowLabelsEditor(false);
}
if (!type) {
if (isGrafanaRecordingRuleByType(type)) {
return null;
}
const step = !isGrafanaManaged ? 4 : 5;
const title = isRecordingRuleByType(type)
? 'Add labels'
: isGrafanaManaged
? 'Configure notifications'
: 'Configure labels and notifications';
return (
<RuleEditorSection
stepNo={4}
title={isRecordingRuleByType(type) ? 'Add labels' : 'Configure labels and notifications'}
stepNo={step}
title={title}
description={
<Stack direction="row" gap={0.5} alignItems="center">
{isRecordingRuleByType(type) ? (
@ -88,13 +93,17 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
}
fullWidth
>
<LabelsFieldInForm onEditClick={() => setShowLabelsEditor(true)} />
<LabelsEditorModal
isOpen={showLabelsEditor}
onClose={onCloseLabelsEditor}
dataSourceName={dataSourceName}
initialLabels={getValues('labels')}
/>
{!isGrafanaManaged && (
<>
<LabelsFieldInForm onEditClick={() => setShowLabelsEditor(true)} />
<LabelsEditorModal
isOpen={showLabelsEditor}
onClose={onCloseLabelsEditor}
dataSourceName={dataSourceName}
initialLabels={getValues('labels')}
/>
</>
)}
{shouldAllowSimplifiedRouting && (
<div className={styles.configureNotifications}>
<Text element="h5">Notifications</Text>

@ -13,6 +13,8 @@ import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedR
import {
getRuleGroupLocationFromFormValues,
getRuleGroupLocationFromRuleWithLocation,
isCloudAlertingRuleByType,
isCloudRecordingRuleByType,
isCloudRulerRule,
isGrafanaManagedRuleByType,
isGrafanaRulerRule,
@ -60,7 +62,8 @@ import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
import AnnotationsStep from '../AnnotationsStep';
import { CloudEvaluationBehavior } from '../CloudEvaluationBehavior';
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
import { GrafanaEvaluationBehaviorStep } from '../GrafanaEvaluationBehavior';
import { GrafanaFolderAndLabelsStep } from '../GrafanaFolderAndLabelsStep';
import { NotificationsStep } from '../NotificationsStep';
import { RecordingRulesNameSpaceAndGroupStep } from '../RecordingRulesNameSpaceAndGroupStep';
import { RuleInspector } from '../RuleInspector';
@ -296,23 +299,24 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
{showDataSourceDependantStep && (
<>
{/* Step 3 */}
{isGrafanaManagedRuleByType(type) && <GrafanaFolderAndLabelsStep />}
{isCloudAlertingRuleByType(type) && <CloudEvaluationBehavior />}
{isCloudRecordingRuleByType(type) && <RecordingRulesNameSpaceAndGroupStep />}
{/* Step 4 & 5 & 6*/}
{isGrafanaManagedRuleByType(type) && (
<GrafanaEvaluationBehavior
<GrafanaEvaluationBehaviorStep
evaluateEvery={evaluateEvery}
setEvaluateEvery={setEvaluateEvery}
existing={Boolean(existing)}
enableProvisionedGroups={false}
/>
)}
{type === RuleFormType.cloudAlerting && <CloudEvaluationBehavior />}
{type === RuleFormType.cloudRecording && <RecordingRulesNameSpaceAndGroupStep />}
{/* Step 4 & 5 */}
{/* Notifications step*/}
<NotificationsStep alertUid={uidFromParams} />
{/* Annotations only for cloud and Grafana */}
{/* Annotations only for alerting rules */}
{!isRecordingRuleByType(type) && <AnnotationsStep />}
</>
)}

@ -24,7 +24,8 @@ import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
import { ExportFormats, allGrafanaExportProviders } from '../../export/providers';
import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
import AnnotationsStep from '../AnnotationsStep';
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
import { GrafanaEvaluationBehaviorStep } from '../GrafanaEvaluationBehavior';
import { GrafanaFolderAndLabelsStep } from '../GrafanaFolderAndLabelsStep';
import { NotificationsStep } from '../NotificationsStep';
import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
@ -90,15 +91,15 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
{/* Step 2 */}
<QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} />
{/* Step 3-4-5 */}
<GrafanaFolderAndLabelsStep />
<GrafanaEvaluationBehavior
{/* Step 4 & 5 */}
<GrafanaEvaluationBehaviorStep
evaluateEvery={evaluateEvery}
setEvaluateEvery={setEvaluateEvery}
existing={Boolean(existing)}
enableProvisionedGroups={true}
/>
{/* Step 4 & 5 */}
{/* Notifications step*/}
<NotificationsStep alertUid={alertUid} />
{/* Annotations only for cloud and Grafana */}

@ -1,5 +1,7 @@
import { Modal } from '@grafana/ui';
import { KBObjectArray } from '../../../types/rule-form';
import { LabelsSubForm } from './LabelsField';
export interface LabelsEditorModalProps {
@ -8,12 +10,7 @@ export interface LabelsEditorModalProps {
key: string;
value: string;
}>;
onClose: (
labelsToUodate?: Array<{
key: string;
value: string;
}>
) => void;
onClose: (labelsToUodate?: KBObjectArray) => void;
dataSourceName: string;
}
export function LabelsEditorModal({ isOpen, onClose, dataSourceName, initialLabels }: LabelsEditorModalProps) {

@ -9,7 +9,7 @@ import { t } from 'app/core/internationalization';
import { labelsApi } from '../../../api/labelsApi';
import { usePluginBridge } from '../../../hooks/usePluginBridge';
import { SupportedPlugin } from '../../../types/pluginBridges';
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
import { KBObjectArray, RuleFormType, RuleFormValues } from '../../../types/rule-form';
import { isPrivateLabelKey } from '../../../utils/labels';
import { isRecordingRuleByType } from '../../../utils/rules';
import AlertLabelDropdown from '../../AlertLabelDropdown';
@ -56,12 +56,7 @@ export type LabelsSubformValues = {
export interface LabelsSubFormProps {
dataSourceName: string;
initialLabels: Array<{ key: string; value: string }>;
onClose: (
labelsToUodate?: Array<{
key: string;
value: string;
}>
) => void;
onClose: (labelsToUodate?: KBObjectArray) => void;
}
export function LabelsSubForm({ dataSourceName, onClose, initialLabels }: LabelsSubFormProps) {

@ -8,16 +8,13 @@ import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { Stack } from 'app/plugins/datasource/parca/QueryEditor/Stack';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { Folder } from '../../../types/rule-form';
import { Folder, KBObjectArray } from '../../../types/rule-form';
import { useGetAlertManagerDataSourcesByPermissionAndConfig } from '../../../utils/datasource';
const NotificationPreviewByAlertManager = lazy(() => import('./NotificationPreviewByAlertManager'));
interface NotificationPreviewProps {
customLabels: Array<{
key: string;
value: string;
}>;
customLabels: KBObjectArray;
alertQueries: AlertQuery[];
condition: string | null;
folder?: Folder;

@ -14,7 +14,7 @@ import {
} from '../../mocks';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { EditCloudGroupModal } from './EditRuleGroupModal';
import { EditRuleGroupModal } from './EditRuleGroupModal';
const ui = {
input: {
@ -40,7 +40,7 @@ describe('EditGroupModal', () => {
const group = namespace.groups[0];
render(<EditCloudGroupModal namespace={namespace} group={group} intervalEditOnly onClose={noop} />);
render(<EditRuleGroupModal namespace={namespace} group={group} intervalEditOnly onClose={noop} />);
expect(await ui.input.namespace.find()).toHaveAttribute('readonly');
expect(ui.input.group.get()).toHaveAttribute('readonly');
@ -80,7 +80,7 @@ describe('EditGroupModal component on cloud alert rules', () => {
const group = promNs.groups[0];
render(<EditCloudGroupModal namespace={promNs} group={group} onClose={noop} />);
render(<EditRuleGroupModal namespace={promNs} group={group} onClose={noop} />);
expect(await ui.input.namespace.find()).toHaveValue('prometheus-ns');
expect(ui.input.namespace.get()).not.toHaveAttribute('readonly');
@ -99,7 +99,7 @@ describe('EditGroupModal component on cloud alert rules', () => {
const group = promNs.groups[0];
render(<EditCloudGroupModal namespace={promNs} group={group} onClose={noop} />);
render(<EditRuleGroupModal namespace={promNs} group={group} onClose={noop} />);
expect(ui.table.query()).not.toBeInTheDocument();
expect(await ui.noRulesText.find()).toBeInTheDocument();
});
@ -133,7 +133,7 @@ describe('EditGroupModal component on grafana-managed alert rules', () => {
const grafanaGroup1 = grafanaNamespace.groups[0];
const renderWithGrafanaGroup = () =>
render(<EditCloudGroupModal namespace={grafanaNamespace} group={grafanaGroup1} onClose={noop} />);
render(<EditRuleGroupModal namespace={grafanaNamespace} group={grafanaGroup1} onClose={noop} />);
it('Should show alert table', async () => {
renderWithGrafanaGroup();

@ -178,7 +178,7 @@ export interface ModalProps {
hideFolder?: boolean;
}
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
export function EditRuleGroupModal(props: ModalProps): React.ReactElement {
const { namespace, group, onClose, intervalEditOnly, folderUid } = props;
const styles = useStyles2(getStyles);

@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { Badge, ConfirmModal, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier, RulesSource } from 'app/types/unified-alerting';
import { LogMessages, logInfo } from '../../Analytics';
import { logInfo, LogMessages } from '../../Analytics';
import { featureDiscoveryApi } from '../../api/featureDiscoveryApi';
import { useDeleteRuleGroup } from '../../hooks/ruleGroup/useDeleteRuleGroup';
import { useFolder } from '../../hooks/useFolder';
@ -23,7 +23,7 @@ import { GrafanaRuleGroupExporter } from '../export/GrafanaRuleGroupExporter';
import { decodeGrafanaNamespace } from '../expressions/util';
import { ActionIcon } from './ActionIcon';
import { EditCloudGroupModal } from './EditRuleGroupModal';
import { EditRuleGroupModal } from './EditRuleGroupModal';
import { ReorderCloudGroupModal } from './ReorderRuleGroupModal';
import { RuleGroupStats } from './RuleStats';
import { RulesTable } from './RulesTable';
@ -275,7 +275,7 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
/>
)}
{isEditingGroup && (
<EditCloudGroupModal
<EditRuleGroupModal
namespace={namespace}
group={group}
onClose={() => closeEditModal()}

@ -27,6 +27,9 @@ export interface SimplifiedEditor {
simplifiedQueryEditor: boolean;
}
export type KVObject = { key: string; value: string };
export type KBObjectArray = KVObject[];
export interface RuleFormValues {
// common
name: string;

@ -40,12 +40,11 @@ import {
RulerRuleDTO,
} from 'app/types/unified-alerting-dto';
type KVObject = { key: string; value: string };
import { EvalFunction } from '../../state/alertDef';
import {
AlertManagerManualRouting,
ContactPoint,
KVObject,
RuleFormType,
RuleFormValues,
SimplifiedEditor,

@ -417,7 +417,7 @@ export function isGrafanaAlertingRuleByType(type?: RuleFormType) {
return type === RuleFormType.grafana;
}
export function isGrafanaRecordingRuleByType(type: RuleFormType) {
export function isGrafanaRecordingRuleByType(type?: RuleFormType) {
return type === RuleFormType.grafanaRecording;
}
@ -429,14 +429,14 @@ export function isCloudRecordingRuleByType(type?: RuleFormType) {
return type === RuleFormType.cloudRecording;
}
export function isGrafanaManagedRuleByType(type: RuleFormType) {
export function isGrafanaManagedRuleByType(type?: RuleFormType) {
return isGrafanaAlertingRuleByType(type) || isGrafanaRecordingRuleByType(type);
}
export function isRecordingRuleByType(type: RuleFormType) {
export function isRecordingRuleByType(type?: RuleFormType) {
return isGrafanaRecordingRuleByType(type) || isCloudRecordingRuleByType(type);
}
export function isDataSourceManagedRuleByType(type: RuleFormType) {
export function isDataSourceManagedRuleByType(type?: RuleFormType) {
return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type);
}

@ -33,29 +33,6 @@
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"info-help": {
"text": "Define the alert behavior when the evaluation fails or the query returns no data."
},
"pending-period": "Pending period"
},
"evaluation-behaviour-description1": "Evaluation groups are containers for evaluating alert and recording rules.",
"evaluation-behaviour-description2": "An evaluation group defines an evaluation interval - how often a rule is evaluated. Alert rules within the same evaluation group are evaluated over the same evaluation interval.",
"evaluation-behaviour-description3": "Pending period specifies how long the threshold condition must be met before the alert starts firing. This option helps prevent alerts from being triggered by temporary issues.",
"evaluation-behaviour-for": {
"error-parsing": "Failed to parse duration",
"validation": "Pending period must be greater than or equal to the evaluation interval."
},
"evaluation-behaviour-group": {
"text": "All rules in the selected group are evaluated every {{evaluateEvery}}."
},
"pause": {
"alerting": "Turn on to pause evaluation for this alert rule.",
"label": "Pause evaluation",
"recording": "Turn on to pause evaluation for this recording rule."
}
},
"alerting": {
"alert-recording-rule-form": {
"evaluation-behaviour": {
@ -64,13 +41,6 @@
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
"text": "Define how the alert rule is evaluated."
}
}
},
"alert-rules": {
"firing-for": "Firing for",
"next-evaluation": "Next evaluation",
@ -177,10 +147,6 @@
"alerting": "Create a new evaluation group to use for this alert rule.",
"recording": "Create a new evaluation group to use for this recording rule."
}
},
"text": {
"alerting": "Define how often the alert rule is evaluated.",
"recording": "Define how often the recording rule is evaluated."
}
}
},
@ -266,6 +232,60 @@
"preview": "Preview",
"previewCondition": "Preview alert rule condition"
},
"rule-form": {
"evaluation": {
"evaluation-group-and-interval": "Evaluation group and interval",
"group": {
"cancel": "Cancel",
"create": "Create",
"interval": "Evaluation interval"
},
"group-name": "Evaluation group name",
"group-text": "All rules in the selected group are evaluated every {{evaluateEvery}}.",
"new-group": "New evaluation group",
"pause": {
"alerting": "Turn on to pause evaluation for this alert rule.",
"recording": "Turn on to pause evaluation for this recording rule."
},
"select-folder-before": "Select a folder before setting evaluation group and interval"
},
"evaluation-behaviour": {
"description": {
"text": "Define how the alert rule is evaluated."
},
"info-help": {
"text": "Define the alert behavior when the evaluation fails or the query returns no data."
},
"pending-period": "Pending period"
},
"evaluation-behaviour-description1": "Evaluation groups are containers for evaluating alert and recording rules.",
"evaluation-behaviour-description2": "An evaluation group defines an evaluation interval - how often a rule is evaluated. Alert rules within the same evaluation group are evaluated over the same evaluation interval.",
"evaluation-behaviour-description3": "Pending period specifies how long the threshold condition must be met before the alert starts firing. This option helps prevent alerts from being triggered by temporary issues.",
"evaluation-behaviour-for": {
"error-parsing": "Failed to parse duration",
"validation": "Pending period must be greater than or equal to the evaluation interval."
},
"folder": {
"cancel": "Cancel",
"create": "Create",
"create-folder": "Create a new folder to store your alert rule in.",
"creating-new-folder": "Creating new folder",
"label": "Folder",
"name": "Folder name",
"new-folder": "New folder",
"new-folder-or": "or"
},
"folder-and-labels": "Organize your alert rule with a folder and set of labels.",
"folders": {
"help-info": "Folders are used for storing alert rules. You can extend the access provided by a role to alert rules and assign permissions to individual folders."
},
"labels": {
"help-info": "Labels are used to differentiate an alert from all other alerts.You can use them for searching, silencing, and routing notifications."
},
"pause": {
"label": "Pause evaluation"
}
},
"rule-groups": {
"delete": {
"success": "Successfully deleted rule group"

@ -33,29 +33,6 @@
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"info-help": {
"text": "Đęƒįʼnę ŧĥę äľęřŧ þęĥävįőř ŵĥęʼn ŧĥę ęväľūäŧįőʼn ƒäįľş őř ŧĥę qūęřy řęŧūřʼnş ʼnő đäŧä."
},
"pending-period": "Pęʼnđįʼnģ pęřįőđ"
},
"evaluation-behaviour-description1": "Ēväľūäŧįőʼn ģřőūpş äřę čőʼnŧäįʼnęřş ƒőř ęväľūäŧįʼnģ äľęřŧ äʼnđ řęčőřđįʼnģ řūľęş.",
"evaluation-behaviour-description2": "Åʼn ęväľūäŧįőʼn ģřőūp đęƒįʼnęş äʼn ęväľūäŧįőʼn įʼnŧęřväľ - ĥőŵ őƒŧęʼn ä řūľę įş ęväľūäŧęđ. Åľęřŧ řūľęş ŵįŧĥįʼn ŧĥę şämę ęväľūäŧįőʼn ģřőūp äřę ęväľūäŧęđ ővęř ŧĥę şämę ęväľūäŧįőʼn įʼnŧęřväľ.",
"evaluation-behaviour-description3": "Pęʼnđįʼnģ pęřįőđ şpęčįƒįęş ĥőŵ ľőʼnģ ŧĥę ŧĥřęşĥőľđ čőʼnđįŧįőʼn mūşŧ þę męŧ þęƒőřę ŧĥę äľęřŧ şŧäřŧş ƒįřįʼnģ. Ŧĥįş őpŧįőʼn ĥęľpş přęvęʼnŧ äľęřŧş ƒřőm þęįʼnģ ŧřįģģęřęđ þy ŧęmpőřäřy įşşūęş.",
"evaluation-behaviour-for": {
"error-parsing": "Fäįľęđ ŧő päřşę đūřäŧįőʼn",
"validation": "Pęʼnđįʼnģ pęřįőđ mūşŧ þę ģřęäŧęř ŧĥäʼn őř ęqūäľ ŧő ŧĥę ęväľūäŧįőʼn įʼnŧęřväľ."
},
"evaluation-behaviour-group": {
"text": "Åľľ řūľęş įʼn ŧĥę şęľęčŧęđ ģřőūp äřę ęväľūäŧęđ ęvęřy {{evaluateEvery}}."
},
"pause": {
"alerting": "Ŧūřʼn őʼn ŧő päūşę ęväľūäŧįőʼn ƒőř ŧĥįş äľęřŧ řūľę.",
"label": "Päūşę ęväľūäŧįőʼn",
"recording": "Ŧūřʼn őʼn ŧő päūşę ęväľūäŧįőʼn ƒőř ŧĥįş řęčőřđįʼnģ řūľę."
}
},
"alerting": {
"alert-recording-rule-form": {
"evaluation-behaviour": {
@ -64,13 +41,6 @@
}
}
},
"alert-rule-form": {
"evaluation-behaviour": {
"description": {
"text": "Đęƒįʼnę ĥőŵ ŧĥę äľęřŧ řūľę įş ęväľūäŧęđ."
}
}
},
"alert-rules": {
"firing-for": "Fįřįʼnģ ƒőř",
"next-evaluation": "Ńęχŧ ęväľūäŧįőʼn",
@ -177,10 +147,6 @@
"alerting": "Cřęäŧę ä ʼnęŵ ęväľūäŧįőʼn ģřőūp ŧő ūşę ƒőř ŧĥįş äľęřŧ řūľę.",
"recording": "Cřęäŧę ä ʼnęŵ ęväľūäŧįőʼn ģřőūp ŧő ūşę ƒőř ŧĥįş řęčőřđįʼnģ řūľę."
}
},
"text": {
"alerting": "Đęƒįʼnę ĥőŵ őƒŧęʼn ŧĥę äľęřŧ řūľę įş ęväľūäŧęđ.",
"recording": "Đęƒįʼnę ĥőŵ őƒŧęʼn ŧĥę řęčőřđįʼnģ řūľę įş ęväľūäŧęđ."
}
}
},
@ -266,6 +232,60 @@
"preview": "Přęvįęŵ",
"previewCondition": "Přęvįęŵ äľęřŧ řūľę čőʼnđįŧįőʼn"
},
"rule-form": {
"evaluation": {
"evaluation-group-and-interval": "Ēväľūäŧįőʼn ģřőūp äʼnđ įʼnŧęřväľ",
"group": {
"cancel": "Cäʼnčęľ",
"create": "Cřęäŧę",
"interval": "Ēväľūäŧįőʼn įʼnŧęřväľ"
},
"group-name": "Ēväľūäŧįőʼn ģřőūp ʼnämę",
"group-text": "Åľľ řūľęş įʼn ŧĥę şęľęčŧęđ ģřőūp äřę ęväľūäŧęđ ęvęřy {{evaluateEvery}}.",
"new-group": "Ńęŵ ęväľūäŧįőʼn ģřőūp",
"pause": {
"alerting": "Ŧūřʼn őʼn ŧő päūşę ęväľūäŧįőʼn ƒőř ŧĥįş äľęřŧ řūľę.",
"recording": "Ŧūřʼn őʼn ŧő päūşę ęväľūäŧįőʼn ƒőř ŧĥįş řęčőřđįʼnģ řūľę."
},
"select-folder-before": "Ŝęľęčŧ ä ƒőľđęř þęƒőřę şęŧŧįʼnģ ęväľūäŧįőʼn ģřőūp äʼnđ įʼnŧęřväľ"
},
"evaluation-behaviour": {
"description": {
"text": "Đęƒįʼnę ĥőŵ ŧĥę äľęřŧ řūľę įş ęväľūäŧęđ."
},
"info-help": {
"text": "Đęƒįʼnę ŧĥę äľęřŧ þęĥävįőř ŵĥęʼn ŧĥę ęväľūäŧįőʼn ƒäįľş őř ŧĥę qūęřy řęŧūřʼnş ʼnő đäŧä."
},
"pending-period": "Pęʼnđįʼnģ pęřįőđ"
},
"evaluation-behaviour-description1": "Ēväľūäŧįőʼn ģřőūpş äřę čőʼnŧäįʼnęřş ƒőř ęväľūäŧįʼnģ äľęřŧ äʼnđ řęčőřđįʼnģ řūľęş.",
"evaluation-behaviour-description2": "Åʼn ęväľūäŧįőʼn ģřőūp đęƒįʼnęş äʼn ęväľūäŧįőʼn įʼnŧęřväľ - ĥőŵ őƒŧęʼn ä řūľę įş ęväľūäŧęđ. Åľęřŧ řūľęş ŵįŧĥįʼn ŧĥę şämę ęväľūäŧįőʼn ģřőūp äřę ęväľūäŧęđ ővęř ŧĥę şämę ęväľūäŧįőʼn įʼnŧęřväľ.",
"evaluation-behaviour-description3": "Pęʼnđįʼnģ pęřįőđ şpęčįƒįęş ĥőŵ ľőʼnģ ŧĥę ŧĥřęşĥőľđ čőʼnđįŧįőʼn mūşŧ þę męŧ þęƒőřę ŧĥę äľęřŧ şŧäřŧş ƒįřįʼnģ. Ŧĥįş őpŧįőʼn ĥęľpş přęvęʼnŧ äľęřŧş ƒřőm þęįʼnģ ŧřįģģęřęđ þy ŧęmpőřäřy įşşūęş.",
"evaluation-behaviour-for": {
"error-parsing": "Fäįľęđ ŧő päřşę đūřäŧįőʼn",
"validation": "Pęʼnđįʼnģ pęřįőđ mūşŧ þę ģřęäŧęř ŧĥäʼn őř ęqūäľ ŧő ŧĥę ęväľūäŧįőʼn įʼnŧęřväľ."
},
"folder": {
"cancel": "Cäʼnčęľ",
"create": "Cřęäŧę",
"create-folder": "Cřęäŧę ä ʼnęŵ ƒőľđęř ŧő şŧőřę yőūř äľęřŧ řūľę įʼn.",
"creating-new-folder": "Cřęäŧįʼnģ ʼnęŵ ƒőľđęř",
"label": "Főľđęř",
"name": "Főľđęř ʼnämę",
"new-folder": "Ńęŵ ƒőľđęř",
"new-folder-or": "őř"
},
"folder-and-labels": "Øřģäʼnįžę yőūř äľęřŧ řūľę ŵįŧĥ ä ƒőľđęř äʼnđ şęŧ őƒ ľäþęľş.",
"folders": {
"help-info": "Főľđęřş äřę ūşęđ ƒőř şŧőřįʼnģ äľęřŧ řūľęş. Ÿőū čäʼn ęχŧęʼnđ ŧĥę äččęşş přővįđęđ þy ä řőľę ŧő äľęřŧ řūľęş äʼnđ äşşįģʼn pęřmįşşįőʼnş ŧő įʼnđįvįđūäľ ƒőľđęřş."
},
"labels": {
"help-info": "Ŀäþęľş äřę ūşęđ ŧő đįƒƒęřęʼnŧįäŧę äʼn äľęřŧ ƒřőm äľľ őŧĥęř äľęřŧş.Ÿőū čäʼn ūşę ŧĥęm ƒőř şęäřčĥįʼnģ, şįľęʼnčįʼnģ, äʼnđ řőūŧįʼnģ ʼnőŧįƒįčäŧįőʼnş."
},
"pause": {
"label": "Päūşę ęväľūäŧįőʼn"
}
},
"rule-groups": {
"delete": {
"success": "Ŝūččęşşƒūľľy đęľęŧęđ řūľę ģřőūp"

Loading…
Cancel
Save