Elasticsearch: Prevent pipeline aggregations to show up in terms order by options (#38448)
* Elasticsearch: Prevent pipeline aggregations to show up in terms order by options * fix optional props * Refactor & add tests * Fix & lower strict ts count * Update public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com> * Update public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com> Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
co-authored by
Piotr Jamróz
parent
fb17f053d3
commit
7f1327d1ed
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { ElasticsearchQuery } from '../../../../types';
|
||||||
|
import React from 'react';
|
||||||
|
import { renderWithESProvider } from '../../../../test-helpers/render';
|
||||||
|
import { Average, Derivative, TopMetrics } from '../../MetricAggregationsEditor/aggregations';
|
||||||
|
import { Terms } from '../aggregations';
|
||||||
|
import { TermsSettingsEditor } from './TermsSettingsEditor';
|
||||||
|
import selectEvent from 'react-select-event';
|
||||||
|
import { describeMetric } from 'app/plugins/datasource/elasticsearch/utils';
|
||||||
|
|
||||||
|
describe('Terms Settings Editor', () => {
|
||||||
|
it('Pipeline aggregations should not be in "order by" options', () => {
|
||||||
|
const termsAgg: Terms = {
|
||||||
|
id: '1',
|
||||||
|
type: 'terms',
|
||||||
|
};
|
||||||
|
const avg: Average = { id: '2', type: 'avg', field: '@value' };
|
||||||
|
const derivative: Derivative = { id: '3', field: avg.id, type: 'derivative' };
|
||||||
|
const topMetrics: TopMetrics = { id: '4', type: 'top_metrics' };
|
||||||
|
const query: ElasticsearchQuery = {
|
||||||
|
refId: 'A',
|
||||||
|
query: '',
|
||||||
|
bucketAggs: [termsAgg],
|
||||||
|
metrics: [avg, derivative, topMetrics],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithESProvider(<TermsSettingsEditor bucketAgg={termsAgg} />, { providerProps: { query } });
|
||||||
|
|
||||||
|
const selectEl = screen.getByLabelText('Order By');
|
||||||
|
expect(selectEl).toBeInTheDocument();
|
||||||
|
|
||||||
|
selectEvent.openMenu(selectEl);
|
||||||
|
|
||||||
|
// Derivative is a pipeline aggregation, it shouldn't be present in the order by options
|
||||||
|
expect(screen.queryByText(describeMetric(derivative))).not.toBeInTheDocument();
|
||||||
|
// TopMetrics cannot be used as order by option
|
||||||
|
expect(screen.queryByText(describeMetric(topMetrics))).not.toBeInTheDocument();
|
||||||
|
// All other metric aggregations can be used in order by
|
||||||
|
expect(screen.getByText(describeMetric(avg))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
+77
-2
@@ -3,10 +3,20 @@ import { InlineField, Select, Input } from '@grafana/ui';
|
|||||||
import { Terms } from '../aggregations';
|
import { Terms } from '../aggregations';
|
||||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||||
import { inlineFieldProps } from '.';
|
import { inlineFieldProps } from '.';
|
||||||
import { bucketAggregationConfig, createOrderByOptionsFromMetrics, orderOptions, sizeOptions } from '../utils';
|
import { bucketAggregationConfig, orderByOptions, orderOptions, sizeOptions } from '../utils';
|
||||||
import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour';
|
import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour';
|
||||||
import { changeBucketAggregationSetting } from '../state/actions';
|
import { changeBucketAggregationSetting } from '../state/actions';
|
||||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
import { useQuery } from '../../ElasticsearchQueryContext';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { describeMetric } from '../../../../utils';
|
||||||
|
import {
|
||||||
|
ExtendedStatMetaType,
|
||||||
|
ExtendedStats,
|
||||||
|
isPipelineAggregation,
|
||||||
|
MetricAggregation,
|
||||||
|
Percentiles,
|
||||||
|
} from '../../MetricAggregationsEditor/aggregations';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bucketAgg: Terms;
|
bucketAgg: Terms;
|
||||||
@@ -14,7 +24,7 @@ interface Props {
|
|||||||
|
|
||||||
export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
|
export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
|
||||||
const { metrics } = useQuery();
|
const { metrics } = useQuery();
|
||||||
const orderBy = createOrderByOptionsFromMetrics(metrics);
|
const orderBy = createOrderByOptions(metrics);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -60,6 +70,7 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
|
|
||||||
<InlineField label="Order By" {...inlineFieldProps}>
|
<InlineField label="Order By" {...inlineFieldProps}>
|
||||||
<Select
|
<Select
|
||||||
|
inputId={uniqueId('es-terms-')}
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'orderBy', newValue: e.value }))
|
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'orderBy', newValue: e.value }))
|
||||||
@@ -80,3 +91,67 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns the valid options for each of the enabled extended stat
|
||||||
|
*/
|
||||||
|
function createOrderByOptionsForExtendedStats(metric: ExtendedStats): SelectableValue<string> {
|
||||||
|
if (!metric.meta) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const metaKeys = Object.keys(metric.meta) as ExtendedStatMetaType[];
|
||||||
|
return metaKeys
|
||||||
|
.filter((key) => metric.meta?.[key])
|
||||||
|
.map((key) => {
|
||||||
|
let method = key as string;
|
||||||
|
// The bucket path for std_deviation_bounds.lower and std_deviation_bounds.upper
|
||||||
|
// is accessed via std_lower and std_upper, respectively.
|
||||||
|
if (key === 'std_deviation_bounds_lower') {
|
||||||
|
method = 'std_lower';
|
||||||
|
}
|
||||||
|
if (key === 'std_deviation_bounds_upper') {
|
||||||
|
method = 'std_upper';
|
||||||
|
}
|
||||||
|
return { label: `${describeMetric(metric)} (${method})`, value: `${metric.id}[${method}]` };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns the valid options for each of the percents listed in the percentile settings
|
||||||
|
*/
|
||||||
|
function createOrderByOptionsForPercentiles(metric: Percentiles): Array<SelectableValue<string>> {
|
||||||
|
if (!metric.settings?.percents) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return metric.settings.percents.map((percent) => {
|
||||||
|
// The bucket path for percentile numbers is appended with a `.0` if the number is whole
|
||||||
|
// otherwise you have to use the actual value.
|
||||||
|
const percentString = /^\d+\.\d+/.test(`${percent}`) ? percent : `${percent}.0`;
|
||||||
|
return { label: `${describeMetric(metric)} (${percent})`, value: `${metric.id}[${percentString}]` };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidOrderTarget(metric: MetricAggregation) {
|
||||||
|
return (
|
||||||
|
// top metrics can't be used for ordering
|
||||||
|
metric.type !== 'top_metrics' &&
|
||||||
|
// pipeline aggregations can't be used for ordering: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-order
|
||||||
|
!isPipelineAggregation(metric)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This creates all the valid order by options based on the metrics
|
||||||
|
*/
|
||||||
|
export const createOrderByOptions = (metrics: MetricAggregation[] = []): Array<SelectableValue<string>> => {
|
||||||
|
const metricOptions = metrics.filter(isValidOrderTarget).flatMap((metric) => {
|
||||||
|
if (metric.type === 'extended_stats') {
|
||||||
|
return createOrderByOptionsForExtendedStats(metric);
|
||||||
|
} else if (metric.type === 'percentiles') {
|
||||||
|
return createOrderByOptionsForPercentiles(metric);
|
||||||
|
} else {
|
||||||
|
return { label: describeMetric(metric), value: metric.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...orderByOptions, ...metricOptions];
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { useQuery } from '../../ElasticsearchQueryContext';
|
|||||||
import { BucketAggregation } from '../aggregations';
|
import { BucketAggregation } from '../aggregations';
|
||||||
import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils';
|
import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils';
|
||||||
|
|
||||||
const hasValue = (value: string) => (object: { value: string }) => object.value === value;
|
const hasValue = (value: string) => (object: { value?: string }) => object.value === value;
|
||||||
|
|
||||||
// FIXME: We should apply the same defaults we have in bucketAggregationsConfig here instead of "custom" values
|
// FIXME: We should apply the same defaults we have in bucketAggregationsConfig here instead of "custom" values
|
||||||
// as they might get out of sync.
|
// as they might get out of sync.
|
||||||
|
|||||||
+6
-74
@@ -1,12 +1,5 @@
|
|||||||
import { BucketsConfiguration } from '../../../types';
|
import { BucketsConfiguration } from '../../../types';
|
||||||
import { defaultFilter } from './SettingsEditor/FiltersSettingsEditor/utils';
|
import { defaultFilter } from './SettingsEditor/FiltersSettingsEditor/utils';
|
||||||
import { describeMetric } from '../../../utils';
|
|
||||||
import {
|
|
||||||
ExtendedStatMetaType,
|
|
||||||
ExtendedStats,
|
|
||||||
MetricAggregation,
|
|
||||||
Percentiles,
|
|
||||||
} from '../MetricAggregationsEditor/aggregations';
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
export const bucketAggregationConfig: BucketsConfiguration = {
|
export const bucketAggregationConfig: BucketsConfiguration = {
|
||||||
@@ -53,9 +46,12 @@ export const bucketAggregationConfig: BucketsConfiguration = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Define better types for the following
|
export const orderByOptions: Array<SelectableValue<string>> = [
|
||||||
type OrderByOption = SelectableValue<string>;
|
{ label: 'Term value', value: '_term' },
|
||||||
export const orderOptions: OrderByOption[] = [
|
{ label: 'Doc Count', value: '_count' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const orderOptions: Array<SelectableValue<string>> = [
|
||||||
{ label: 'Top', value: 'desc' },
|
{ label: 'Top', value: 'desc' },
|
||||||
{ label: 'Bottom', value: 'asc' },
|
{ label: 'Bottom', value: 'asc' },
|
||||||
];
|
];
|
||||||
@@ -70,67 +66,3 @@ export const sizeOptions = [
|
|||||||
{ label: '15', value: '15' },
|
{ label: '15', value: '15' },
|
||||||
{ label: '20', value: '20' },
|
{ label: '20', value: '20' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const orderByOptions = [
|
|
||||||
{ label: 'Term value', value: '_term' },
|
|
||||||
{ label: 'Doc Count', value: '_count' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This returns the valid options for each of the enabled extended stat
|
|
||||||
*/
|
|
||||||
function createOrderByOptionsForExtendedStats(metric: ExtendedStats): SelectableValue<string> {
|
|
||||||
if (!metric.meta) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const metaKeys = Object.keys(metric.meta) as ExtendedStatMetaType[];
|
|
||||||
return metaKeys
|
|
||||||
.filter((key) => metric.meta?.[key])
|
|
||||||
.map((key) => {
|
|
||||||
let method = key as string;
|
|
||||||
// The bucket path for std_deviation_bounds.lower and std_deviation_bounds.upper
|
|
||||||
// is accessed via std_lower and std_upper, respectively.
|
|
||||||
if (key === 'std_deviation_bounds_lower') {
|
|
||||||
method = 'std_lower';
|
|
||||||
}
|
|
||||||
if (key === 'std_deviation_bounds_upper') {
|
|
||||||
method = 'std_upper';
|
|
||||||
}
|
|
||||||
return { label: `${describeMetric(metric)} (${method})`, value: `${metric.id}[${method}]` };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This returns the valid options for each of the percents listed in the percentile settings
|
|
||||||
*/
|
|
||||||
function createOrderByOptionsForPercentiles(metric: Percentiles): OrderByOption[] {
|
|
||||||
if (!metric.settings?.percents) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return metric.settings.percents.map((percent) => {
|
|
||||||
// The bucket path for percentile numbers is appended with a `.0` if the number is whole
|
|
||||||
// otherwise you have to use the actual value.
|
|
||||||
const percentString = /^\d+\.\d+/.test(`${percent}`) ? percent : `${percent}.0`;
|
|
||||||
return { label: `${describeMetric(metric)} (${percent})`, value: `${metric.id}[${percentString}]` };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const INCOMPATIBLE_ORDER_BY_AGGS = ['top_metrics'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This creates all the valid order by options based on the metrics
|
|
||||||
*/
|
|
||||||
export const createOrderByOptionsFromMetrics = (metrics: MetricAggregation[] = []): OrderByOption[] => {
|
|
||||||
const metricOptions = metrics
|
|
||||||
.filter((metric) => !INCOMPATIBLE_ORDER_BY_AGGS.includes(metric.type))
|
|
||||||
.flatMap((metric) => {
|
|
||||||
if (metric.type === 'extended_stats') {
|
|
||||||
return createOrderByOptionsForExtendedStats(metric);
|
|
||||||
} else if (metric.type === 'percentiles') {
|
|
||||||
return createOrderByOptionsForPercentiles(metric);
|
|
||||||
} else {
|
|
||||||
return { label: describeMetric(metric), value: metric.id };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [...orderByOptions, ...metricOptions];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,20 +4,28 @@ import { getDefaultTimeRange } from '@grafana/data';
|
|||||||
import { ElasticDatasource } from '../datasource';
|
import { ElasticDatasource } from '../datasource';
|
||||||
import { ElasticsearchProvider } from '../components/QueryEditor/ElasticsearchQueryContext';
|
import { ElasticsearchProvider } from '../components/QueryEditor/ElasticsearchQueryContext';
|
||||||
|
|
||||||
|
const defaultProviderProps = {
|
||||||
|
datasource: {} as ElasticDatasource,
|
||||||
|
query: { refId: 'A' },
|
||||||
|
onChange: () => void 0,
|
||||||
|
onRunQuery: () => void 0,
|
||||||
|
range: getDefaultTimeRange(),
|
||||||
|
};
|
||||||
|
|
||||||
export const renderWithESProvider = (
|
export const renderWithESProvider = (
|
||||||
ui: ReactNode,
|
ui: ReactNode,
|
||||||
{
|
{
|
||||||
providerProps: {
|
providerProps: {
|
||||||
datasource = {} as ElasticDatasource,
|
datasource = defaultProviderProps.datasource,
|
||||||
query = { refId: 'A' },
|
query = defaultProviderProps.query,
|
||||||
onChange = () => void 0,
|
onChange = defaultProviderProps.onChange,
|
||||||
onRunQuery = () => void 0,
|
onRunQuery = defaultProviderProps.onRunQuery,
|
||||||
range = getDefaultTimeRange(),
|
range = defaultProviderProps.range,
|
||||||
} = {},
|
} = defaultProviderProps,
|
||||||
...renderOptions
|
...renderOptions
|
||||||
}: { providerProps?: Partial<Omit<ComponentProps<typeof ElasticsearchProvider>, 'children'>> } & Parameters<
|
}: { providerProps?: Partial<Omit<ComponentProps<typeof ElasticsearchProvider>, 'children'>> } & Parameters<
|
||||||
typeof render
|
typeof render
|
||||||
>[1]
|
>[1] = { providerProps: defaultProviderProps }
|
||||||
) => {
|
) => {
|
||||||
return render(
|
return render(
|
||||||
<ElasticsearchProvider
|
<ElasticsearchProvider
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ set -e
|
|||||||
|
|
||||||
echo -e "Collecting code stats (typescript errors & more)"
|
echo -e "Collecting code stats (typescript errors & more)"
|
||||||
|
|
||||||
ERROR_COUNT_LIMIT=50
|
ERROR_COUNT_LIMIT=49
|
||||||
ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --strict true | grep -oP 'Found \K(\d+)')"
|
ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --strict true | grep -oP 'Found \K(\d+)')"
|
||||||
|
|
||||||
if [ "$ERROR_COUNT" -gt $ERROR_COUNT_LIMIT ]; then
|
if [ "$ERROR_COUNT" -gt $ERROR_COUNT_LIMIT ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user