From 16c2a2b6cbc25791629d1ecabb1765bd3c99e317 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 29 Jun 2021 12:50:18 +0200 Subject: [PATCH] Elasticsearch: Allow case sensitive custom options in date_histogram interval (#36168) (#36255) * 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 (cherry picked from commit 0a2a6690d926bce6b2b908c1a1b2ce34ada5bd91) Co-authored-by: Giordano Ricci --- .../DateHistogramSettingsEditor.test.tsx | 92 ++++++++++++++ .../DateHistogramSettingsEditor.tsx | 115 ++++++++++++++++++ .../SettingsEditor/index.tsx | 57 +-------- .../BucketAggregationsEditor/utils.ts | 11 -- 4 files changed, 212 insertions(+), 63 deletions(-) create mode 100644 public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx create mode 100644 public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx new file mode 100644 index 00000000000..3d1a5198739 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx @@ -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, 'children'>> } & Parameters< + typeof render + >[1] +) => { + return render( + + {ui} + , + 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(, { + 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(); + }); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx new file mode 100644 index 00000000000..4bbb446f395 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx @@ -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; + +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['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['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( + getInitialState(bucketAgg.settings?.interval) + ); + + const addIntervalOption = (value: string) => setIntervalOptions([...intervalOptions, { value, label: value }]); + + const handleIntervalChange = (v: string) => dispatch(changeBucketAggregationSetting(bucketAgg, 'interval', v)); + + return ( + <> + + + 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); + }} + /> + + + + dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))} + defaultValue={ + bucketAgg.settings?.min_doc_count || bucketAggregationConfig[bucketAgg.type].defaultSettings?.min_doc_count + } + /> + + + + dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))} + defaultValue={ + bucketAgg.settings?.trimEdges || bucketAggregationConfig[bucketAgg.type].defaultSettings?.trimEdges + } + /> + + + + dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))} + defaultValue={bucketAgg.settings?.offset || bucketAggregationConfig[bucketAgg.type].defaultSettings?.offset} + /> + + + ); +}; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx index 1b03018398f..a8ba600e78f 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx @@ -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> = { +export const inlineFieldProps: Partial> = { 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) => { )} - {bucketAgg.type === 'date_histogram' && ( - <> - - dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))} - defaultValue={ - bucketAgg.settings?.min_doc_count || - bucketAggregationConfig[bucketAgg.type].defaultSettings?.min_doc_count - } - /> - - - - dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))} - defaultValue={ - bucketAgg.settings?.trimEdges || bucketAggregationConfig[bucketAgg.type].defaultSettings?.trimEdges - } - /> - - - - dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))} - defaultValue={ - bucketAgg.settings?.offset || bucketAggregationConfig[bucketAgg.type].defaultSettings?.offset - } - /> - - - )} + {bucketAgg.type === 'date_histogram' && } {bucketAgg.type === 'histogram' && ( <> diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts index 45541c7e63c..a08eaeca6a2 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/utils.ts @@ -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 */