Elasticsearch: Allow case sensitive custom options in date_histogram interval (#36168)

* Elasticsearch: make interval select handle case-sensitive input

* Elasticsearch: Allow case sensitive custom options in date_histogram interval

* asd is not a good input id
This commit is contained in:
Giordano Ricci 2021-06-29 09:51:05 +01:00 committed by GitHub
parent a4368790d5
commit 0a2a6690d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 212 additions and 63 deletions

View File

@ -0,0 +1,92 @@
import { getDefaultTimeRange } from '@grafana/data';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ElasticDatasource } from 'app/plugins/datasource/elasticsearch/datasource';
import { ElasticsearchQuery } from 'app/plugins/datasource/elasticsearch/types';
import React, { ComponentProps, ReactNode } from 'react';
import { ElasticsearchProvider } from '../../ElasticsearchQueryContext';
import { DateHistogram } from '../aggregations';
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
const renderWithESProvider = (
ui: ReactNode,
{
providerProps: {
datasource = {} as ElasticDatasource,
query = { refId: 'A' },
onChange = () => void 0,
onRunQuery = () => void 0,
range = getDefaultTimeRange(),
} = {},
...renderOptions
}: { providerProps?: Partial<Omit<ComponentProps<typeof ElasticsearchProvider>, 'children'>> } & Parameters<
typeof render
>[1]
) => {
return render(
<ElasticsearchProvider
query={query}
onChange={onChange}
datasource={datasource}
onRunQuery={onRunQuery}
range={range}
>
{ui}
</ElasticsearchProvider>,
renderOptions
);
};
describe('DateHistogram Settings Editor', () => {
describe('Custom options for interval', () => {
it('Allows users to create and select case sensitive custom options', () => {
const bucketAgg: DateHistogram = {
id: '1',
type: 'date_histogram',
settings: {
interval: 'auto',
},
};
const query: ElasticsearchQuery = {
refId: 'A',
bucketAggs: [bucketAgg],
metrics: [{ id: '2', type: 'count' }],
query: '',
};
const onChange = jest.fn();
renderWithESProvider(<DateHistogramSettingsEditor bucketAgg={bucketAgg} />, {
providerProps: { query, onChange },
});
const intervalInput = screen.getByLabelText('Interval') as HTMLInputElement;
expect(intervalInput).toBeInTheDocument();
expect(screen.getByText('auto')).toBeInTheDocument();
// we open the menu
userEvent.click(intervalInput);
// default options don't have 1M but 1m
expect(screen.queryByText('1M')).not.toBeInTheDocument();
expect(screen.getByText('1m')).toBeInTheDocument();
// we type in the input 1M, which should prompt an option creation
userEvent.type(intervalInput, '1M');
const creatableOption = screen.getByLabelText('Select option');
expect(creatableOption).toHaveTextContent('Create: 1M');
// we click on the creatable option to trigger its creation
userEvent.click(creatableOption);
expect(onChange).toHaveBeenCalled();
// we open the menu again
userEvent.click(intervalInput);
// the created option should be available
expect(screen.getByText('1M')).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,115 @@
import React, { ComponentProps, useState } from 'react';
import { InlineField, Input, Select } from '@grafana/ui';
import { DateHistogram } from '../aggregations';
import { bucketAggregationConfig } from '../utils';
import { useDispatch } from '../../../../hooks/useStatelessReducer';
import { SelectableValue } from '@grafana/data';
import { changeBucketAggregationSetting } from '../state/actions';
import { inlineFieldProps } from '.';
import { uniqueId } from 'lodash';
type IntervalOption = SelectableValue<string>;
const defaultIntervalOptions: IntervalOption[] = [
{ label: 'auto', value: 'auto' },
{ label: '10s', value: '10s' },
{ label: '1m', value: '1m' },
{ label: '5m', value: '5m' },
{ label: '10m', value: '10m' },
{ label: '20m', value: '20m' },
{ label: '1h', value: '1h' },
{ label: '1d', value: '1d' },
];
const hasValue = (searchValue: IntervalOption['value']) => ({ value }: IntervalOption) => value === searchValue;
const isValidNewOption: ComponentProps<typeof Select>['isValidNewOption'] = (
inputValue,
_,
options: IntervalOption[]
) => {
// TODO: would be extremely nice here to allow only template variables and values that are
// valid date histogram's Interval options
const valueExists = options.some(hasValue(inputValue));
// we also don't want users to create "empty" values
return !valueExists && inputValue.trim().length > 0;
};
const optionStartsWithValue: ComponentProps<typeof Select>['filterOption'] = (option: IntervalOption, value) =>
option.value?.startsWith(value) || false;
interface Props {
bucketAgg: DateHistogram;
}
const getInitialState = (initialValue?: string): IntervalOption[] => {
return defaultIntervalOptions.concat(
defaultIntervalOptions.some(hasValue(initialValue))
? []
: {
value: initialValue,
label: initialValue,
}
);
};
export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
const dispatch = useDispatch();
const [intervalOptions, setIntervalOptions] = useState<IntervalOption[]>(
getInitialState(bucketAgg.settings?.interval)
);
const addIntervalOption = (value: string) => setIntervalOptions([...intervalOptions, { value, label: value }]);
const handleIntervalChange = (v: string) => dispatch(changeBucketAggregationSetting(bucketAgg, 'interval', v));
return (
<>
<InlineField label="Interval" {...inlineFieldProps}>
<Select<string>
inputId={uniqueId('es-date_histogram-interval')}
onChange={(e) => handleIntervalChange(e.value!)}
options={intervalOptions}
value={bucketAgg.settings?.interval || bucketAggregationConfig[bucketAgg.type].defaultSettings?.interval}
allowCustomValue
isValidNewOption={isValidNewOption}
filterOption={optionStartsWithValue}
onCreateOption={(value) => {
addIntervalOption(value);
handleIntervalChange(value);
}}
/>
</InlineField>
<InlineField label="Min Doc Count" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
defaultValue={
bucketAgg.settings?.min_doc_count || bucketAggregationConfig[bucketAgg.type].defaultSettings?.min_doc_count
}
/>
</InlineField>
<InlineField label="Trim Edges" {...inlineFieldProps} tooltip="Trim the edges on the timeseries datapoints">
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))}
defaultValue={
bucketAgg.settings?.trimEdges || bucketAggregationConfig[bucketAgg.type].defaultSettings?.trimEdges
}
/>
</InlineField>
<InlineField
label="Offset"
{...inlineFieldProps}
tooltip="Change the start value of each bucket by the specified positive (+) or negative offset (-) duration, such as 1h for an hour, or 1d for a day"
>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))}
defaultValue={bucketAgg.settings?.offset || bucketAggregationConfig[bucketAgg.type].defaultSettings?.offset}
/>
</InlineField>
</>
);
};

