diff --git a/.betterer.results b/.betterer.results index 055f09fc45a..09bf2286471 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3453,9 +3453,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rule-editor/AnnotationKeyInput.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/alerting/unified/components/rule-editor/AnnotationsField.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.tsx index 1fce0edf88e..aea0da4ecb8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationsField.tsx @@ -1,9 +1,9 @@ import { css, cx } from '@emotion/css'; import React, { FC, useCallback } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFieldArray, useFormContext } from 'react-hook-form'; import { GrafanaTheme } from '@grafana/data'; -import { Button, Field, FieldArray, Input, InputControl, Label, TextArea, useStyles } from '@grafana/ui'; +import { Button, Field, Input, InputControl, Label, TextArea, useStyles } from '@grafana/ui'; import { RuleFormValues } from '../../types/rule-form'; @@ -16,85 +16,83 @@ const AnnotationsField: FC = () => { register, watch, formState: { errors }, - } = useFormContext(); - const annotations = watch('annotations') as RuleFormValues['annotations']; + } = useFormContext(); + const annotations = watch('annotations'); const existingKeys = useCallback( (index: number): string[] => annotations.filter((_, idx: number) => idx !== index).map(({ key }) => key), [annotations] ); + const { fields, append, remove } = useFieldArray({ control, name: 'annotations' }); + return ( <> - - {({ fields, append, remove }) => { +
+ {fields.map((annotationField, index) => { + const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url'); + const ValueInputComponent = isUrl ? Input : TextArea; + return ( -
- {fields.map((field, index) => { - const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url'); - const ValueInputComponent = isUrl ? Input : TextArea; - return ( -
- - ( - - )} - control={control} - rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }} - /> - - - - -
- ); - })} - + ( + + )} + control={control} + rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }} + /> + + + + +
); - }} - + })} + +
); }; diff --git a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx index e17ce8b6c6a..9427778a4fd 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx @@ -1,5 +1,5 @@ import { noop } from 'lodash'; -import React, { FC, useCallback, useMemo, useState } from 'react'; +import React, { FC, useCallback, useMemo } from 'react'; import { useAsync } from 'react-use'; import { CoreApp, DataQuery } from '@grafana/data'; @@ -15,7 +15,8 @@ export interface ExpressionEditorProps { export const ExpressionEditor: FC = ({ value, onChange, dataSourceName }) => { const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName); - const [query, setQuery] = useState(mapToQuery({ refId: 'A', hide: false }, value)); + const query = mapToQuery({ refId: 'A', hide: false }, value); + const { error, loading, @@ -26,7 +27,6 @@ export const ExpressionEditor: FC = ({ value, onChange, d const onChangeQuery = useCallback( (query: DataQuery) => { - setQuery(query); onChange(mapToValue(query)); }, [onChange, mapToValue] diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx index 9c209fc9235..7d4d96d4943 100644 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx @@ -1,9 +1,11 @@ import { css, cx } from '@emotion/css'; import React, { FC } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFieldArray, useFormContext } from 'react-hook-form'; import { GrafanaTheme } from '@grafana/data'; -import { Button, Field, FieldArray, Input, InlineLabel, Label, useStyles } from '@grafana/ui'; +import { Button, Field, Input, InlineLabel, Label, useStyles } from '@grafana/ui'; + +import { RuleFormValues } from '../../types/rule-form'; interface Props { className?: string; @@ -16,81 +18,78 @@ const LabelsField: FC = ({ className }) => { control, watch, formState: { errors }, - } = useFormContext(); + } = useFormContext(); const labels = watch('labels'); + + const { fields, append, remove } = useFieldArray({ control, name: 'labels' }); + return (
- - {({ fields, append, remove }) => { - return ( - <> -
- Labels -
- {fields.map((field, index) => { - return ( -
-
- - - - = - - - -
-
- ); - })} - + <> +
+ Labels +
+ {fields.map((field, index) => { + return ( +
+
+ + + + = + + + +
-
- - ); - }} - + ); + })} + +
+
+
); }; diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx index 1b50995dc61..52828b993a2 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx @@ -5,9 +5,16 @@ import { useFormContext } from 'react-hook-form'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, CodeEditor, Drawer, Tab, TabsBar, useStyles2 } from '@grafana/ui'; +import { Button, CodeEditor, Drawer, Icon, Tab, TabsBar, useStyles2, Tooltip } from '@grafana/ui'; +import { RulerRuleDTO } from '../../../../../types/unified-alerting-dto'; import { RuleFormValues } from '../../types/rule-form'; +import { + alertingRulerRuleToRuleForm, + formValuesToRulerRuleDTO, + recordingRulerRuleToRuleForm, +} from '../../utils/rule-form'; +import { isAlertingRulerRule, isRecordingRulerRule } from '../../utils/rules'; interface Props { onClose: () => void; @@ -75,10 +82,16 @@ interface YamlTabProps { const InspectorYamlTab: FC = ({ onSubmit }) => { const styles = useStyles2(yamlTabStyle); const { getValues } = useFormContext(); - const [alertRuleAsYaml, setAlertRuleAsYaml] = useState(dump(getValues())); + + const yamlValues = formValuesToRulerRuleDTO(getValues()); + const [alertRuleAsYaml, setAlertRuleAsYaml] = useState(dump(yamlValues)); const onApply = () => { - onSubmit(load(alertRuleAsYaml) as RuleFormValues); + const rulerRule = load(alertRuleAsYaml) as RulerRuleDTO; + const currentFormValues = getValues(); + + const yamlFormValues = rulerRuleToRuleFormValues(rulerRule); + onSubmit({ ...currentFormValues, ...yamlFormValues }); }; return ( @@ -87,6 +100,9 @@ const InspectorYamlTab: FC = ({ onSubmit }) => { + } theme="info" placement="left-start" interactive={true}> + +
@@ -111,6 +127,32 @@ const InspectorYamlTab: FC = ({ onSubmit }) => { ); }; +function YamlContentInfo() { + return ( +
+ The YAML content in the editor only contains alert rule configuration
+ To configure Prometheus, you need to provide the rest of the{' '} + + configuration file content. + +
+ ); +} + +function rulerRuleToRuleFormValues(rulerRule: RulerRuleDTO): Partial { + if (isAlertingRulerRule(rulerRule)) { + return alertingRulerRuleToRuleForm(rulerRule); + } else if (isRecordingRulerRule(rulerRule)) { + return recordingRulerRuleToRuleForm(rulerRule); + } + + return {}; +} + const yamlTabStyle = (theme: GrafanaTheme2) => ({ content: css` flex-grow: 1; @@ -120,7 +162,11 @@ const yamlTabStyle = (theme: GrafanaTheme2) => ({ `, applyButton: css` display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; flex-grow: 0; + margin-bottom: ${theme.spacing(2)}; `, }); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index bd03002c23c..321216323da 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -21,6 +21,8 @@ import { GrafanaAlertStateDecision, Labels, PostableRuleGrafanaRuleDTO, + RulerAlertingRuleDTO, + RulerRecordingRuleDTO, RulerRuleDTO, } from 'app/types/unified-alerting-dto'; @@ -136,32 +138,26 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF } } else { if (isAlertingRulerRule(rule)) { - const [forTime, forTimeUnit] = rule.for - ? parseInterval(rule.for) - : [defaultFormValues.forTime, defaultFormValues.forTimeUnit]; + const alertingRuleValues = alertingRulerRuleToRuleForm(rule); + return { ...defaultFormValues, - name: rule.alert, + ...alertingRuleValues, type: RuleFormType.cloudAlerting, dataSourceName: ruleSourceName, namespace, group: group.name, - expression: rule.expr, - forTime, - forTimeUnit, - annotations: listifyLabelsOrAnnotations(rule.annotations), - labels: listifyLabelsOrAnnotations(rule.labels), }; } else if (isRecordingRulerRule(rule)) { + const recordingRuleValues = recordingRulerRuleToRuleForm(rule); + return { ...defaultFormValues, - name: rule.record, + ...recordingRuleValues, type: RuleFormType.cloudRecording, dataSourceName: ruleSourceName, namespace, group: group.name, - expression: rule.expr, - labels: listifyLabelsOrAnnotations(rule.labels), }; } else { throw new Error('Unexpected type of rule for cloud rules source'); @@ -169,6 +165,35 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF } } +export function alertingRulerRuleToRuleForm( + rule: RulerAlertingRuleDTO +): Pick { + const defaultFormValues = getDefaultFormValues(); + + const [forTime, forTimeUnit] = rule.for + ? parseInterval(rule.for) + : [defaultFormValues.forTime, defaultFormValues.forTimeUnit]; + + return { + name: rule.alert, + expression: rule.expr, + forTime, + forTimeUnit, + annotations: listifyLabelsOrAnnotations(rule.annotations), + labels: listifyLabelsOrAnnotations(rule.labels), + }; +} + +export function recordingRulerRuleToRuleForm( + rule: RulerRecordingRuleDTO +): Pick { + return { + name: rule.record, + expression: rule.expr, + labels: listifyLabelsOrAnnotations(rule.labels), + }; +} + export const getDefaultQueries = (): AlertQuery[] => { const dataSource = getDefaultOrFirstCompatibleDataSource();