From 1b0cc60d65e644cddb5c87dfb11de54c8657a79d Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:26:49 +0100 Subject: [PATCH] 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 --- .betterer.results | 17 +- .../rule-editor/AnnotationsStep.tsx | 6 +- .../components/rule-editor/FolderAndGroup.tsx | 489 ------------ .../components/rule-editor/FolderSelector.tsx | 186 +++++ .../rule-editor/GrafanaEvaluationBehavior.tsx | 730 ++++++++++++------ .../GrafanaFolderAndLabelsStep.tsx | 73 ++ .../rule-editor/NotificationsStep.tsx | 45 +- .../alert-rule-form/AlertRuleForm.tsx | 22 +- .../alert-rule-form/ModifyExportRuleForm.tsx | 9 +- .../rule-editor/labels/LabelsEditorModal.tsx | 9 +- .../rule-editor/labels/LabelsField.tsx | 9 +- .../NotificationPreview.tsx | 7 +- .../rules/EditRuleGroupModal.test.tsx | 10 +- .../components/rules/EditRuleGroupModal.tsx | 2 +- .../unified/components/rules/RulesGroup.tsx | 6 +- .../alerting/unified/types/rule-form.ts | 3 + .../alerting/unified/utils/rule-form.ts | 3 +- .../features/alerting/unified/utils/rules.ts | 8 +- public/locales/en-US/grafana.json | 88 ++- public/locales/pseudo-LOCALE/grafana.json | 88 ++- 20 files changed, 954 insertions(+), 856 deletions(-) delete mode 100644 public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx diff --git a/.betterer.results b/.betterer.results index ff134e57eaa..152968f9869 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1615,21 +1615,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "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 ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "13"] + "public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx index 31c3d1318d3..52039ffc562 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx @@ -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(); const annotations = watch('annotations'); + const type = watch('type'); const { fields, append, remove } = useFieldArray({ control, name: 'annotations' }); @@ -104,10 +106,12 @@ const AnnotationsStep = () => { ); } + // 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 ( { - // 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>((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, b: SelectableValue) => { - return a.label?.localeCompare(b.label ?? '') || 0; -}; - -const findGroupMatchingLabel = (group: SelectableValue, 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(); - - 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 ( -
- - { - - Folder - - } - className={styles.formInput} - error={errors.folder?.message} - data-testid="folder-picker" - > - - {(!isCreatingFolder && ( - <> - ( -
- { - if (uid && title) { - setValue('folder', { title, uid }); - } else { - setValue('folder', undefined); - } - - resetGroup(); - }} - /> -
- )} - name="folder" - rules={{ - required: { value: true, message: 'Select a folder' }, - }} - /> - or - - - )) ||
Creating new folder...
} -
-
- } - {isCreatingFolder && ( - setIsCreatingFolder(false)} /> - )} -
- - -
- - ( - { - 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) => ( -
- {option.label} - {option.isProvisioned && ( - <> - {' '} - - - )} -
- )} - placeholder={'Select an evaluation group...'} - /> - )} - name="group" - control={control} - rules={{ - required: { value: true, message: 'Must enter a group name' }, - }} - /> -
-
- - or - - - {isCreatingEvaluationGroup && ( - setIsCreatingEvaluationGroup(false)} - groupfoldersForGrafana={groupfoldersForGrafana} - /> - )} -
-
- ); -} - -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 ( - -
Create a new folder to store your rule
- -
- Folder name}> - setTitle(e.currentTarget.value)} - className={styles.formInput} - /> - - - - - - -
-
- ); -} - -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(); - - 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 ( - -
{modalTitle}
- - -
onSubmit())}> - - Evaluation group name - - } - error={formState.errors.group?.message} - invalid={Boolean(formState.errors.group)} - > - - - - - Evaluation interval - - } - invalid={Boolean(formState.errors.evaluateEvery)} - > - (groupRules) - )} - /> - - - - - - - - - -
-
- ); -} - -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), - }), -}); diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx new file mode 100644 index 00000000000..e81d18e251c --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx @@ -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(); + + 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 ( + <> + + { + + Folder + + } + error={errors.folder?.message} + data-testid="folder-picker" + > + + {(!isCreatingFolder && ( + <> + ( +
+ { + if (uid && title) { + setValue('folder', { title, uid }); + } else { + setValue('folder', undefined); + } + + resetGroup(); + }} + /> +
+ )} + name="folder" + rules={{ + required: { value: true, message: 'Select a folder' }, + }} + /> + + or + + + + )) || ( +
+ Creating new folder + {'...'} +
+ )} +
+
+ } +
+ + {isCreatingFolder && ( + 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 ( + + + + + Create a new folder to store your alert rule in. + + + +
+ + Folder name + + } + > + setTitle(e.currentTarget.value)} + /> + + + + + + +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + modal: css({ + width: `${theme.breakpoints.values.sm}px`, + }), +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 24f13f06bbc..95612dc75f7 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -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>((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, b: SelectableValue) => { + return a.label?.localeCompare(b.label ?? '') || 0; +}; + +const findGroupMatchingLabel = (group: SelectableValue, 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(); - 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(); + + 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 ( -
- - {folderName && isEditingGroup && ( - closeEditGroupModal()} - intervalEditOnly - hideFolder={true} - /> - )} - {folderName && groupName && ( -
- -
- - {getValues('group') && getValues('evaluateEvery') && ( - - - All rules in the selected group are evaluated every {{ evaluateEvery }}. - - {!isNewGroup && ( - - )} - - )} - -
-
-
- )} -
- ); -} + const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false); -function ForInput({ evaluateEvery }: { evaluateEvery: string }) { - const styles = useStyles2(getStyles); - const { - register, - formState: { errors }, - setValue, - watch, - } = useFormContext(); - - 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 ( - - - Pending period - - } - className={styles.inlineField} - error={errors.evaluateFor?.message} - invalid={Boolean(errors.evaluateFor?.message) ? true : undefined} - validationMessageHorizontalOverflow={true} - > - - - - + 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 ( - - - - Define the alert behavior when the evaluation fails or the query returns no data. - - - - - ); -} - -function getDescription(isGrafanaRecordingRule: boolean) { - const docsLink = 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/'; - - return ( - - - {isGrafanaRecordingRule ? ( - - Define how the recording rule is evaluated. - - ) : ( - - Define how the alert rule is evaluated. - - )} - - -

- - Evaluation groups are containers for evaluating alert and recording rules. - -

-

- - 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. - -

-

- - 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. - -

- - } - externalLink={docsLink} - linkText={`Read about evaluation and alert states`} - title="Alert rule evaluation" - /> -
- ); -} - -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(); - - 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 - + - + +
+ + ( + { + 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) => ( +
+ {option.label} + {option.isProvisioned && ( + <> + {' '} + + + )} +
+ )} + placeholder={'Select an evaluation group...'} + /> + )} + name="group" + control={control} + rules={{ + required: { value: true, message: 'Must enter a group name' }, + }} + /> +
+
+ + or + + + {isCreatingEvaluationGroup && ( + setIsCreatingEvaluationGroup(false)} + groupfoldersForGrafana={groupfoldersForGrafana?.result} + /> + )} +
+ + {folderName && isEditingGroup && ( + closeEditGroupModal()} + intervalEditOnly + hideFolder={true} + /> + )} + {folderName && group && ( +
+ +
+ + {getValues('group') && getValues('evaluateEvery') && ( + + + All rules in the selected group are evaluated every {{ evaluateEvery }}. + + {!isNewGroup && ( + + )} + + )} + +
+
+
+ )} {/* Show the pending period input only for Grafana alerting rules */} {isGrafanaAlertingRule && } @@ -328,7 +395,7 @@ export function GrafanaEvaluationBehavior({ value={Boolean(isPaused)} /> + } + className={styles.inlineField} + error={errors.evaluateFor?.message} + invalid={Boolean(errors.evaluateFor?.message) ? true : undefined} + validationMessageHorizontalOverflow={true} + > + + + +
+ ); +} + +function NeedHelpInfoForConfigureNoDataError() { + const docsLink = + 'https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling'; + + return ( + + + + Define the alert behavior when the evaluation fails or the query returns no data. + + + + + ); +} + +function getDescription(isGrafanaRecordingRule: boolean) { + const docsLink = 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/'; + + return ( + + + {isGrafanaRecordingRule ? ( + + Define how the recording rule is evaluated. + + ) : ( + + Define how the alert rule is evaluated. + + )} + + +

+ + Evaluation groups are containers for evaluating alert and recording rules. + +

+

+ + 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. + +

+

+ + 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. + +

+ + } + externalLink={docsLink} + linkText={`Read about evaluation and alert states`} + title="Alert rule evaluation" + /> +
+ ); +} + 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), + }), }); diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx new file mode 100644 index 00000000000..e91ec0cb697 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaFolderAndLabelsStep.tsx @@ -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(); + const [showLabelsEditor, setShowLabelsEditor] = useState(false); + + function onCloseLabelsEditor(labelsToUpdate?: KBObjectArray) { + if (labelsToUpdate) { + setValue('labels', labelsToUpdate); + } + setShowLabelsEditor(false); + } + + function SectionDescription() { + return ( + + + + Organize your alert rule with a folder and set of labels. + + + +

+ {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.' + )} +

+

+ {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.' + )} +

+ + } + /> +
+ ); + } + + return ( + }> + + + setShowLabelsEditor(true)} /> + + + + ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index d52f12fd042..5e63b3afed5 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -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 ( {isRecordingRuleByType(type) ? ( @@ -88,13 +93,17 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { } fullWidth > - setShowLabelsEditor(true)} /> - + {!isGrafanaManaged && ( + <> + setShowLabelsEditor(true)} /> + + + )} {shouldAllowSimplifiedRouting && (
Notifications diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 447f538136e..30f21ce9017 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -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) && } + + {isCloudAlertingRuleByType(type) && } + + {isCloudRecordingRuleByType(type) && } + + {/* Step 4 & 5 & 6*/} {isGrafanaManagedRuleByType(type) && ( - )} - - {type === RuleFormType.cloudAlerting && } - - {type === RuleFormType.cloudRecording && } - - {/* Step 4 & 5 */} {/* Notifications step*/} - {/* Annotations only for cloud and Grafana */} + {/* Annotations only for alerting rules */} {!isRecordingRuleByType(type) && } )} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index e35f6da5cf2..a4bf8e6b4ba 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -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 */} {/* Step 3-4-5 */} + - - - {/* Step 4 & 5 */} {/* Notifications step*/} {/* Annotations only for cloud and Grafana */} diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx index a98ad871b49..e6472fa31cb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsEditorModal.tsx @@ -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) { diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx index 604f7f69157..1335580837d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx @@ -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) { diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx index 8ad79d2fe9a..5f42608231a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx @@ -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; diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx index 65c65e07685..dcbc646ae03 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx @@ -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(); + render(); 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(); + render(); 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(); + render(); 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(); + render(); it('Should show alert table', async () => { renderWithGrafanaGroup(); diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx index 2ff9d57baaf..f6c4a116005 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx @@ -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); diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index f8a1f6d7431..5987704636b 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -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 && ( - closeEditModal()} diff --git a/public/app/features/alerting/unified/types/rule-form.ts b/public/app/features/alerting/unified/types/rule-form.ts index 1ff9c721e87..8314961ba5d 100644 --- a/public/app/features/alerting/unified/types/rule-form.ts +++ b/public/app/features/alerting/unified/types/rule-form.ts @@ -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; diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 2154b66e026..f3be44d152b 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -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, diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index fd372acdda4..ad6ffc15a3f 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -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); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6ddc36f82b7..e2fb153b789 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index b24be9678e2..a471dce121f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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"