Alerting: better detect cortex/loki ruler api (#36030)

* wip

* beter detect non existing rules stuff

* fix useIsRuleEditable

* test for detecting editable-ness of a rules datasource

* tests!

* fix lint errors
This commit is contained in:
Domas
2021-07-13 00:10:13 +03:00
committed by GitHub
parent d4e53a9be4
commit 3ea8880d7f
18 changed files with 481 additions and 100 deletions

View File

@@ -1,32 +1,14 @@
import React, { FC, useCallback, useEffect } from 'react';
import React, { FC, useMemo } from 'react';
import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Field, Input, InputControl, Select, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { RuleEditorSection } from './RuleEditorSection';
import { useFormContext } from 'react-hook-form';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { DataSourcePicker } from '@grafana/runtime';
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
import { RuleFolderPicker } from './RuleFolderPicker';
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
import { contextSrv } from 'app/core/services/context_srv';
const alertTypeOptions: SelectableValue[] = [
{
label: 'Grafana managed alert',
value: RuleFormType.grafana,
description: 'Classic Grafana alerts based on thresholds.',
},
];
if (contextSrv.isEditor) {
alertTypeOptions.push({
label: 'Cortex/Loki managed alert',
value: RuleFormType.cloud,
description: 'Alert based on a system or application behavior. Based on Prometheus.',
});
}
import { CloudRulesSourcePicker } from './CloudRulesSourcePicker';
interface Props {
editingExistingRule: boolean;
@@ -46,21 +28,25 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
const ruleFormType = watch('type');
const dataSourceName = watch('dataSourceName');
useEffect(() => {}, [ruleFormType]);
const alertTypeOptions = useMemo((): SelectableValue[] => {
const result = [
{
label: 'Grafana managed alert',
value: RuleFormType.grafana,
description: 'Classic Grafana alerts based on thresholds.',
},
];
const rulesSourcesWithRuler = useRulesSourcesWithRuler();
if (contextSrv.isEditor) {
result.push({
label: 'Cortex/Loki managed alert',
value: RuleFormType.cloud,
description: 'Alert based on a system or application behavior. Based on Prometheus.',
});
}
const dataSourceFilter = useCallback(
(ds: DataSourceInstanceSettings): boolean => {
if (ruleFormType === RuleFormType.grafana) {
return !!ds.meta.alerting;
} else {
// filter out only rules sources that support ruler and thus can have alerts edited
return !!rulesSourcesWithRuler.find(({ id }) => id === ds.id);
}
},
[ruleFormType, rulesSourcesWithRuler]
);
return result;
}, []);
return (
<RuleEditorSection stepNo={1} title="Alert type">
@@ -71,6 +57,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
invalid={!!errors.name?.message}
>
<Input
id="name"
{...register('name', { required: { value: true, message: 'Must enter an alert name' } })}
autoFocus={true}
/>
@@ -82,25 +69,11 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
className={styles.formInput}
error={errors.type?.message}
invalid={!!errors.type?.message}
data-testid="alert-type-picker"
>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
options={alertTypeOptions}
onChange={(v: SelectableValue) => {
const value = v?.value;
// when switching to system alerts, null out data source selection if it's not a rules source with ruler
if (
value === RuleFormType.cloud &&
dataSourceName &&
!rulesSourcesWithRuler.find(({ name }) => name === dataSourceName)
) {
setValue('dataSourceName', null);
}
onChange(value);
}}
/>
<Select {...field} options={alertTypeOptions} onChange={(v: SelectableValue) => onChange(v?.value)} />
)}
name="type"
control={control}
@@ -115,15 +88,12 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
label="Select data source"
error={errors.dataSourceName?.message}
invalid={!!errors.dataSourceName?.message}
data-testid="datasource-picker"
>
<InputControl
render={({ field: { onChange, ref, value, ...field } }) => (
<DataSourcePicker
render={({ field: { onChange, ref, ...field } }) => (
<CloudRulesSourcePicker
{...field}
current={value}
filter={dataSourceFilter}
noDefault
alerting
onChange={(ds: DataSourceInstanceSettings) => {
// reset location if switching data sources, as different rules source will have different groups and namespaces
setValue('location', undefined);
@@ -149,6 +119,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
className={styles.formInput}
error={errors.folder?.message}
invalid={!!errors.folder?.message}
data-testid="folder-picker"
>
<InputControl
render={({ field: { ref, ...field } }) => (

View File

@@ -37,6 +37,7 @@ const AnnotationsField: FC = () => {
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`}
@@ -53,6 +54,7 @@ const AnnotationsField: FC = () => {
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`}

View File

@@ -0,0 +1,24 @@
import React, { useCallback } from 'react';
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
interface Props {
onChange: (ds: DataSourceInstanceSettings) => void;
value: string | null;
onBlur?: () => void;
name?: string;
}
export function CloudRulesSourcePicker({ value, ...props }: Props): JSX.Element {
const rulesSourcesWithRuler = useRulesSourcesWithRuler();
const dataSourceFilter = useCallback(
(ds: DataSourceInstanceSettings): boolean => {
return !!rulesSourcesWithRuler.find(({ id }) => id === ds.id);
},
[rulesSourcesWithRuler]
);
return <DataSourcePicker noDefault alerting filter={dataSourceFilter} current={value} {...props} />;
}

View File

@@ -6,13 +6,13 @@ import { useAsync } from 'react-use';
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
import { LokiQuery } from 'app/plugins/datasource/loki/types';
interface Props {
export interface ExpressionEditorProps {
value?: string;
onChange: (value: string) => void;
dataSourceName: string; // will be a prometheus or loki datasource
}
export const ExpressionEditor: FC<Props> = ({ value, onChange, dataSourceName }) => {
export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, dataSourceName }) => {
const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName);
const [query, setQuery] = useState(mapToQuery({ refId: 'A', hide: false }, value));
const { error, loading, value: dataSource } = useAsync(() => {

View File

@@ -49,7 +49,12 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
return (
<div className={style.flexRow}>
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
<Field
data-testid="namespace-picker"
label="Namespace"
error={errors.namespace?.message}
invalid={!!errors.namespace?.message}
>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<SelectWithAdd
@@ -73,7 +78,7 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
}}
/>
</Field>
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
<Field data-testid="group-picker" label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
<InputControl
render={({ field: { ref, ...field } }) => (
<SelectWithAdd {...field} options={groupOptions} width={42} custom={customGroup} className={style.input} />

View File

@@ -41,6 +41,7 @@ const LabelsField: FC<Props> = ({ className }) => {
required: { value: !!labels[index]?.value, message: 'Required.' },
})}
placeholder="key"
data-testid={`label-key-${index}`}
defaultValue={field.key}
/>
</Field>
@@ -55,6 +56,7 @@ const LabelsField: FC<Props> = ({ className }) => {
required: { value: !!labels[index]?.key, message: 'Required.' },
})}
placeholder="value"
data-testid={`label-value-${index}`}
defaultValue={field.value}
/>
</Field>