From b42f973a7ae27d9739d41b8b6dd21de2a9cec59e Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 24 Feb 2023 13:43:54 +0100 Subject: [PATCH] Alerting: Fix changing datasource and creating new query not using defaults. (#63092) * Set default query when changing data source or adding new alert query * Set default query when creating new alert rule * Set fefault query for cloud and recording rules * Create hook for getting defaults in AlertRuleForm * fixing tests * Use conditionals with 'if' for more clarity and rename lazy to async * Add loading indicator for default queries * Fix tests * Make newModel a sync method and fix tests error * Use useAsync instead of useEffect for an async call --- .../src/services/__mocks__/dataSourceSrv.ts | 7 ++ .../RuleEditorCloudOnlyAllowed.test.tsx | 6 +- .../unified/RuleEditorCloudRules.test.tsx | 7 +- .../unified/RuleEditorExisting.test.tsx | 41 +++++- .../unified/RuleEditorGrafanaRules.test.tsx | 118 ++++++++++++++++-- .../unified/RuleEditorRecordingRule.test.tsx | 8 +- .../components/rule-editor/AlertRuleForm.tsx | 80 ++++++++++-- .../rule-editor/ExpressionEditor.tsx | 34 ++++- .../components/rule-editor/QueryRows.tsx | 66 +++++++--- .../QueryAndExpressionsStep.tsx | 38 ++++-- .../__snapshots__/reducer.test.tsx.snap | 16 ++- .../reducer.test.tsx | 29 ++++- .../query-and-alert-condition/reducer.ts | 25 ++-- .../alerting/unified/utils/rule-form.ts | 43 ++++++- 14 files changed, 441 insertions(+), 77 deletions(-) diff --git a/packages/grafana-runtime/src/services/__mocks__/dataSourceSrv.ts b/packages/grafana-runtime/src/services/__mocks__/dataSourceSrv.ts index 61995de2b0a..13b565b9ef2 100644 --- a/packages/grafana-runtime/src/services/__mocks__/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/__mocks__/dataSourceSrv.ts @@ -19,5 +19,12 @@ export function getDataSourceSrv() { return { getList: () => [ds1], getInstanceSettings: () => ds1, + get: () => + Promise.resolve({ + filterQuery: () => true, + getDefaultQuery: () => ({ + expr: 'vector(1)', + }), + }), }; } diff --git a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx index 01c66b8cf27..3d900bbd148 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorCloudOnlyAllowed.test.tsx @@ -2,6 +2,7 @@ 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'; @@ -159,7 +160,10 @@ describe('RuleEditor cloud: checking editable data sources', () => { return null; }); - setDataSourceSrv(new MockDataSourceSrv(dataSources)); + const dsServer = new MockDataSourceSrv(dataSources); + jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds')); + + setDataSourceSrv(dsServer); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.searchFolders.mockResolvedValue([]); diff --git a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx index b1429b3a32d..35128cbbc8f 100644 --- a/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorCloudRules.test.tsx @@ -1,8 +1,9 @@ -import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import { screen, waitFor, waitForElementToBeRemoved, within } 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'; @@ -79,8 +80,10 @@ describe('RuleEditor cloud', () => { { alerting: true } ), }; + const dsServer = new MockDataSourceSrv(dataSources); + jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds')); - setDataSourceSrv(new MockDataSourceSrv(dataSources)); + setDataSourceSrv(dsServer); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.setRulerRuleGroup.mockResolvedValue(); mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); diff --git a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx index ebb6b894b44..c9677548952 100644 --- a/public/app/features/alerting/unified/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorExisting.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor, screen, within } from '@testing-library/react'; +import { render, screen, waitFor, 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 { getDefaultQueries } from './utils/rule-form'; +import * as ruleForm from './utils/rule-form'; jest.mock('./components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name @@ -46,12 +46,14 @@ 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), @@ -119,6 +121,17 @@ 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]: [ { @@ -133,8 +146,16 @@ describe('RuleEditor grafana managed rules', () => { uid, namespace_uid: 'abcd', namespace_id: 1, - condition: 'B', - data: getDefaultQueries(), + condition: 'A', + data: [ + { + refId: 'A', + relativeTimeRange: { from: 900, to: 1000 }, + datasourceUid: 'dsuid', + model: { refId: 'A' }, + queryType: 'query', + }, + ], exec_err_state: GrafanaAlertStateDecision.Error, no_data_state: GrafanaAlertStateDecision.NoData, title: 'my great new rule', @@ -202,8 +223,16 @@ describe('RuleEditor grafana managed rules', () => { for: '5m', grafana_alert: { uid, - condition: 'B', - data: getDefaultQueries(), + condition: 'A', + data: [ + { + refId: 'A', + relativeTimeRange: { from: 900, to: 1000 }, + datasourceUid: 'dsuid', + model: { refId: 'A' }, + queryType: 'query', + }, + ], exec_err_state: GrafanaAlertStateDecision.Error, is_paused: false, no_data_state: 'NoData', diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx index 99e943fb74f..f810ee9497a 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx @@ -1,8 +1,9 @@ -import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import { screen, waitFor, waitForElementToBeRemoved, within } 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'; @@ -19,7 +20,6 @@ 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,11 +74,14 @@ describe('RuleEditor grafana managed rules', () => { name: 'Prom', isDefault: true, }, - { alerting: false } + { alerting: true } ), }; - setDataSourceSrv(new MockDataSourceSrv(dataSources)); + const dsServer = new MockDataSourceSrv(dataSources); + jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds')); + + setDataSourceSrv(dsServer); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.setRulerRuleGroup.mockResolvedValue(); mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); @@ -121,6 +124,7 @@ describe('RuleEditor grafana managed rules', () => { rulerApiEnabled: false, }, }); + renderRuleEditor(); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); @@ -142,6 +146,7 @@ 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 @@ -149,20 +154,107 @@ describe('RuleEditor grafana managed rules', () => { { dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' }, 'Folder A', { - interval: '1m', name: 'group1', + interval: '1m', rules: [ { - annotations: { description: 'some description' }, - labels: { severity: 'warn' }, - for: '5m', grafana_alert: { - condition: 'B', - data: getDefaultQueries(), - exec_err_state: GrafanaAlertStateDecision.Error, - is_paused: false, - no_data_state: 'NoData', 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, + }, + for: '5m', + annotations: { + description: 'some description', + }, + labels: { + severity: 'warn', }, }, ], diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx index 9573d851405..946dbe273ea 100644 --- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx @@ -1,8 +1,9 @@ -import { waitFor, screen, within, waitForElementToBeRemoved } from '@testing-library/react'; +import { screen, waitFor, waitForElementToBeRemoved, within } 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'; @@ -75,7 +76,10 @@ describe('RuleEditor recording rules', () => { ), }; - setDataSourceSrv(new MockDataSourceSrv(dataSources)); + const dsServer = new MockDataSourceSrv(dataSources); + jest.spyOn(dsServer, 'get').mockResolvedValue(new MockDataSourceApi('ds')); + + setDataSourceSrv(dsServer); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.setRulerRuleGroup.mockResolvedValue(); mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index 0e7a6c485a2..f822db3a53f 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -2,23 +2,40 @@ 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 { GrafanaTheme2 } from '@grafana/data'; -import { logInfo, config } from '@grafana/runtime'; -import { Button, ConfirmModal, CustomScrollbar, Spinner, useStyles2, HorizontalGroup, Field, Input } from '@grafana/ui'; +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 { 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, getDefaultQueries, rulerRuleToFormValues } from '../../utils/rule-form'; +import { + getDefaultFormValues, + getDefaultQueriesAsync, + getInitialDefaultQueries, + rulerRuleToFormValues, +} from '../../utils/rule-form'; import * as ruleId from '../../utils/rule-id'; import { CloudEvaluationBehavior } from './CloudEvaluationBehavior'; @@ -73,6 +90,24 @@ type Props = { prefill?: Partial; // Existing implies we modify existing rule. Prefill only provides default form values }; +export const useGetDefaults = (queryParams: UrlQueryMap, existing: RuleWithLocation | undefined) => { + const [defaultDsAndQueries, setDefaultDsAndQueries] = useState<{ + queries: AlertQuery[] | null; + ds?: DataSourceApi; + }>({ 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 }; +}; + export const AlertRuleForm: FC = ({ existing, prefill }) => { const styles = useStyles2(getStyles); const dispatch = useDispatch(); @@ -84,6 +119,8 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => { const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list'; const [showDeleteModal, setShowDeleteModal] = useState(false); + const { defaultDsAndQueries, defaultsInQueryParamsObject } = useGetDefaults(queryParams, existing); + const defaultValues: RuleFormValues = useMemo(() => { if (existing) { return rulerRuleToFormValues(existing); @@ -98,13 +135,13 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => { return { ...getDefaultFormValues(), - queries: getDefaultQueries(), + queries: getInitialDefaultQueries(), condition: 'C', - ...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}), + ...defaultsInQueryParamsObject, type: RuleFormType.grafana, evaluateEvery: evaluateEvery, }; - }, [existing, prefill, queryParams, evaluateEvery]); + }, [existing, prefill, evaluateEvery, defaultsInQueryParamsObject]); const formAPI = useForm({ mode: 'onSubmit', @@ -112,7 +149,22 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => { shouldFocusError: true, }); - const { handleSubmit, watch } = formAPI; + 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; + if (shouldReset) { + reset({ + ...getDefaultFormValues(), + queries: defaultDsAndQueries.queries, + condition: 'C', + ...defaultsInQueryParamsObject, + type: RuleFormType.grafana, + evaluateEvery: evaluateEvery, + }); + } + }, [defaultDsAndQueries.queries, reset, existing, prefill, defaultsInQueryParamsObject, evaluateEvery]); const type = watch('type'); const dataSourceName = watch('dataSourceName'); @@ -194,7 +246,7 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => { const evaluateEveryInForm = watch('evaluateEvery'); useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); - return ( + return defaultDsAndQueries.queries ? (
e.preventDefault()} className={styles.form}> @@ -247,7 +299,13 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => {
- + {showStep2 && ( <> {type === RuleFormType.grafana ? ( @@ -281,6 +339,8 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => { ) : null} {showEditYaml ? setShowEditYaml(false)} /> : null} + ) : ( + ); }; 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 a803455386d..28a03209c20 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { noop } from 'lodash'; -import React, { FC, useCallback, useMemo } from 'react'; -import { useAsync } from 'react-use'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { useAsync, usePrevious } from 'react-use'; import { CoreApp, DataQuery, DataSourcePluginContextProvider, GrafanaTheme2, LoadingState } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; @@ -9,6 +9,8 @@ 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'; @@ -17,6 +19,8 @@ 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 = ({ @@ -24,11 +28,31 @@ export const ExpressionEditor: FC = ({ onChange, dataSourceName, showPreviewAlertsButton = true, + asyncDefaultQuery, + preservePreviousValue, }) => { const styles = useStyles2(getStyles); const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName); - const dataQuery = mapToQuery({ refId: 'A', hide: false }, value); + 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 { error, @@ -71,7 +95,7 @@ export const ExpressionEditor: FC = ({ // 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 ( + return dataQuery ? ( <> = ({
)} - ); + ) : null; }; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index 60acdd0011a..51d10edd106 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -2,7 +2,16 @@ import { omit } from 'lodash'; import React, { PureComponent, useState } from 'react'; import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; -import { DataQuery, DataSourceInstanceSettings, LoadingState, PanelData, RelativeTimeRange } from '@grafana/data'; +import { + CoreApp, + DataQuery, + DataSourceApi, + DataSourceInstanceSettings, + DataSourceJsonData, + 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'; @@ -66,22 +75,30 @@ export class QueryRows extends PureComponent { ); }; - onChangeDataSource = (settings: DataSourceInstanceSettings, index: number) => { + onChangeDataSource = async (settings: DataSourceInstanceSettings, index: number) => { const { queries, onQueriesChange } = this.props; - const updatedQueries = queries.map((item, itemIndex) => { - if (itemIndex !== index) { - return item; - } + const updatedQueries = await Promise.all( + queries.map(async (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); - } - return newModel(item, settings); - }); + // 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); + }) + ); onQueriesChange(updatedQueries); }; @@ -218,13 +235,34 @@ function copyModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit }; } -function newModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit { +function newModel( + item: AlertQuery, + settings: DataSourceInstanceSettings, + ds?: DataSourceApi +): Omit { + 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, + }, + }, + }; + } return { refId: item.refId, relativeTimeRange: item.relativeTimeRange, queryType: '', datasourceUid: settings.uid, model: { + ...ds?.getDefaultQuery?.(CoreApp.UnifiedAlerting), refId: item.refId, hide: false, datasource: { diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 4b426163f84..1b7163ddcb2 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { useFormContext } from 'react-hook-form'; -import { LoadingState, PanelData } from '@grafana/data'; +import { DataQuery, DataSourceApi, DataSourceJsonData, LoadingState, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; @@ -35,16 +35,25 @@ import { interface Props { editingExistingRule: boolean; + prefill: boolean; onDataChange: (error: string) => void; + asyncDefaultQueries?: AlertQuery[]; + asyncDataSource?: DataSourceApi; } -export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onDataChange }) => { +export const QueryAndExpressionsStep: FC = ({ + editingExistingRule, + onDataChange, + asyncDefaultQueries, + asyncDataSource, + prefill, +}) => { const runner = useRef(new AlertingQueryRunner()); const { setValue, getValues, watch, - formState: { errors }, + formState: { errors, isDirty }, control, } = useFormContext(); const [panelData, setPanelData] = useState>({}); @@ -54,7 +63,6 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onData panelData: {}, }; const [{ queries }, dispatch] = useReducer(queriesAndExpressionsReducer, initialState); - const [type, condition, dataSourceName] = watch(['type', 'condition', 'dataSourceName']); const isGrafanaManagedType = type === RuleFormType.grafana; @@ -71,6 +79,15 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onData 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 }); @@ -176,6 +193,13 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onData } }, [condition, queries, handleSetCondition]); + const defaultQueryAvailable = asyncDataSource && asyncDefaultQueries; + const onAddNewQuery = () => { + if (defaultQueryAvailable) { + dispatch(addNewDataQuery({ ds: asyncDataSource, defaultQuery: asyncDefaultQueries[0]?.model })); + } + }; + return ( @@ -191,6 +215,8 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onData {...field} dataSourceName={dataSourceName} showPreviewAlertsButton={!isRecordingRuleType} + asyncDefaultQuery={asyncDefaultQueries?.[0]} + preservePreviousValue={!updateWithDefault} /> ); }} @@ -239,9 +265,7 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onData