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 deleted file mode 100644 index 3d1a5198739..00000000000 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -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 index 4bbb446f395..b2c4b7260da 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx @@ -1,4 +1,4 @@ -import React, { ComponentProps, useState } from 'react'; +import React, { ComponentProps } from 'react'; import { InlineField, Input, Select } from '@grafana/ui'; import { DateHistogram } from '../aggregations'; import { bucketAggregationConfig } from '../utils'; @@ -7,8 +7,9 @@ import { SelectableValue } from '@grafana/data'; import { changeBucketAggregationSetting } from '../state/actions'; import { inlineFieldProps } from '.'; import { uniqueId } from 'lodash'; +import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour'; -type IntervalOption = SelectableValue; +type IntervalOption = Required, 'label' | 'value'>>; const defaultIntervalOptions: IntervalOption[] = [ { label: 'auto', value: 'auto' }, @@ -42,43 +43,23 @@ 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 ( <> - + dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))} defaultValue={ - bucketAgg.settings?.min_doc_count || bucketAggregationConfig[bucketAgg.type].defaultSettings?.min_doc_count + bucketAgg.settings?.min_doc_count || bucketAggregationConfig.date_histogram.defaultSettings?.min_doc_count } /> @@ -95,7 +76,7 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => { dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))} defaultValue={ - bucketAgg.settings?.trimEdges || bucketAggregationConfig[bucketAgg.type].defaultSettings?.trimEdges + bucketAgg.settings?.trimEdges || bucketAggregationConfig.date_histogram.defaultSettings?.trimEdges } /> @@ -107,7 +88,7 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => { > dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))} - defaultValue={bucketAgg.settings?.offset || bucketAggregationConfig[bucketAgg.type].defaultSettings?.offset} + defaultValue={bucketAgg.settings?.offset || bucketAggregationConfig.date_histogram.defaultSettings?.offset} /> diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx index 8e61909d3eb..f8a84814d67 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx @@ -10,25 +10,25 @@ import { addFilter, changeFilter, removeFilter } from './state/actions'; import { reducer as filtersReducer } from './state/reducer'; interface Props { - value: Filters; + bucketAgg: Filters; } -export const FiltersSettingsEditor = ({ value }: Props) => { +export const FiltersSettingsEditor = ({ bucketAgg }: Props) => { const upperStateDispatch = useDispatch>(); const dispatch = useStatelessReducer( - (newState) => upperStateDispatch(changeBucketAggregationSetting(value, 'filters', newState)), - value.settings?.filters, + (newState) => upperStateDispatch(changeBucketAggregationSetting(bucketAgg, 'filters', newState)), + bucketAgg.settings?.filters, filtersReducer ); // The model might not have filters (or an empty array of filters) in it because of the way it was built in previous versions of the datasource. // If this is the case we add a default one. useEffect(() => { - if (!value.settings?.filters?.length) { + if (!bucketAgg.settings?.filters?.length) { dispatch(addFilter()); } - }, [dispatch, value.settings?.filters?.length]); + }, [dispatch, bucketAgg.settings?.filters?.length]); return ( <> @@ -38,7 +38,7 @@ export const FiltersSettingsEditor = ({ value }: Props) => { flex-direction: column; `} > - {value.settings?.filters!.map((filter, index) => ( + {bucketAgg.settings?.filters!.map((filter, index) => (
{ dispatch(addFilter())} onRemove={() => dispatch(removeFilter(index))} /> diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx new file mode 100644 index 00000000000..07bc7556e7b --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { InlineField, Select, Input } from '@grafana/ui'; +import { Terms } from '../aggregations'; +import { useDispatch } from '../../../../hooks/useStatelessReducer'; +import { inlineFieldProps } from '.'; +import { bucketAggregationConfig, createOrderByOptionsFromMetrics, orderOptions, sizeOptions } from '../utils'; +import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour'; +import { changeBucketAggregationSetting } from '../state/actions'; +import { useQuery } from '../../ElasticsearchQueryContext'; + +interface Props { + bucketAgg: Terms; +} + +export const TermsSettingsEditor = ({ bucketAgg }: Props) => { + const { metrics } = useQuery(); + const orderBy = createOrderByOptionsFromMetrics(metrics); + + const dispatch = useDispatch(); + + return ( + <> + + + + + + dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))} + defaultValue={ + bucketAgg.settings?.min_doc_count || bucketAggregationConfig.terms.defaultSettings?.min_doc_count + } + /> + + + + dispatch(changeBucketAggregationSetting(bucketAgg, 'missing', e.target.value!))} + defaultValue={bucketAgg.settings?.missing || bucketAggregationConfig.terms.defaultSettings?.missing} + /> + + + ); +}; 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 a8ba600e78f..6d9c11101cd 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 @@ -1,14 +1,14 @@ -import { InlineField, Input, Select } from '@grafana/ui'; +import { InlineField, Input } from '@grafana/ui'; import React, { ComponentProps } from 'react'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { SettingsEditorContainer } from '../../SettingsEditorContainer'; import { changeBucketAggregationSetting } from '../state/actions'; import { BucketAggregation } from '../aggregations'; -import { bucketAggregationConfig, createOrderByOptionsFromMetrics, orderOptions, sizeOptions } from '../utils'; +import { bucketAggregationConfig } from '../utils'; import { FiltersSettingsEditor } from './FiltersSettingsEditor'; import { useDescription } from './useDescription'; -import { useQuery } from '../../ElasticsearchQueryContext'; import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor'; +import { TermsSettingsEditor } from './TermsSettingsEditor'; export const inlineFieldProps: Partial> = { labelWidth: 16, @@ -21,59 +21,13 @@ interface Props { export const SettingsEditor = ({ bucketAgg }: Props) => { const dispatch = useDispatch(); - const { metrics } = useQuery(); const settingsDescription = useDescription(bucketAgg); - const orderBy = createOrderByOptionsFromMetrics(metrics); return ( - {bucketAgg.type === 'terms' && ( - <> - - dispatch(changeBucketAggregationSetting(bucketAgg, 'size', e.value!))} - options={sizeOptions} - value={bucketAgg.settings?.size || bucketAggregationConfig[bucketAgg.type].defaultSettings?.size} - allowCustomValue - /> - - - - 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, 'missing', e.target.value!))} - defaultValue={ - bucketAgg.settings?.missing || bucketAggregationConfig[bucketAgg.type].defaultSettings?.missing - } - /> - - - )} + {bucketAgg.type === 'terms' && } + {bucketAgg.type === 'date_histogram' && } + {bucketAgg.type === 'filters' && } {bucketAgg.type === 'geohash_grid' && ( @@ -86,8 +40,6 @@ export const SettingsEditor = ({ bucketAgg }: Props) => { )} - {bucketAgg.type === 'date_histogram' && } - {bucketAgg.type === 'histogram' && ( <> @@ -110,8 +62,6 @@ export const SettingsEditor = ({ bucketAgg }: Props) => { )} - - {bucketAgg.type === 'filters' && } ); }; diff --git a/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx b/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx new file mode 100644 index 00000000000..d50bc823a4f --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Select, InlineField } from '@grafana/ui'; +import { useCreatableSelectPersistedBehaviour } from './useCreatableSelectPersistedBehaviour'; +import userEvent from '@testing-library/user-event'; + +describe('useCreatableSelectPersistedBehaviour', () => { + it('Should make a Select accept custom values', () => { + const MyComp = (_: { force?: boolean }) => ( + + + + ); + + render(); + + const input = screen.getByLabelText('label') as HTMLInputElement; + expect(input).toBeInTheDocument(); + + // we open the menu + userEvent.click(input); + + const option1 = screen.getByText('Option 1'); + expect(option1).toBeInTheDocument(); + + // Should call onChange when selecting an already existing option + userEvent.click(option1); + expect(onChange).toHaveBeenCalledWith('Option 1'); + + userEvent.click(input); + + // we type in the input 'Option 2', which should prompt an option creation + userEvent.type(input, 'Option 2'); + userEvent.click(screen.getByLabelText('Select option')); + + expect(onChange).toHaveBeenCalledWith('Option 2'); + }); + + it('Should create an option for value if value is not in options', () => { + const MyComp = (_: { force?: boolean }) => ( + +