View File

@ -4,18 +4,13 @@ import { useDispatch } from '../../../../hooks/useStatelessReducer';
import { SettingsEditorContainer } from '../../SettingsEditorContainer';
import { changeBucketAggregationSetting } from '../state/actions';
import { BucketAggregation } from '../aggregations';
import {
bucketAggregationConfig,
createOrderByOptionsFromMetrics,
intervalOptions,
orderOptions,
sizeOptions,
} from '../utils';
import { bucketAggregationConfig, createOrderByOptionsFromMetrics, orderOptions, sizeOptions } from '../utils';
import { FiltersSettingsEditor } from './FiltersSettingsEditor';
import { useDescription } from './useDescription';
import { useQuery } from '../../ElasticsearchQueryContext';
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
const inlineFieldProps: Partial<ComponentProps<typeof InlineField>> = {
export const inlineFieldProps: Partial<ComponentProps<typeof InlineField>> = {
labelWidth: 16,
};
@ -25,6 +20,7 @@ interface Props {
export const SettingsEditor = ({ bucketAgg }: Props) => {
const dispatch = useDispatch();
const { metrics } = useQuery();
const settingsDescription = useDescription(bucketAgg);
const orderBy = createOrderByOptionsFromMetrics(metrics);
@ -90,50 +86,7 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
</InlineField>
)}
{bucketAgg.type === 'date_histogram' && (
<>
<InlineField label="Interval" {...inlineFieldProps}>
<Select
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'interval', e.value!))}
options={intervalOptions}
value={bucketAgg.settings?.interval || bucketAggregationConfig[bucketAgg.type].defaultSettings?.interval}
allowCustomValue
/>
</InlineField>
<InlineField label="Min Doc Count" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
defaultValue={
bucketAgg.settings?.min_doc_count ||
bucketAggregationConfig[bucketAgg.type].defaultSettings?.min_doc_count
}
/>
</InlineField>
<InlineField label="Trim Edges" {...inlineFieldProps} tooltip="Trim the edges on the timeseries datapoints">
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))}
defaultValue={
bucketAgg.settings?.trimEdges || bucketAggregationConfig[bucketAgg.type].defaultSettings?.trimEdges
}
/>
</InlineField>
<InlineField
label="Offset"
{...inlineFieldProps}
tooltip="Change the start value of each bucket by the specified positive (+) or negative offset (-) duration, such as 1h for an hour, or 1d for a day"
>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))}
defaultValue={
bucketAgg.settings?.offset || bucketAggregationConfig[bucketAgg.type].defaultSettings?.offset
}
/>
</InlineField>
</>
)}
{bucketAgg.type === 'date_histogram' && <DateHistogramSettingsEditor bucketAgg={bucketAgg} />}
{bucketAgg.type === 'histogram' && (
<>

View File

@ -76,17 +76,6 @@ export const orderByOptions = [
{ label: 'Doc Count', value: '_count' },
];
export const intervalOptions = [
{ label: 'auto', value: 'auto' },
{ label: '10s', value: '10s' },
{ label: '1m', value: '1m' },
{ label: '5m', value: '5m' },
{ label: '10m', value: '10m' },
{ label: '20m', value: '20m' },
{ label: '1h', value: '1h' },
{ label: '1d', value: '1d' },
];
/**
* This returns the valid options for each of the enabled extended stat
*/