mirror of https://github.com/grafana/grafana
Elasticsearch: Allow omitting field when metric supports inline script (#32839)
* Elasticsearch: Allow omitting field when metric supports inline script * Add tests for MetricEditor to show a None option * Add tests for useFields hook * Alerting: allow elasticsearch metrics without fieldpull/32997/head
parent
4b801be98c
commit
cdb4785496
@ -0,0 +1,85 @@ |
||||
import { act, fireEvent, render, screen } from '@testing-library/react'; |
||||
import { ElasticsearchProvider } from '../ElasticsearchQueryContext'; |
||||
import { MetricEditor } from './MetricEditor'; |
||||
import React, { ReactNode } from 'react'; |
||||
import { ElasticDatasource } from '../../../datasource'; |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
import { ElasticsearchQuery } from '../../../types'; |
||||
import { Average, UniqueCount } from './aggregations'; |
||||
import { defaultBucketAgg } from '../../../query_def'; |
||||
import { from } from 'rxjs'; |
||||
|
||||
describe('Metric Editor', () => { |
||||
it('Should display a "None" option for "field" if the metric supports inline script', async () => { |
||||
const avg: Average = { |
||||
id: '1', |
||||
type: 'avg', |
||||
}; |
||||
|
||||
const query: ElasticsearchQuery = { |
||||
refId: 'A', |
||||
query: '', |
||||
metrics: [avg], |
||||
bucketAggs: [defaultBucketAgg('2')], |
||||
}; |
||||
|
||||
const getFields: ElasticDatasource['getFields'] = jest.fn(() => from([[]])); |
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => ( |
||||
<ElasticsearchProvider |
||||
datasource={{ getFields } as ElasticDatasource} |
||||
query={query} |
||||
range={getDefaultTimeRange()} |
||||
onChange={() => {}} |
||||
onRunQuery={() => {}} |
||||
> |
||||
{children} |
||||
</ElasticsearchProvider> |
||||
); |
||||
|
||||
render(<MetricEditor value={avg} />, { wrapper }); |
||||
|
||||
act(() => { |
||||
fireEvent.click(screen.getByText('Select Field')); |
||||
}); |
||||
|
||||
expect(await screen.findByText('None')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('Should not display a "None" option for "field" if the metric does not support inline script', async () => { |
||||
const avg: UniqueCount = { |
||||
id: '1', |
||||
type: 'cardinality', |
||||
}; |
||||
|
||||
const query: ElasticsearchQuery = { |
||||
refId: 'A', |
||||
query: '', |
||||
metrics: [avg], |
||||
bucketAggs: [defaultBucketAgg('2')], |
||||
}; |
||||
|
||||
const getFields: ElasticDatasource['getFields'] = jest.fn(() => from([[]])); |
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => ( |
||||
<ElasticsearchProvider |
||||
datasource={{ getFields } as ElasticDatasource} |
||||
query={query} |
||||
range={getDefaultTimeRange()} |
||||
onChange={() => {}} |
||||
onRunQuery={() => {}} |
||||
> |
||||
{children} |
||||
</ElasticsearchProvider> |
||||
); |
||||
|
||||
render(<MetricEditor value={avg} />, { wrapper }); |
||||
|
||||
act(() => { |
||||
fireEvent.click(screen.getByText('Select Field')); |
||||
}); |
||||
|
||||
expect(await screen.findByText('No options found')).toBeInTheDocument(); |
||||
expect(screen.queryByText('None')).not.toBeInTheDocument(); |
||||
}); |
||||
}); |
@ -0,0 +1,73 @@ |
||||
import React, { ReactNode } from 'react'; |
||||
import { ElasticDatasource } from '../datasource'; |
||||
import { from } from 'rxjs'; |
||||
import { ElasticsearchProvider } from '../components/QueryEditor/ElasticsearchQueryContext'; |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
import { ElasticsearchQuery } from '../types'; |
||||
import { defaultBucketAgg, defaultMetricAgg } from '../query_def'; |
||||
import { renderHook } from '@testing-library/react-hooks'; |
||||
import { useFields } from './useFields'; |
||||
import { MetricAggregationType } from '../components/QueryEditor/MetricAggregationsEditor/aggregations'; |
||||
import { BucketAggregationType } from '../components/QueryEditor/BucketAggregationsEditor/aggregations'; |
||||
|
||||
describe('useFields hook', () => { |
||||
// TODO: If we move the field type to the configuration objects as described in the hook's source
|
||||
// we can stop testing for getField to be called with the correct parameters.
|
||||
it("returns a function that calls datasource's getFields with the correct parameters", async () => { |
||||
const timeRange = getDefaultTimeRange(); |
||||
const query: ElasticsearchQuery = { |
||||
refId: 'A', |
||||
query: '', |
||||
metrics: [defaultMetricAgg()], |
||||
bucketAggs: [defaultBucketAgg()], |
||||
}; |
||||
|
||||
const getFields: ElasticDatasource['getFields'] = jest.fn(() => from([[]])); |
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => ( |
||||
<ElasticsearchProvider |
||||
datasource={{ getFields } as ElasticDatasource} |
||||
query={query} |
||||
range={timeRange} |
||||
onChange={() => {}} |
||||
onRunQuery={() => {}} |
||||
> |
||||
{children} |
||||
</ElasticsearchProvider> |
||||
); |
||||
|
||||
//
|
||||
// METRIC AGGREGATIONS
|
||||
//
|
||||
// Cardinality works on every kind of data
|
||||
const { result, rerender } = renderHook( |
||||
(aggregationType: BucketAggregationType | MetricAggregationType) => useFields(aggregationType), |
||||
{ wrapper, initialProps: 'cardinality' } |
||||
); |
||||
result.current(); |
||||
expect(getFields).toHaveBeenLastCalledWith(undefined, timeRange); |
||||
|
||||
// All other metric aggregations only work on numbers
|
||||
rerender('avg'); |
||||
result.current(); |
||||
expect(getFields).toHaveBeenLastCalledWith('number', timeRange); |
||||
|
||||
//
|
||||
// BUCKET AGGREGATIONS
|
||||
//
|
||||
// Date Histrogram only works on dates
|
||||
rerender('date_histogram'); |
||||
result.current(); |
||||
expect(getFields).toHaveBeenLastCalledWith('date', timeRange); |
||||
|
||||
// Geohash Grid only works on geo_point data
|
||||
rerender('geohash_grid'); |
||||
result.current(); |
||||
expect(getFields).toHaveBeenLastCalledWith('geo_point', timeRange); |
||||
|
||||
// All other bucket aggregation work on any kind of data
|
||||
rerender('terms'); |
||||
result.current(); |
||||
expect(getFields).toHaveBeenLastCalledWith(undefined, timeRange); |
||||
}); |
||||
}); |
@ -0,0 +1,53 @@ |
||||
import { MetricFindValue, SelectableValue } from '@grafana/data'; |
||||
import { BucketAggregationType } from '../components/QueryEditor/BucketAggregationsEditor/aggregations'; |
||||
import { useDatasource, useRange } from '../components/QueryEditor/ElasticsearchQueryContext'; |
||||
import { |
||||
isMetricAggregationType, |
||||
MetricAggregationType, |
||||
} from '../components/QueryEditor/MetricAggregationsEditor/aggregations'; |
||||
|
||||
type AggregationType = BucketAggregationType | MetricAggregationType; |
||||
|
||||
const getFilter = (aggregationType: AggregationType) => { |
||||
// For all metric types we want only numbers, except for cardinality
|
||||
// TODO: To have a more configuration-driven editor, it would be nice to move this logic in
|
||||
// metricAggregationConfig and bucketAggregationConfig so that each aggregation type can specify on
|
||||
// which kind of data it operates.
|
||||
if (isMetricAggregationType(aggregationType)) { |
||||
if (aggregationType !== 'cardinality') { |
||||
return 'number'; |
||||
} |
||||
|
||||
return void 0; |
||||
} |
||||
|
||||
switch (aggregationType) { |
||||
case 'date_histogram': |
||||
return 'date'; |
||||
case 'geohash_grid': |
||||
return 'geo_point'; |
||||
default: |
||||
return void 0; |
||||
} |
||||
}; |
||||
|
||||
const toSelectableValue = ({ text }: MetricFindValue): SelectableValue<string> => ({ |
||||
label: text, |
||||
value: text, |
||||
}); |
||||
|
||||
/** |
||||
* Returns a function to query the configured datasource for autocomplete values for the specified aggregation type. |
||||
* Each aggregation can be run on different types, for example avg only operates on numeric fields, geohash_grid only on geo_point fields. |
||||
* @param aggregationType the type of aggregation to get fields for |
||||
*/ |
||||
export const useFields = (aggregationType: AggregationType) => { |
||||
const datasource = useDatasource(); |
||||
const range = useRange(); |
||||
const filter = getFilter(aggregationType); |
||||
|
||||
return async () => { |
||||
const rawFields = await datasource.getFields(filter, range).toPromise(); |
||||
return rawFields.map(toSelectableValue); |
||||
}; |
||||
}; |
Loading…
Reference in new issue