Elasticsearch: Decouple frontend dependencies from core (#82179)

* Elasticsearch: Decouple frontend dependencies from core

* Remove not needed code change
This commit is contained in:
Ivana Huckova 2024-02-09 13:11:08 +01:00 committed by GitHub
parent b1dc505a2b
commit 48b4ca8228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 282 additions and 84 deletions

View File

@ -113,7 +113,9 @@
"public/app/plugins/datasource/tempo/*.{ts,tsx}",
"public/app/plugins/datasource/tempo/**/*.{ts,tsx}",
"public/app/plugins/datasource/loki/*.{ts,tsx}",
"public/app/plugins/datasource/loki/**/*.{ts,tsx}"
"public/app/plugins/datasource/loki/**/*.{ts,tsx}",
"public/app/plugins/datasource/elasticsearch/*.{ts,tsx}",
"public/app/plugins/datasource/elasticsearch/**/*.{ts,tsx}"
],
"settings": {
"import/resolver": {

View File

@ -1,9 +1,9 @@
import { DataFrame, DataFrameView, Field, FieldCache, FieldType, KeyValue, MutableDataFrame } from '@grafana/data';
import flatten from 'app/core/utils/flatten';
import { ElasticResponse } from './ElasticResponse';
import { highlightTags } from './queryDef';
import { ElasticsearchQuery } from './types';
import { flattenObject } from './utils';
function getTimeField(frame: DataFrame): Field {
const field = frame.fields[0];
@ -1445,7 +1445,7 @@ describe('ElasticResponse', () => {
expect(r._id).toEqual(response.responses[0].hits.hits[i]._id);
expect(r._type).toEqual(response.responses[0].hits.hits[i]._type);
expect(r._index).toEqual(response.responses[0].hits.hits[i]._index);
expect(r._source).toEqual(flatten(response.responses[0].hits.hits[i]._source));
expect(r._source).toEqual(flattenObject(response.responses[0].hits.hits[i]._source));
}
// Make a map from the histogram results

View File

@ -10,13 +10,12 @@ import {
} from '@grafana/data';
import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType';
import TableModel from 'app/core/TableModel';
import flatten from 'app/core/utils/flatten';
import { isMetricAggregationWithField } from './components/QueryEditor/MetricAggregationsEditor/aggregations';
import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils';
import * as queryDef from './queryDef';
import { ElasticsearchAggregation, ElasticsearchQuery, TopMetrics, ExtendedStatMetaType } from './types';
import { describeMetric, getScriptValue } from './utils';
import { describeMetric, flattenObject, getScriptValue } from './utils';
const HIGHLIGHT_TAGS_EXP = `${queryDef.highlightTags.pre}([^@]+)${queryDef.highlightTags.post}`;
type TopMetricMetric = Record<string, number>;
@ -678,7 +677,7 @@ const flattenHits = (hits: Doc[]): { docs: Array<Record<string, any>>; propNames
let propNames: string[] = [];
for (const hit of hits) {
const flattened = hit._source ? flatten(hit._source) : {};
const flattened = hit._source ? flattenObject(hit._source) : {};
const doc = {
_id: hit._id,
_type: hit._type,

View File

@ -1,5 +1,3 @@
///<amd-dependency path="test/specs/helpers" name="helpers" />
import { toUtc, getLocale, setLocale, dateTime } from '@grafana/data';
import { IndexPattern } from './IndexPattern';

View File

@ -1,10 +1,9 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { DateHistogram } from 'app/plugins/datasource/elasticsearch/types';
import { select } from 'react-select-event';
import { useDispatch } from '../../../../hooks/useStatelessReducer';
import { DateHistogram } from '../../../../types';
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
@ -63,7 +62,7 @@ describe('DateHistogramSettingsEditor', () => {
expect(await screen.findByText('Calendar interval')).toBeInTheDocument();
expect(await screen.findByText('1w')).toBeInTheDocument();
await selectOptionInTest(screen.getByLabelText('Calendar interval'), '10s');
await select(screen.getByLabelText('Calendar interval'), '10s', { container: document.body });
expect(dispatch).toHaveBeenCalledTimes(1);
});
@ -79,7 +78,7 @@ describe('DateHistogramSettingsEditor', () => {
expect(await screen.findByText('Fixed interval')).toBeInTheDocument();
expect(await screen.findByText('1m')).toBeInTheDocument();
await selectOptionInTest(screen.getByLabelText('Fixed interval'), '1q');
await select(screen.getByLabelText('Fixed interval'), '1q', { container: document.body });
expect(dispatch).toHaveBeenCalledTimes(1);
});

View File

@ -4,8 +4,8 @@ import { GroupBase, OptionsOrGroups } from 'react-select';
import { InternalTimeZones, SelectableValue } from '@grafana/data';
import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui';
import { calendarIntervals } from 'app/plugins/datasource/elasticsearch/QueryBuilder';
import { calendarIntervals } from '../../../../QueryBuilder';
import { useDispatch } from '../../../../hooks/useStatelessReducer';
import { DateHistogram } from '../../../../types';
import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour';

View File

@ -2,10 +2,9 @@ import { screen } from '@testing-library/react';
import React from 'react';
import selectEvent from 'react-select-event';
import { describeMetric } from 'app/plugins/datasource/elasticsearch/utils';
import { renderWithESProvider } from '../../../../test-helpers/render';
import { ElasticsearchQuery, Terms, Average, Derivative, TopMetrics } from '../../../../types';
import { describeMetric } from '../../../../utils';
import { TermsSettingsEditor } from './TermsSettingsEditor';

View File

@ -1,5 +1,4 @@
import { defaultGeoHashPrecisionString } from 'app/plugins/datasource/elasticsearch/queryDef';
import { defaultGeoHashPrecisionString } from '../../../../queryDef';
import { BucketAggregation } from '../../../../types';
import { describeMetric, convertOrderByToMetricId } from '../../../../utils';
import { useQuery } from '../../ElasticsearchQueryContext';

View File

@ -1,9 +1,6 @@
import { reducerTester } from 'test/core/redux/reducerTester';
import { defaultBucketAgg } from 'app/plugins/datasource/elasticsearch/queryDef';
import { ElasticsearchQuery } from 'app/plugins/datasource/elasticsearch/types';
import { BucketAggregation, DateHistogram } from '../../../../types';
import { defaultBucketAgg } from '../../../../queryDef';
import { BucketAggregation, DateHistogram, ElasticsearchQuery } from '../../../../types';
import { reducerTester } from '../../../reducerTester';
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
import { initQuery } from '../../state';
import { bucketAggregationConfig } from '../utils';

View File

@ -1,6 +1,5 @@
import { reducerTester } from 'test/core/redux/reducerTester';
import { PipelineVariable } from '../../../../../../types';
import { reducerTester } from '../../../../../reducerTester';
import {
addPipelineVariable,

View File

@ -2,10 +2,10 @@ import { uniqueId } from 'lodash';
import React, { ComponentProps, useState } from 'react';
import { InlineField, Input } from '@grafana/ui';
import { getScriptValue } from 'app/plugins/datasource/elasticsearch/utils';
import { useDispatch } from '../../../../hooks/useStatelessReducer';
import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../../../../types';
import { getScriptValue } from '../../../../utils';
import { SettingKeyOf } from '../../../types';
import { changeMetricSetting } from '../state/actions';

View File

@ -1,9 +1,6 @@
import { reducerTester } from 'test/core/redux/reducerTester';
import { ElasticsearchQuery } from 'app/plugins/datasource/elasticsearch/types';
import { defaultMetricAgg } from '../../../../queryDef';
import { Derivative, ExtendedStats, MetricAggregation } from '../../../../types';
import { Derivative, ElasticsearchQuery, ExtendedStats, MetricAggregation } from '../../../../types';
import { reducerTester } from '../../../reducerTester';
import { initQuery } from '../../state';
import { metricAggregationConfig } from '../utils';

View File

@ -1,6 +1,5 @@
import { reducerTester } from 'test/core/redux/reducerTester';
import { ElasticsearchQuery } from '../../types';
import { reducerTester } from '../reducerTester';
import { aliasPatternReducer, changeAliasPattern, changeQuery, initQuery, queryReducer } from './state';

View File

@ -0,0 +1,109 @@
import { AnyAction } from '@reduxjs/toolkit';
import { cloneDeep } from 'lodash';
import { Action } from 'redux';
import { StoreState } from 'app/types';
type GrafanaReducer<S = StoreState, A extends Action = AnyAction> = (state: S, action: A) => S;
export interface Given<State> {
givenReducer: (
reducer: GrafanaReducer<State, AnyAction>,
state: State,
showDebugOutput?: boolean,
disableDeepFreeze?: boolean
) => When<State>;
}
export interface When<State> {
whenActionIsDispatched: (action: AnyAction) => Then<State>;
}
export interface Then<State> {
thenStateShouldEqual: (state: State) => When<State>;
thenStatePredicateShouldEqual: (predicate: (resultingState: State) => boolean) => When<State>;
whenActionIsDispatched: (action: AnyAction) => Then<State>;
}
const isNotException = (object: unknown, propertyName: string) =>
typeof object === 'function'
? propertyName !== 'caller' && propertyName !== 'callee' && propertyName !== 'arguments'
: true;
export const deepFreeze = <T>(obj: T): T => {
if (typeof obj === 'object') {
for (const key in obj) {
const prop = obj[key];
if (
prop &&
Object.hasOwn(obj, key) &&
isNotException(obj, key) &&
(typeof prop === 'object' || typeof prop === 'function') &&
!Object.isFrozen(prop)
) {
deepFreeze(prop);
}
}
}
return Object.freeze(obj);
};
interface ReducerTester<State> extends Given<State>, When<State>, Then<State> {}
export const reducerTester = <State>(): Given<State> => {
let reducerUnderTest: GrafanaReducer<State, AnyAction>;
let resultingState: State;
let initialState: State;
let showDebugOutput = false;
const givenReducer = (
reducer: GrafanaReducer<State, AnyAction>,
state: State,
debug = false,
disableDeepFreeze = false
): When<State> => {
reducerUnderTest = reducer;
initialState = cloneDeep(state);
if (!disableDeepFreeze && (typeof state === 'object' || typeof state === 'function')) {
deepFreeze(initialState);
}
showDebugOutput = debug;
return instance;
};
const whenActionIsDispatched = (action: AnyAction): Then<State> => {
resultingState = reducerUnderTest(resultingState || initialState, action);
return instance;
};
const thenStateShouldEqual = (state: State): When<State> => {
if (showDebugOutput) {
console.log(JSON.stringify(resultingState, null, 2));
}
expect(resultingState).toEqual(state);
return instance;
};
const thenStatePredicateShouldEqual = (predicate: (resultingState: State) => boolean): When<State> => {
if (showDebugOutput) {
console.log(JSON.stringify(resultingState, null, 2));
}
expect(predicate(resultingState)).toBe(true);
return instance;
};
const instance: ReducerTester<State> = {
thenStateShouldEqual,
thenStatePredicateShouldEqual,
givenReducer,
whenActionIsDispatched,
};
return instance;
};

View File

@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import { ConfigEditor } from './ConfigEditor';
import { createDefaultConfigOptions } from './mocks';
import { createDefaultConfigOptions } from './__mocks__/configOptions';
describe('ConfigEditor', () => {
it('should render without error', () => {

View File

@ -11,8 +11,8 @@ import {
convertLegacyAuthProps,
DataSourceDescription,
} from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Alert, SecureSocksProxySettings, Divider, Stack } from '@grafana/ui';
import { config } from 'app/core/config';
import { ElasticsearchOptions } from '../types';

View File

@ -3,6 +3,7 @@ import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { usePrevious } from 'react-use';
import { DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import {
Button,
DataLinkInput,
@ -13,7 +14,6 @@ import {
Input,
useStyles2,
} from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { DataLinkConfig } from '../types';

View File

@ -3,7 +3,7 @@ import React from 'react';
import selectEvent from 'react-select-event';
import { ElasticDetails } from './ElasticDetails';
import { createDefaultConfigOptions } from './mocks';
import { createDefaultConfigOptions } from './__mocks__/configOptions';
describe('ElasticDetails', () => {
describe('Max concurrent Shard Requests', () => {

View File

@ -2,7 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import { LogsConfig } from './LogsConfig';
import { createDefaultConfigOptions } from './mocks';
import { createDefaultConfigOptions } from './__mocks__/configOptions';
describe('ElasticDetails', () => {
it('should pass correct data to onChange', () => {

View File

@ -0,0 +1,17 @@
import { DataSourceSettings } from '@grafana/data';
import { ElasticsearchOptions } from '../../types';
export function createDefaultConfigOptions(): DataSourceSettings<ElasticsearchOptions> {
return {
jsonData: {
timeField: '@time',
interval: 'Hourly',
timeInterval: '10s',
maxConcurrentShardRequests: 300,
logMessageField: 'test.message',
logLevelField: 'test.level',
},
secureJsonFields: {},
} as DataSourceSettings<ElasticsearchOptions>;
}

View File

@ -1,20 +0,0 @@
import { DataSourceSettings } from '@grafana/data';
import { getMockDataSource } from 'app/features/datasources/__mocks__';
import { ElasticsearchOptions } from '../types';
export function createDefaultConfigOptions(
options?: Partial<ElasticsearchOptions>
): DataSourceSettings<ElasticsearchOptions> {
return getMockDataSource<ElasticsearchOptions>({
jsonData: {
timeField: '@time',
interval: 'Hourly',
timeInterval: '10s',
maxConcurrentShardRequests: 300,
logMessageField: 'test.message',
logLevelField: 'test.level',
...options,
},
});
}

View File

@ -1,6 +1,5 @@
import { map } from 'lodash';
import { Observable, of, throwError } from 'rxjs';
import { getQueryOptions } from 'test/helpers/getQueryOptions';
import {
CoreApp,
@ -18,9 +17,6 @@ import {
toUtc,
} from '@grafana/data';
import { BackendSrvRequest, FetchResponse, reportInteraction, config } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { createFetchResponse } from '../../../../test/helpers/createFetchResponse';
import { enhanceDataFrame } from './LegacyQueryRunner';
import { ElasticDatasource } from './datasource';
@ -30,7 +26,9 @@ import { Filters, ElasticsearchOptions, ElasticsearchQuery } from './types';
const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local';
const originalConsoleError = console.error;
const backendSrv = {
fetch: jest.fn(),
};
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
@ -44,6 +42,15 @@ jest.mock('@grafana/runtime', () => ({
},
}));
const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({
from,
to,
raw: {
from,
to,
},
});
const TIME_START = [2022, 8, 21, 6, 10, 10];
const TIME_END = [2022, 8, 24, 6, 10, 21];
const DATAQUERY_BASE = {
@ -56,16 +63,22 @@ const DATAQUERY_BASE = {
timezone: '',
app: 'test',
startTime: 0,
range: createTimeRange(toUtc(TIME_START), toUtc(TIME_END)),
};
const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({
from,
to,
raw: {
from,
to,
},
});
function createFetchResponse<T>(data: T): FetchResponse<T> {
return {
data,
status: 200,
url: 'http://localhost:3000/api/ds/query',
config: { url: 'http://localhost:3000/api/ds/query' },
type: 'basic',
statusText: 'Ok',
redirected: false,
headers: {} as unknown as Headers,
ok: true,
};
}
interface TestContext {
data?: Data;
@ -977,27 +990,29 @@ describe('ElasticDatasource', () => {
});
it('does not create a logs sample provider for non time series query', () => {
const options = getQueryOptions<ElasticsearchQuery>({
const options: DataQueryRequest<ElasticsearchQuery> = {
...DATAQUERY_BASE,
targets: [
{
refId: 'A',
metrics: [{ type: 'logs', id: '1', settings: { limit: '100' } }],
},
],
});
};
expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, options)).not.toBeDefined();
});
it('does create a logs sample provider for time series query', () => {
const options = getQueryOptions<ElasticsearchQuery>({
const options: DataQueryRequest<ElasticsearchQuery> = {
...DATAQUERY_BASE,
targets: [
{
refId: 'A',
bucketAggs: [{ type: 'date_histogram', id: '1' }],
},
],
});
};
expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, options)).toBeDefined();
});
@ -1010,27 +1025,29 @@ describe('ElasticDatasource', () => {
});
it("doesn't return a logs sample provider given a non time series query", () => {
const request = getQueryOptions<ElasticsearchQuery>({
const request: DataQueryRequest<ElasticsearchQuery> = {
...DATAQUERY_BASE,
targets: [
{
refId: 'A',
metrics: [{ type: 'logs', id: '1', settings: { limit: '100' } }],
},
],
});
};
expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, request)).not.toBeDefined();
});
it('returns a logs sample provider given a time series query', () => {
const request = getQueryOptions<ElasticsearchQuery>({
const request: DataQueryRequest<ElasticsearchQuery> = {
...DATAQUERY_BASE,
targets: [
{
refId: 'A',
bucketAggs: [{ type: 'date_histogram', id: '1' }],
},
],
});
};
expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, request)).toBeDefined();
});

View File

@ -38,6 +38,7 @@ import {
AdHocVariableFilter,
DataSourceWithQueryModificationSupport,
AdHocVariableModel,
TypedVariableModel,
} from '@grafana/data';
import {
DataSourceWithBackend,
@ -47,7 +48,6 @@ import {
TemplateSrv,
getTemplateSrv,
} from '@grafana/runtime';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { IndexPattern, intervalMap } from './IndexPattern';
import LanguageProvider from './LanguageProvider';
@ -264,7 +264,8 @@ export class ElasticDatasource
private prepareAnnotationRequest(options: {
annotation: ElasticsearchAnnotationQuery;
dashboard: DashboardModel;
// Should be DashboardModel but cannot import that here from the main app. This is a temporary solution as we need to move from deprecated annotations.
dashboard: { getVariables: () => TypedVariableModel[] };
range: TimeRange;
}) {
const annotation = options.annotation;

View File

@ -1,10 +1,10 @@
import { CoreApp, DashboardLoadedEvent, DataQueryRequest, DataQueryResponse } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { variableRegex } from 'app/features/variables/utils';
import { REF_ID_STARTER_LOG_VOLUME } from './datasource';
import pluginJson from './plugin.json';
import { ElasticsearchAnnotationQuery, ElasticsearchQuery } from './types';
import { variableRegex } from './utils';
type ElasticSearchOnDashboardLoadedTrackingEvent = {
grafana_version?: string;

View File

@ -1,5 +1,5 @@
import { ElasticsearchQuery } from './types';
import { isTimeSeriesQuery, removeEmpty } from './utils';
import { flattenObject, isTimeSeriesQuery, removeEmpty } from './utils';
describe('removeEmpty', () => {
it('Should remove all empty', () => {
@ -79,3 +79,39 @@ describe('isTimeSeriesQuery', () => {
expect(isTimeSeriesQuery(query)).toBe(true);
});
});
describe('flattenObject', () => {
it('flattens objects of arbitrary depth', () => {
const nestedObject = {
a: {
b: {
c: 1,
d: {
e: 2,
f: 3,
},
},
g: 4,
},
h: 5,
};
expect(flattenObject(nestedObject)).toEqual({
'a.b.c': 1,
'a.b.d.e': 2,
'a.b.d.f': 3,
'a.g': 4,
h: 5,
});
});
it('does not alter other objects', () => {
const nestedObject = {
a: 'uno',
b: 'dos',
c: 3,
};
expect(flattenObject(nestedObject)).toEqual(nestedObject);
});
});

View File

@ -106,3 +106,53 @@ export const unsupportedVersionMessage =
export const isTimeSeriesQuery = (query: ElasticsearchQuery): boolean => {
return query?.bucketAggs?.slice(-1)[0]?.type === 'date_histogram';
};
/*
* This regex matches 3 types of variable reference with an optional format specifier
* There are 6 capture groups that replace will return
* \$(\w+) $var1
* \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
* \${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?} ${var3} or ${var3.fieldPath} or ${var3:fmt3} (or ${var3.fieldPath:fmt3} but that is not a separate capture group)
*/
export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
// Copyright (c) 2014, Hugh Kennedy
// Based on code from https://github.com/hughsk/flat/blob/master/index.js
//
export function flattenObject(
target: Record<string, unknown>,
opts?: { delimiter?: string; maxDepth?: number; safe?: boolean }
): Record<string, unknown> {
opts = opts || {};
const delimiter = opts.delimiter || '.';
let maxDepth = opts.maxDepth || 3;
let currentDepth = 1;
const output: Record<string, unknown> = {};
function step(object: Record<string, unknown>, prev: string | null) {
Object.keys(object).forEach((key) => {
const value = object[key];
const isarray = opts?.safe && Array.isArray(value);
const type = Object.prototype.toString.call(value);
const isobject = type === '[object Object]';
const newKey = prev ? prev + delimiter + key : key;
if (!opts?.maxDepth) {
maxDepth = currentDepth + 1;
}
if (!isarray && isobject && value && Object.keys(value).length && currentDepth < maxDepth) {
++currentDepth;
return step({ ...value }, newKey);
}
output[newKey] = value;
});
}
step(target, null);
return output;
}