Revert "Alerting: Fix alert form broken coming from panel (#64148)" (#64240)

This commit is contained in:
Gilles De Mey 2023-03-06 16:31:03 +01:00 committed by GitHub
parent 4e81aab60a
commit 282d021c53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 83 additions and 446 deletions

View File

@ -1,5 +1,5 @@
// BETTERER RESULTS V2.
//
//
// If this file contains merge conflicts, use `betterer merge` to automatically resolve them:
// https://phenomnomnominal.github.io/betterer/docs/results-file/#merge
//
@ -2685,6 +2685,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/alerting/unified/components/rules/RuleDetailsDataSources.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],

View File

@ -19,12 +19,5 @@ export function getDataSourceSrv() {
return {
getList: () => [ds1],
getInstanceSettings: () => ds1,
get: () =>
Promise.resolve({
filterQuery: () => true,
getDefaultQuery: () => ({
expr: 'vector(1)',
}),
}),
};
}

View File

@ -2,7 +2,6 @@ import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { byRole, byText } from 'testing-library-selector';
import { setDataSourceSrv } from '@grafana/runtime';
@ -160,10 +159,7 @@ describe('RuleEditor cloud: checking editable data sources', () => {
return null;
});
const dsServer = new MockDataSourceSrv(dataSources);
jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds'));
setDataSourceSrv(dsServer);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.searchFolders.mockResolvedValue([]);

View File

@ -1,9 +1,8 @@
import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event';
import React from 'react';
import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { byRole } from 'testing-library-selector';
import { setDataSourceSrv } from '@grafana/runtime';
@ -80,10 +79,8 @@ describe('RuleEditor cloud', () => {
{ alerting: true }
),
};
const dsServer = new MockDataSourceSrv(dataSources);
jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds'));
setDataSourceSrv(dsServer);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);

View File

