mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Change the rule yaml data to reflect Prom-based rule format (#54520)
This commit is contained in:
parent
ef245874da
commit
933ec4817f
@ -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"]
|
||||
],
|
||||
|
@ -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<RuleFormValues>();
|
||||
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 (
|
||||
<>
|
||||
<Label>Summary and annotations</Label>
|
||||
<FieldArray name={'annotations'} control={control}>
|
||||
{({ fields, append, remove }) => {
|
||||
<div className={styles.flexColumn}>
|
||||
{fields.map((annotationField, index) => {
|
||||
const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url');
|
||||
const ValueInputComponent = isUrl ? Input : TextArea;
|
||||
|
||||
return (
|
||||
<div className={styles.flexColumn}>
|
||||
{fields.map((field, index) => {
|
||||
const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url');
|
||||
const ValueInputComponent = isUrl ? Input : TextArea;
|
||||
return (
|
||||
<div key={field.id} className={styles.flexRow}>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.annotations?.[index]?.key?.message}
|
||||
error={errors.annotations?.[index]?.key?.message}
|
||||
data-testid={`annotation-key-${index}`}
|
||||
>
|
||||
<InputControl
|
||||
name={`annotations[${index}].key`}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<AnnotationKeyInput
|
||||
{...field}
|
||||
aria-label={`Annotation detail ${index + 1}`}
|
||||
existingKeys={existingKeys(index)}
|
||||
width={18}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
className={cx(styles.flexRowItemMargin, styles.field)}
|
||||
invalid={!!errors.annotations?.[index]?.value?.message}
|
||||
error={errors.annotations?.[index]?.value?.message}
|
||||
>
|
||||
<ValueInputComponent
|
||||
data-testid={`annotation-value-${index}`}
|
||||
className={cx(styles.annotationValueInput, { [styles.textarea]: !isUrl })}
|
||||
{...register(`annotations[${index}].value`)}
|
||||
placeholder={isUrl ? 'https://' : `Text`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="button"
|
||||
className={styles.flexRowItemMargin}
|
||||
aria-label="delete annotation"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className={styles.addAnnotationsButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
<div key={annotationField.id} className={styles.flexRow}>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.annotations?.[index]?.key?.message}
|
||||
error={errors.annotations?.[index]?.key?.message}
|
||||
data-testid={`annotation-key-${index}`}
|
||||
>
|
||||
Add info
|
||||
</Button>
|
||||
<InputControl
|
||||
name={`annotations.${index}.key`}
|
||||
defaultValue={annotationField.key}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<AnnotationKeyInput
|
||||
{...field}
|
||||
aria-label={`Annotation detail ${index + 1}`}
|
||||
existingKeys={existingKeys(index)}
|
||||
width={18}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
className={cx(styles.flexRowItemMargin, styles.field)}
|
||||
invalid={!!errors.annotations?.[index]?.value?.message}
|
||||
error={errors.annotations?.[index]?.value?.message}
|
||||
>
|
||||
<ValueInputComponent
|
||||
data-testid={`annotation-value-${index}`}
|
||||
className={cx(styles.annotationValueInput, { [styles.textarea]: !isUrl })}
|
||||
{...register(`annotations.${index}.value`)}
|
||||
placeholder={isUrl ? 'https://' : `Text`}
|
||||
defaultValue={annotationField.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="button"
|
||||
className={styles.flexRowItemMargin}
|
||||
aria-label="delete annotation"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FieldArray>
|
||||
})}
|
||||
<Button
|
||||
className={styles.addAnnotationsButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
>
|
||||
Add info
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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<ExpressionEditorProps> = ({ 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<ExpressionEditorProps> = ({ value, onChange, d
|
||||
|
||||
const onChangeQuery = useCallback(
|
||||
(query: DataQuery) => {
|
||||
setQuery(query);
|
||||
onChange(mapToValue(query));
|
||||
},
|
||||
[onChange, mapToValue]
|
||||
|
@ -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<Props> = ({ className }) => {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
} = useFormContext<RuleFormValues>();
|
||||
const labels = watch('labels');
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'labels' });
|
||||
|
||||
return (
|
||||
<div className={cx(className, styles.wrapper)}>
|
||||
<Label>Custom Labels</Label>
|
||||
<FieldArray control={control} name="labels">
|
||||
{({ fields, append, remove }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.flexRow}>
|
||||
<InlineLabel width={18}>Labels</InlineLabel>
|
||||
<div className={styles.flexColumn}>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
<div className={cx(styles.flexRow, styles.centerAlignRow)}>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.key?.message}
|
||||
error={errors.labels?.[index]?.key?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels[${index}].key`, {
|
||||
required: { value: !!labels[index]?.value, message: 'Required.' },
|
||||
})}
|
||||
placeholder="key"
|
||||
data-testid={`label-key-${index}`}
|
||||
defaultValue={field.key}
|
||||
/>
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.value?.message}
|
||||
error={errors.labels?.[index]?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels[${index}].value`, {
|
||||
required: { value: !!labels[index]?.key, message: 'Required.' },
|
||||
})}
|
||||
placeholder="value"
|
||||
data-testid={`label-value-${index}`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
className={styles.deleteLabelButton}
|
||||
aria-label="delete label"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className={styles.addLabelButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({});
|
||||
}}
|
||||
>
|
||||
Add label
|
||||
</Button>
|
||||
<>
|
||||
<div className={styles.flexRow}>
|
||||
<InlineLabel width={18}>Labels</InlineLabel>
|
||||
<div className={styles.flexColumn}>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
<div className={cx(styles.flexRow, styles.centerAlignRow)}>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.key?.message}
|
||||
error={errors.labels?.[index]?.key?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.key`, {
|
||||
required: { value: !!labels[index]?.value, message: 'Required.' },
|
||||
})}
|
||||
placeholder="key"
|
||||
data-testid={`label-key-${index}`}
|
||||
defaultValue={field.key}
|
||||
/>
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.value?.message}
|
||||
error={errors.labels?.[index]?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.value`, {
|
||||
required: { value: !!labels[index]?.key, message: 'Required.' },
|
||||
})}
|
||||
placeholder="value"
|
||||
data-testid={`label-value-${index}`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
className={styles.deleteLabelButton}
|
||||
aria-label="delete label"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</FieldArray>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className={styles.addLabelButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({});
|
||||
}}
|
||||
>
|
||||
Add label
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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<YamlTabProps> = ({ onSubmit }) => {
|
||||
const styles = useStyles2(yamlTabStyle);
|
||||
const { getValues } = useFormContext<RuleFormValues>();
|
||||
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<YamlTabProps> = ({ onSubmit }) => {
|
||||
<Button type="button" onClick={onApply}>
|
||||
Apply
|
||||
</Button>
|
||||
<Tooltip content={<YamlContentInfo />} theme="info" placement="left-start" interactive={true}>
|
||||
<Icon name="exclamation-triangle" size="xl" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
@ -111,6 +127,32 @@ const InspectorYamlTab: FC<YamlTabProps> = ({ onSubmit }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function YamlContentInfo() {
|
||||
return (
|
||||
<div>
|
||||
The YAML content in the editor only contains alert rule configuration <br />
|
||||
To configure Prometheus, you need to provide the rest of the{' '}
|
||||
<a
|
||||
href="https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
configuration file content.
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function rulerRuleToRuleFormValues(rulerRule: RulerRuleDTO): Partial<RuleFormValues> {
|
||||
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)};
|
||||
`,
|
||||
});
|
||||
|
||||
|
@ -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<RuleFormValues, 'name' | 'forTime' | 'forTimeUnit' | 'expression' | 'annotations' | 'labels'> {
|
||||
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<RuleFormValues, 'name' | 'expression' | 'labels'> {
|
||||
return {
|
||||
name: rule.record,
|
||||
expression: rule.expr,
|
||||
labels: listifyLabelsOrAnnotations(rule.labels),
|
||||
};
|
||||
}
|
||||
|
||||
export const getDefaultQueries = (): AlertQuery[] => {
|
||||
const dataSource = getDefaultOrFirstCompatibleDataSource();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user