mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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 } }) => (
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user