@ -1,4 +1,4 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import { render, waitFor, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Route } from 'react-router-dom';
@ -25,7 +25,7 @@ import { disableRBAC, mockDataSource, MockDataSourceSrv, mockFolder } from './mo
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
import * as config from './utils/config';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import * as ruleForm from './utils/rule-form';
import { getDefaultQueries } from './utils/rule-form';
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
// eslint-disable-next-line react/display-name
@ -46,14 +46,12 @@ jest.mock('app/features/query/components/QueryEditorRow', () => ({
}));
jest.spyOn(config, 'getAllDataSources');
jest.spyOn(ruleForm, 'getDefaultQueriesAsync');
jest.setTimeout(60 * 1000);
const mocks = {
getAllDataSources: jest.mocked(config.getAllDataSources),
searchFolders: jest.mocked(searchFolders),
getDefaultQueriesAsync: jest.mocked(ruleForm.getDefaultQueriesAsync),
api: {
discoverFeatures: jest.mocked(discoverFeatures),
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
@ -121,17 +119,6 @@ describe('RuleEditor grafana managed rules', () => {
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
mocks.getDefaultQueriesAsync.mockResolvedValue({
queries: [
{
refId: 'A',
relativeTimeRange: { from: 900, to: 1000 },
datasourceUid: 'dsuid',
model: { refId: 'A' },
queryType: 'query',
},
],
});
mocks.api.fetchRulerRules.mockResolvedValue({
[folder.title]: [
{
@ -146,16 +133,8 @@ describe('RuleEditor grafana managed rules', () => {
uid,
namespace_uid: 'abcd',
namespace_id: 1,
condition: 'A',
data: [
{
refId: 'A',
relativeTimeRange: { from: 900, to: 1000 },
datasourceUid: 'dsuid',
model: { refId: 'A' },
queryType: 'query',
},
],
condition: 'B',
data: getDefaultQueries(),
exec_err_state: GrafanaAlertStateDecision.Error,
no_data_state: GrafanaAlertStateDecision.NoData,
title: 'my great new rule',
@ -223,16 +202,8 @@ describe('RuleEditor grafana managed rules', () => {
for: '5m',
grafana_alert: {
uid,
condition: 'A',
data: [
{
refId: 'A',
relativeTimeRange: { from: 900, to: 1000 },
datasourceUid: 'dsuid',
model: { refId: 'A' },
queryType: 'query',
},
],
condition: 'B',
data: getDefaultQueries(),
exec_err_state: GrafanaAlertStateDecision.Error,
is_paused: false,
no_data_state: 'NoData',

View File

@ -1,9 +1,8 @@
import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event';
import React from 'react';
import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { byRole } from 'testing-library-selector';
import { setDataSourceSrv } from '@grafana/runtime';
@ -20,6 +19,7 @@ import { disableRBAC, mockDataSource, MockDataSourceSrv } from './mocks';
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
import * as config from './utils/config';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { getDefaultQueries } from './utils/rule-form';
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
// eslint-disable-next-line react/display-name
@ -74,14 +74,11 @@ describe('RuleEditor grafana managed rules', () => {
name: 'Prom',
isDefault: true,
},
{ alerting: true }
{ alerting: false }
),
};
const dsServer = new MockDataSourceSrv(dataSources);
jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds'));
setDataSourceSrv(dsServer);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
@ -124,7 +121,6 @@ describe('RuleEditor grafana managed rules', () => {
rulerApiEnabled: false,
},
});
renderRuleEditor();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
@ -146,7 +142,6 @@ describe('RuleEditor grafana managed rules', () => {
// save and check what was sent to backend
await userEvent.click(ui.buttons.save.get());
// 9seg
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
// 9seg
@ -154,107 +149,20 @@ describe('RuleEditor grafana managed rules', () => {
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
'Folder A',
{
name: 'group1',
interval: '1m',
name: 'group1',
rules: [
{
grafana_alert: {
title: 'my great new rule',
condition: 'C',
no_data_state: 'NoData',
exec_err_state: GrafanaAlertStateDecision.Error,
data: [
{
refId: 'A',
relativeTimeRange: {
from: 600,
to: 0,
},
datasourceUid: 'mock-ds-2',
model: {
hide: false,
refId: 'A',
},
queryType: '',
},
{
refId: 'B',
datasourceUid: '__expr__',
queryType: '',
model: {
refId: 'B',
hide: false,
type: 'reduce',
datasource: {
uid: '__expr__',
type: '__expr__',
},
conditions: [
{
type: 'query',
evaluator: {
params: [],
type: 'gt',
},
operator: {
type: 'and',
},
query: {
params: ['B'],
},
reducer: {
params: [],
type: 'last',
},
},
],
reducer: 'last',
expression: 'A',
},
},
{
refId: 'C',
datasourceUid: '__expr__',
queryType: '',
model: {
refId: 'C',
hide: false,
type: 'threshold',
datasource: {
uid: '__expr__',
type: '__expr__',
},
conditions: [
{
type: 'query',
evaluator: {
params: [0],
type: 'gt',
},
operator: {
type: 'and',
},
query: {
params: ['C'],
},
reducer: {
params: [],
type: 'last',
},
},
],
expression: 'B',
},
},
],
is_paused: false,
},
annotations: { description: 'some description' },
labels: { severity: 'warn' },
for: '5m',
annotations: {
description: 'some description',
},
labels: {
severity: 'warn',
grafana_alert: {
condition: 'B',
data: getDefaultQueries(),
exec_err_state: GrafanaAlertStateDecision.Error,
is_paused: false,
no_data_state: 'NoData',
title: 'my great new rule',
},
},
],

View File

@ -1,9 +1,8 @@
import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event';
import React from 'react';
import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { byRole, byText } from 'testing-library-selector';
import { setDataSourceSrv } from '@grafana/runtime';
@ -76,10 +75,7 @@ describe('RuleEditor recording rules', () => {
),
};
const dsServer = new MockDataSourceSrv(dataSources);
jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds'));
setDataSourceSrv(dsServer);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);

View File

@ -2,40 +2,23 @@ import { css } from '@emotion/css';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
import { Link } from 'react-router-dom';
import { useAsync } from 'react-use';
import { DataQuery, DataSourceApi, DataSourceJsonData, GrafanaTheme2, UrlQueryMap } from '@grafana/data';
import { config, logInfo } from '@grafana/runtime';
import {
Button,
ConfirmModal,
CustomScrollbar,
Field,
HorizontalGroup,
Input,
LoadingPlaceholder,
Spinner,
useStyles2,
} from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { logInfo, config } from '@grafana/runtime';
import { Button, ConfirmModal, CustomScrollbar, Spinner, useStyles2, HorizontalGroup, Field, Input } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/core';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types';
import { RuleWithLocation } from 'app/types/unified-alerting';
import { AlertQuery, RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { LogMessages, trackNewAlerRuleFormCancelled, trackNewAlerRuleFormError } from '../../Analytics';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { deleteRuleAction, saveRuleFormAction } from '../../state/actions';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { initialAsyncRequestState } from '../../utils/redux';
import {
getDefaultFormValues,
getDefaultQueriesAsync,
getInitialDefaultQueries,
rulerRuleToFormValues,
} from '../../utils/rule-form';
import { getDefaultFormValues, getDefaultQueries, rulerRuleToFormValues } from '../../utils/rule-form';
import * as ruleId from '../../utils/rule-id';
import { CloudEvaluationBehavior } from './CloudEvaluationBehavior';
@ -90,24 +73,6 @@ type Props = {
prefill?: Partial<RuleFormValues>; // Existing implies we modify existing rule. Prefill only provides default form values
};
export const useGetDefaults = (queryParams: UrlQueryMap, existing: RuleWithLocation<RulerRuleDTO> | undefined) => {
const [defaultDsAndQueries, setDefaultDsAndQueries] = useState<{
queries: AlertQuery[] | null;
ds?: DataSourceApi<DataQuery, DataSourceJsonData, {}>;
}>({ queries: null });
useAsync(async () => {
setDefaultDsAndQueries(await getDefaultQueriesAsync());
}, [existing]);
const defaultsInQueryParams: string = queryParams['defaults'] as string;
const defaultsInQueryParamsObject = useMemo(
() => ({ ...(defaultsInQueryParams ? JSON.parse(defaultsInQueryParams) : {}) }),
[defaultsInQueryParams]
);
return { defaultDsAndQueries, defaultsInQueryParamsObject, defaultsInQueryParams };
};
export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
@ -119,11 +84,6 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
const { defaultDsAndQueries, defaultsInQueryParamsObject, defaultsInQueryParams } = useGetDefaults(
queryParams,
existing
);
const defaultValues: RuleFormValues = useMemo(() => {
if (existing) {
return rulerRuleToFormValues(existing);
@ -138,13 +98,13 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
return {
...getDefaultFormValues(),
queries: getInitialDefaultQueries(),
queries: getDefaultQueries(),
condition: 'C',
...defaultsInQueryParamsObject,
...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}),
type: RuleFormType.grafana,
evaluateEvery: evaluateEvery,
};
}, [existing, prefill, evaluateEvery, defaultsInQueryParamsObject]);
}, [existing, prefill, queryParams, evaluateEvery]);
const formAPI = useForm<RuleFormValues>({
mode: 'onSubmit',
@ -152,21 +112,7 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
shouldFocusError: true,
});
const { handleSubmit, watch, reset } = formAPI;
// only reset once we get some value in defaultDsAndQueries.queries, adding this value.
useEffect(() => {
const shouldReset = !existing && !prefill && defaultDsAndQueries.queries && !Boolean(defaultsInQueryParams);
if (shouldReset) {
reset({
...getDefaultFormValues(),
queries: defaultDsAndQueries.queries,
condition: 'C',
...defaultsInQueryParamsObject,
type: RuleFormType.grafana,
});
}
}, [defaultDsAndQueries.queries, reset, existing, prefill, defaultsInQueryParamsObject, defaultsInQueryParams]);
const { handleSubmit, watch } = formAPI;
const type = watch('type');
const dataSourceName = watch('dataSourceName');
@ -248,7 +194,7 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
const evaluateEveryInForm = watch('evaluateEvery');
useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]);
return defaultDsAndQueries.queries ? (
return (
<FormProvider {...formAPI}>
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
<HorizontalGroup height="auto" justify="flex-end">
@ -301,13 +247,7 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
<div className={styles.contentInner}>
<AlertRuleNameInput />
<QueryAndExpressionsStep
editingExistingRule={!!existing}
prefill={Boolean(prefill) || Boolean(defaultsInQueryParams)}
onDataChange={checkAlertCondition}
asyncDefaultQueries={defaultDsAndQueries.queries}
asyncDataSource={defaultDsAndQueries.ds}
/>
<QueryAndExpressionsStep editingExistingRule={!!existing} onDataChange={checkAlertCondition} />
{showStep2 && (
<>
{type === RuleFormType.grafana ? (
@ -341,8 +281,6 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
) : null}
{showEditYaml ? <RuleInspector onClose={() => setShowEditYaml(false)} /> : null}
</FormProvider>
) : (
<LoadingPlaceholder text={'Loading defaults...'} />
);
};

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { noop } from 'lodash';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useAsync, usePrevious } from 'react-use';
import React, { FC, useCallback, useMemo } from 'react';
import { useAsync } from 'react-use';
import { CoreApp, DataQuery, DataSourcePluginContextProvider, GrafanaTheme2, LoadingState } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
@ -9,8 +9,6 @@ import { Alert, Button, useStyles2 } from '@grafana/ui';
import { LokiQuery } from 'app/plugins/datasource/loki/types';
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
import { AlertQuery } from '../../../../../types/unified-alerting-dto';
import { CloudAlertPreview } from './CloudAlertPreview';
import { usePreview } from './PreviewRule';
@ -19,8 +17,6 @@ export interface ExpressionEditorProps {
onChange: (value: string) => void;
dataSourceName: string; // will be a prometheus or loki datasource
showPreviewAlertsButton: boolean;
asyncDefaultQuery?: AlertQuery;
preservePreviousValue: boolean;
}
export const ExpressionEditor: FC<ExpressionEditorProps> = ({
@ -28,31 +24,11 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({
onChange,
dataSourceName,
showPreviewAlertsButton = true,
asyncDefaultQuery,
preservePreviousValue,
}) => {
const styles = useStyles2(getStyles);
const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName);
const [dataQuery, setDataQuery] = useState(mapToQuery({ refId: 'A', hide: false }, value));
const defaultModel = asyncDefaultQuery?.model;
const previousDataSource = usePrevious(dataSourceName);
// New alert: update with default query once we have the async default value
useEffect(() => {
const shouldSetDefaultQuery = !preservePreviousValue && defaultModel;
if (shouldSetDefaultQuery) {
setDataQuery((dataQuery) => ({ ...dataQuery, ...{ ...defaultModel } }));
}
}, [defaultModel, preservePreviousValue]);
// when data source is changed
useEffect(() => {
const shouldSetDefaultQuery =
!!previousDataSource && previousDataSource !== dataSourceName && Boolean(dataSourceName) && defaultModel;
if (shouldSetDefaultQuery) {
setDataQuery((dataQuery) => ({ ...dataQuery, ...{ ...defaultModel } }));
}
}, [dataSourceName, defaultModel, previousDataSource]);
const dataQuery = mapToQuery({ refId: 'A', hide: false }, value);
const {
error,
@ -95,7 +71,7 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({
// The preview API returns arrays with empty elements when there are no firing alerts
const previewHasAlerts = previewDataFrame && previewDataFrame.fields.some((field) => field.values.length > 0);
return dataQuery ? (
return (
<>
<DataSourcePluginContextProvider instanceSettings={dsi}>
<QueryEditor
@ -125,7 +101,7 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({
</div>
)}
</>
) : null;
);
};
const getStyles = (theme: GrafanaTheme2) => ({

View File

@ -2,16 +2,7 @@ import { omit } from 'lodash';
import React, { PureComponent, useState } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import {
CoreApp,
DataQuery,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
LoadingState,
PanelData,
RelativeTimeRange,
} from '@grafana/data';
import { DataQuery, DataSourceInstanceSettings, LoadingState, PanelData, RelativeTimeRange } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { Button, Card, Icon } from '@grafana/ui';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
@ -75,30 +66,22 @@ export class QueryRows extends PureComponent<Props> {
);
};
onChangeDataSource = async (settings: DataSourceInstanceSettings, index: number) => {
onChangeDataSource = (settings: DataSourceInstanceSettings, index: number) => {
const { queries, onQueriesChange } = this.props;
const updatedQueries = await Promise.all(
queries.map(async (item, itemIndex) => {
if (itemIndex !== index) {
return item;
}
const updatedQueries = queries.map((item, itemIndex) => {
if (itemIndex !== index) {
return item;
}
const previousSettings = this.getDataSourceSettings(item);
const previousSettings = this.getDataSourceSettings(item);
// Copy model if changing to a datasource of same type.
if (settings.type === previousSettings?.type) {
return copyModel(item, settings);
}
let ds;
try {
ds = await getDataSourceSrv().get(settings.uid); // get new ds
} catch (e) {
return newModel(item, settings);
}
return newModel(item, settings, ds);
})
);
// Copy model if changing to a datasource of same type.
if (settings.type === previousSettings?.type) {
return copyModel(item, settings);
}
return newModel(item, settings);
});
onQueriesChange(updatedQueries);
};
@ -235,34 +218,13 @@ function copyModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit
};
}
function newModel(
item: AlertQuery,
settings: DataSourceInstanceSettings,
ds?: DataSourceApi<DataQuery, DataSourceJsonData, {}>
): Omit<AlertQuery, 'datasource'> {
if (!ds) {
return {
refId: item.refId,
relativeTimeRange: item.relativeTimeRange,
queryType: '',
datasourceUid: settings.uid,
model: {
refId: item.refId,
hide: false,
datasource: {
type: settings.type,
uid: settings.uid,
},
},
};
}
function newModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit<AlertQuery, 'datasource'> {
return {
refId: item.refId,
relativeTimeRange: item.relativeTimeRange,
queryType: '',
datasourceUid: settings.uid,
model: {
...ds?.getDefaultQuery?.(CoreApp.UnifiedAlerting),
refId: item.refId,
hide: false,
datasource: {

View File

@ -1,7 +1,7 @@
import React, { FC, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { DataQuery, DataSourceApi, DataSourceJsonData, LoadingState, PanelData } from '@grafana/data';
import { LoadingState, PanelData } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental';
import { config } from '@grafana/runtime';
@ -35,25 +35,16 @@ import {
interface Props {
editingExistingRule: boolean;
prefill: boolean;
onDataChange: (error: string) => void;
asyncDefaultQueries?: AlertQuery[];
asyncDataSource?: DataSourceApi<DataQuery, DataSourceJsonData, {}>;
}
export const QueryAndExpressionsStep: FC<Props> = ({
editingExistingRule,
onDataChange,
asyncDefaultQueries,
asyncDataSource,
prefill,
}) => {
export const QueryAndExpressionsStep: FC<Props> = ({ editingExistingRule, onDataChange }) => {
const runner = useRef(new AlertingQueryRunner());
const {
setValue,
getValues,
watch,
formState: { errors, isDirty },
formState: { errors },
control,
} = useFormContext<RuleFormValues>();
const [panelData, setPanelData] = useState<Record<string, PanelData>>({});
@ -63,6 +54,7 @@ export const QueryAndExpressionsStep: FC<Props> = ({
panelData: {},
};
const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState);
const [type, condition, dataSourceName] = watch(['type', 'condition', 'dataSourceName']);
const isGrafanaManagedType = type === RuleFormType.grafana;
@ -79,15 +71,6 @@ export const QueryAndExpressionsStep: FC<Props> = ({
runner.current.run(getValues('queries'));
}, [getValues]);
const updateWithDefault = !editingExistingRule && !prefill;
//once default queries is updated
useEffect(() => {
const shouldSetDataQuery = updateWithDefault && !isDirty && asyncDefaultQueries;
if (shouldSetDataQuery) {
dispatch(setDataQueries(asyncDefaultQueries));
}
}, [asyncDefaultQueries, updateWithDefault, isDirty]);
// whenever we update the queries we have to update the form too
useEffect(() => {
setValue('queries', queries, { shouldValidate: false });
@ -193,13 +176,6 @@ export const QueryAndExpressionsStep: FC<Props> = ({
}
}, [condition, queries, handleSetCondition]);
const defaultQueryAvailable = asyncDataSource && asyncDefaultQueries;
const onAddNewQuery = () => {
if (defaultQueryAvailable) {
dispatch(addNewDataQuery({ ds: asyncDataSource, defaultQuery: asyncDefaultQueries[0]?.model }));
}
};
return (
<RuleEditorSection stepNo={2} title="Set a query and alert condition">
<AlertType editingExistingRule={editingExistingRule} />
@ -215,8 +191,6 @@ export const QueryAndExpressionsStep: FC<Props> = ({
{...field}
dataSourceName={dataSourceName}
showPreviewAlertsButton={!isRecordingRuleType}
asyncDefaultQuery={asyncDefaultQueries?.[0]}
preservePreviousValue={!updateWithDefault}
/>
);
}}
@ -265,9 +239,11 @@ export const QueryAndExpressionsStep: FC<Props> = ({
<Button
type="button"
icon="plus"
onClick={onAddNewQuery}
onClick={() => {
dispatch(addNewDataQuery());
}}
variant="secondary"
data-testid={selectors.components.QueryTab.addQuery}
aria-label={selectors.components.QueryTab.addQuery}
disabled={noCompatibleDataSources}
>
Add query

View File

@ -66,24 +66,14 @@ exports[`Query and expressions reducer should add query 1`] = `
"refId": "A",
},
{
"datasourceUid": "some uid",
"datasourceUid": "c8eceabb-0275-4108-8f03-8f74faf4bf6d",
"model": {
"datasource": {
"type": undefined,
"uid": "some uid",
"type": "prometheus",
"uid": "c8eceabb-0275-4108-8f03-8f74faf4bf6d",
},
"datasourceUid": "",
"hide": false,
"model": {
"expression": "THIS IS THE DEFAULT EXPRESSION",
"refId": "A",
},
"queryType": "",
"refId": "B",
"relativeTimeRange": {
"from": 600,
"to": 0,
},
},
"queryType": "",
"refId": "B",

View File

@ -1,4 +1,4 @@
import { DataSourceApi, getDefaultRelativeTimeRange, PluginMeta, RelativeTimeRange } from '@grafana/data';
import { getDefaultRelativeTimeRange, RelativeTimeRange } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime/src/services/__mocks__/dataSourceSrv';
import {
dataSource as expressionDatasource,
@ -8,9 +8,6 @@ import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/t
import { defaultCondition } from 'app/features/expressions/utils/expressionTypes';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { mockDataSource } from '../../../mocks';
import * as dataSource from '../../../utils/datasource';
import {
addNewDataQuery,
addNewExpression,
@ -52,12 +49,6 @@ const expressionQuery: AlertQuery = {
queryType: '',
};
const defaultDataSource = mockDataSource();
const dataSourceMocked = jest.spyOn(dataSource, 'getDefaultOrFirstCompatibleDataSource');
beforeAll(() => {
return dataSourceMocked.mockReturnValue(defaultDataSource);
});
describe('Query and expressions reducer', () => {
it('should return initial state', () => {
expect(queriesAndExpressionsReducer(undefined, { type: undefined })).toEqual({
@ -105,23 +96,7 @@ describe('Query and expressions reducer', () => {
queries: [alertQuery],
};
const newDataSource: DataSourceApi = {
meta: { alerting: true } as unknown as PluginMeta,
name: 'some name',
uid: 'some uid',
} as unknown as DataSourceApi;
const defaultQuery: AlertQuery = {
refId: 'A',
queryType: '',
datasourceUid: '',
model: { refId: 'A', expression: 'THIS IS THE DEFAULT EXPRESSION' },
relativeTimeRange: getDefaultRelativeTimeRange(),
};
const newState = queriesAndExpressionsReducer(
initialState,
addNewDataQuery({ ds: newDataSource, defaultQuery: defaultQuery })
);
const newState = queriesAndExpressionsReducer(initialState, addNewDataQuery());
expect(newState.queries).toHaveLength(2);
expect(newState).toMatchSnapshot();
});

View File

@ -1,12 +1,6 @@
import { createAction, createReducer } from '@reduxjs/toolkit';
import {
DataQuery,
DataSourceApi,
DataSourceJsonData,
getDefaultRelativeTimeRange,
RelativeTimeRange,
} from '@grafana/data';
import { DataQuery, getDefaultRelativeTimeRange, RelativeTimeRange } from '@grafana/data';
import { getNextRefIdChar } from 'app/core/utils/query';
import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/utils/dataSourceFromExpression';
import {
@ -18,6 +12,7 @@ import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/t
import { defaultCondition } from 'app/features/expressions/utils/expressionTypes';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource';
import { queriesWithUpdatedReferences, refIdExists } from '../util';
export interface QueriesAndExpressionsState {
@ -38,10 +33,7 @@ const initialState: QueriesAndExpressionsState = {
};
export const duplicateQuery = createAction<AlertQuery>('duplicateQuery');
export const addNewDataQuery = createAction<{
ds: DataSourceApi<DataQuery, DataSourceJsonData, {}>;
defaultQuery: Partial<DataQuery> | undefined;
}>('addNewDataQuery');
export const addNewDataQuery = createAction('addNewDataQuery');
export const setDataQueries = createAction<AlertQuery[]>('setDataQueries');
export const addNewExpression = createAction('addNewExpression');
@ -59,17 +51,20 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder
.addCase(duplicateQuery, (state, { payload }) => {
state.queries = addQuery(state.queries, payload);
})
.addCase(addNewDataQuery, (state, { payload }) => {
const datasource = payload.ds;
.addCase(addNewDataQuery, (state) => {
const datasource = getDefaultOrFirstCompatibleDataSource();
if (!datasource) {
return;
}
state.queries = addQuery(state.queries, {
datasourceUid: datasource.uid,
model: {
refId: '',
datasource: {
type: datasource.type,
uid: datasource.uid,
},
...payload.defaultQuery,
refId: '',
},
});
})

View File

@ -1,8 +1,5 @@
import {
CoreApp,
DataQuery,
DataSourceApi,
DataSourceJsonData,
DataSourceRef,
getDefaultRelativeTimeRange,
IntervalValues,
@ -202,46 +199,9 @@ export function recordingRulerRuleToRuleForm(
};
}
export const getDefaultQueriesAsync = async (): Promise<{
queries: AlertQuery[] | null;
ds?: DataSourceApi<DataQuery, DataSourceJsonData, {}>;
}> => {
export const getDefaultQueries = (): AlertQuery[] => {
const dataSource = getDefaultOrFirstCompatibleDataSource();
if (!dataSource) {
return { queries: [...getDefaultExpressions('A', 'B')] };
}
const relativeTimeRange = getDefaultRelativeTimeRange();
let ds;
try {
ds = await getDataSourceSrv().get(dataSource.uid);
} catch (error) {
return { queries: [...getDefaultExpressions('A', 'B')] };
}
return {
queries: [
{
refId: 'A',
datasourceUid: dataSource.uid,
queryType: '',
relativeTimeRange,
model: {
refId: 'A',
hide: false,
...ds?.getDefaultQuery?.(CoreApp.UnifiedAlerting),
},
},
...getDefaultExpressions('B', 'C'),
],
ds: ds,
};
};
// Needed to init default queries before the async call
export const getInitialDefaultQueries = (): AlertQuery[] => {
const dataSource = getDefaultOrFirstCompatibleDataSource();
if (!dataSource) {
return [...getDefaultExpressions('A', 'B')];
}
@ -258,6 +218,7 @@ export const getInitialDefaultQueries = (): AlertQuery[] => {
hide: false,
},
},
...getDefaultExpressions('B', 'C'),
];
};