mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
d3a000e7da
commit
1b0cc60d65
@ -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 { 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 { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
||||
import { LogMessages, logInfo } from '../../Analytics';
|
||||
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,204 +219,166 @@ 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: {} };
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
|
||||
|
||||
function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const evaluateForId = 'eval-for-input';
|
||||
const currentPendingPeriod = watch('evaluateFor');
|
||||
|
||||
const setPendingPeriod = (pendingPeriod: string) => {
|
||||
setValue('evaluateFor', pendingPeriod);
|
||||
const handleEvalGroupCreation = (groupName: string, evaluationInterval: string) => {
|
||||
setValue('group', groupName);
|
||||
setValue('evaluateEvery', evaluationInterval);
|
||||
setIsCreatingEvaluationGroup(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||
<Field
|
||||
label={
|
||||
<Label
|
||||
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>
|
||||
</Label>
|
||||
}
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateFor?.message}
|
||||
invalid={Boolean(errors.evaluateFor?.message) ? true : undefined}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
|
||||
</Field>
|
||||
<PendingPeriodQuickPick
|
||||
selectedPendingPeriod={currentPendingPeriod}
|
||||
groupEvaluationInterval={evaluateEvery}
|
||||
onSelect={setPendingPeriod}
|
||||
/>
|
||||
</Stack>
|
||||
const getOptions = useCallback(
|
||||
async (query: string) => {
|
||||
const results = query ? groupOptions.filter((group) => findGroupMatchingLabel(group, query)) : groupOptions;
|
||||
return take(results, MAX_GROUP_RESULTS);
|
||||
},
|
||||
[groupOptions]
|
||||
);
|
||||
}
|
||||
|
||||
function NeedHelpInfoForConfigureNoDataError() {
|
||||
const docsLink =
|
||||
'https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling';
|
||||
const debouncedSearch = useMemo(() => {
|
||||
return debounce(getOptions, 300, { leading: true });
|
||||
}, [getOptions]);
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<Trans i18nKey="alert-rule-form.evaluation-behaviour.info-help.text">
|
||||
Define the alert behavior when the evaluation fails or the query returns no data.
|
||||
</Trans>
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText="These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts."
|
||||
externalLink={docsLink}
|
||||
linkText={`Read more about this option`}
|
||||
title="Configure no data and error handling"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getDescription(isGrafanaRecordingRule: boolean) {
|
||||
const docsLink = 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/';
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{isGrafanaRecordingRule ? (
|
||||
<Trans i18nKey="alerting.alert-recording-rule-form.evaluation-behaviour.description.text">
|
||||
Define how the recording rule is evaluated.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="alerting.alert-rule-form.evaluation-behaviour.description.text">
|
||||
Define how the alert rule is evaluated.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText={
|
||||
<>
|
||||
<p>
|
||||
<Trans i18nKey="alert-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">
|
||||
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">
|
||||
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>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
externalLink={docsLink}
|
||||
linkText={`Read about evaluation and alert states`}
|
||||
title="Alert rule evaluation"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
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 defaultGroupValue = group ? { value: group, label: group } : undefined;
|
||||
|
||||
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.');
|
||||
? 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 (
|
||||
// TODO remove "and alert condition" for recording rules
|
||||
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription(isGrafanaRecordingRule)}>
|
||||
<RuleEditorSection
|
||||
stepNo={step}
|
||||
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}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
placeholder={'Select an evaluation group...'}
|
||||
/>
|
||||
)}
|
||||
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} />}
|
||||
|
||||
@ -328,7 +395,7 @@ export function GrafanaEvaluationBehavior({
|
||||
value={Boolean(isPaused)}
|
||||
/>
|
||||
<label htmlFor="pause-alert" className={styles.switchLabel}>
|
||||
<Trans i18nKey="alert-rule-form.pause.label">Pause evaluation</Trans>
|
||||
<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>
|
||||
@ -388,39 +455,254 @@ export function GrafanaEvaluationBehavior({
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const evaluateForId = 'eval-for-input';
|
||||
const currentPendingPeriod = watch('evaluateFor');
|
||||
|
||||
const setPendingPeriod = (pendingPeriod: string) => {
|
||||
setValue('evaluateFor', pendingPeriod);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||
<Field
|
||||
label={
|
||||
<Label
|
||||
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="alerting.rule-form.evaluation-behaviour.pending-period">Pending period</Trans>
|
||||
</Label>
|
||||
}
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateFor?.message}
|
||||
invalid={Boolean(errors.evaluateFor?.message) ? true : undefined}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input id={evaluateForId} width={8} {...register('evaluateFor', forValidationOptions(evaluateEvery))} />
|
||||
</Field>
|
||||
<PendingPeriodQuickPick
|
||||
selectedPendingPeriod={currentPendingPeriod}
|
||||
groupEvaluationInterval={evaluateEvery}
|
||||
onSelect={setPendingPeriod}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function NeedHelpInfoForConfigureNoDataError() {
|
||||
const docsLink =
|
||||
'https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling';
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<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>
|
||||
<NeedHelpInfo
|
||||
contentText="These settings can help mitigate temporary data source issues, preventing alerts from unintentionally firing due to lack of data, errors, or timeouts."
|
||||
externalLink={docsLink}
|
||||
linkText={`Read more about this option`}
|
||||
title="Configure no data and error handling"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getDescription(isGrafanaRecordingRule: boolean) {
|
||||
const docsLink = 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/';
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{isGrafanaRecordingRule ? (
|
||||
<Trans i18nKey="alerting.alert-recording-rule-form.evaluation-behaviour.description.text">
|
||||
Define how the recording rule is evaluated.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="alerting.rule-form.evaluation-behaviour.description.text">
|
||||
Define how the alert rule is evaluated.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText={
|
||||
<>
|
||||
<p>
|
||||
<Trans i18nKey="alerting.rule-form.evaluation-behaviour-description1">
|
||||
Evaluation groups are containers for evaluating alert and recording rules.
|
||||
</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<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="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>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
externalLink={docsLink}
|
||||
linkText={`Read about evaluation and alert states`}
|
||||
title="Alert rule evaluation"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
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…
Reference in New Issue
Block a user