mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 20:54:22 -06:00
This commit is contained in:
parent
4e81aab60a
commit
282d021c53
@ -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"]
|
||||
],
|
||||
|
@ -19,12 +19,5 @@ export function getDataSourceSrv() {
|
||||
return {
|
||||
getList: () => [ds1],
|
||||
getInstanceSettings: () => ds1,
|
||||
get: () =>
|
||||
Promise.resolve({
|
||||
filterQuery: () => true,
|
||||
getDefaultQuery: () => ({
|
||||
expr: 'vector(1)',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -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([]);
|
||||
|
||||
|
@ -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([]);
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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([]);
|
||||
|
@ -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...'} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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) => ({
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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: '',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
@ -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'),
|
||||
];
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user