mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Elasticsearch: Persist custom value for size option in Terms Bucket Agg (#36194)
* Add custom hook to handle creatable select options * Refactor Terms Settings Editor * Make props of Bucket Aggregation settings editors consistent * Rename hook to something more descriptive * Move render test helper * Add tests * small refactor * Remove useless test
This commit is contained in:
@@ -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<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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { ComponentProps, useState } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { InlineField, Input, Select } from '@grafana/ui';
|
import { InlineField, Input, Select } from '@grafana/ui';
|
||||||
import { DateHistogram } from '../aggregations';
|
import { DateHistogram } from '../aggregations';
|
||||||
import { bucketAggregationConfig } from '../utils';
|
import { bucketAggregationConfig } from '../utils';
|
||||||
@@ -7,8 +7,9 @@ import { SelectableValue } from '@grafana/data';
|
|||||||
import { changeBucketAggregationSetting } from '../state/actions';
|
import { changeBucketAggregationSetting } from '../state/actions';
|
||||||
import { inlineFieldProps } from '.';
|
import { inlineFieldProps } from '.';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
|
import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour';
|
||||||
|
|
||||||
type IntervalOption = SelectableValue<string>;
|
type IntervalOption = Required<Pick<SelectableValue<string>, 'label' | 'value'>>;
|
||||||
|
|
||||||
const defaultIntervalOptions: IntervalOption[] = [
|
const defaultIntervalOptions: IntervalOption[] = [
|
||||||
{ label: 'auto', value: 'auto' },
|
{ label: 'auto', value: 'auto' },
|
||||||
@@ -42,43 +43,23 @@ interface Props {
|
|||||||
bucketAgg: DateHistogram;
|
bucketAgg: DateHistogram;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitialState = (initialValue?: string): IntervalOption[] => {
|
|
||||||
return defaultIntervalOptions.concat(
|
|
||||||
defaultIntervalOptions.some(hasValue(initialValue))
|
|
||||||
? []
|
|
||||||
: {
|
|
||||||
value: initialValue,
|
|
||||||
label: initialValue,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
|
export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
|
||||||
const dispatch = useDispatch();
|
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));
|
const handleIntervalChange = (v: string) => dispatch(changeBucketAggregationSetting(bucketAgg, 'interval', v));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InlineField label="Interval" {...inlineFieldProps}>
|
<InlineField label="Interval" {...inlineFieldProps}>
|
||||||
<Select<string>
|
<Select
|
||||||
inputId={uniqueId('es-date_histogram-interval')}
|
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}
|
isValidNewOption={isValidNewOption}
|
||||||
filterOption={optionStartsWithValue}
|
filterOption={optionStartsWithValue}
|
||||||
onCreateOption={(value) => {
|
{...useCreatableSelectPersistedBehaviour({
|
||||||
addIntervalOption(value);
|
options: defaultIntervalOptions,
|
||||||
handleIntervalChange(value);
|
value: bucketAgg.settings?.interval || bucketAggregationConfig.date_histogram.defaultSettings?.interval,
|
||||||
}}
|
onChange: handleIntervalChange,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
|
|
||||||
@@ -86,7 +67,7 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
|
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
|
||||||
defaultValue={
|
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
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
@@ -95,7 +76,7 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))}
|
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
bucketAgg.settings?.trimEdges || bucketAggregationConfig[bucketAgg.type].defaultSettings?.trimEdges
|
bucketAgg.settings?.trimEdges || bucketAggregationConfig.date_histogram.defaultSettings?.trimEdges
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
@@ -107,7 +88,7 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))}
|
onBlur={(e) => 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}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</>
|
</>
|
||||||
|
@@ -10,25 +10,25 @@ import { addFilter, changeFilter, removeFilter } from './state/actions';
|
|||||||
import { reducer as filtersReducer } from './state/reducer';
|
import { reducer as filtersReducer } from './state/reducer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: Filters;
|
bucketAgg: Filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FiltersSettingsEditor = ({ value }: Props) => {
|
export const FiltersSettingsEditor = ({ bucketAgg }: Props) => {
|
||||||
const upperStateDispatch = useDispatch<BucketAggregationAction<Filters>>();
|
const upperStateDispatch = useDispatch<BucketAggregationAction<Filters>>();
|
||||||
|
|
||||||
const dispatch = useStatelessReducer(
|
const dispatch = useStatelessReducer(
|
||||||
(newState) => upperStateDispatch(changeBucketAggregationSetting(value, 'filters', newState)),
|
(newState) => upperStateDispatch(changeBucketAggregationSetting(bucketAgg, 'filters', newState)),
|
||||||
value.settings?.filters,
|
bucketAgg.settings?.filters,
|
||||||
filtersReducer
|
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.
|
// 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.
|
// If this is the case we add a default one.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!value.settings?.filters?.length) {
|
if (!bucketAgg.settings?.filters?.length) {
|
||||||
dispatch(addFilter());
|
dispatch(addFilter());
|
||||||
}
|
}
|
||||||
}, [dispatch, value.settings?.filters?.length]);
|
}, [dispatch, bucketAgg.settings?.filters?.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -38,7 +38,7 @@ export const FiltersSettingsEditor = ({ value }: Props) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{value.settings?.filters!.map((filter, index) => (
|
{bucketAgg.settings?.filters!.map((filter, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={css`
|
className={css`
|
||||||
@@ -69,7 +69,7 @@ export const FiltersSettingsEditor = ({ value }: Props) => {
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
<AddRemove
|
<AddRemove
|
||||||
index={index}
|
index={index}
|
||||||
elements={value.settings?.filters || []}
|
elements={bucketAgg.settings?.filters || []}
|
||||||
onAdd={() => dispatch(addFilter())}
|
onAdd={() => dispatch(addFilter())}
|
||||||
onRemove={() => dispatch(removeFilter(index))}
|
onRemove={() => dispatch(removeFilter(index))}
|
||||||
/>
|
/>
|
||||||
|
@@ -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 (
|
||||||
|
<>
|
||||||
|
<InlineField label="Order" {...inlineFieldProps}>
|
||||||
|
<Select
|
||||||
|
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'order', e.value!))}
|
||||||
|
options={orderOptions}
|
||||||
|
value={bucketAgg.settings?.order || bucketAggregationConfig.terms.defaultSettings?.order}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
|
||||||
|
<InlineField label="Size" {...inlineFieldProps}>
|
||||||
|
<Select
|
||||||
|
// TODO: isValidNewOption should only allow numbers & template variables
|
||||||
|
{...useCreatableSelectPersistedBehaviour({
|
||||||
|
options: sizeOptions,
|
||||||
|
value: bucketAgg.settings?.size || bucketAggregationConfig.terms.defaultSettings?.size,
|
||||||
|
onChange(value) {
|
||||||
|
dispatch(changeBucketAggregationSetting(bucketAgg, 'size', 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.terms.defaultSettings?.min_doc_count
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
|
||||||
|
<InlineField label="Order By" {...inlineFieldProps}>
|
||||||
|
<Select
|
||||||
|
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'orderBy', e.value!))}
|
||||||
|
options={orderBy}
|
||||||
|
value={bucketAgg.settings?.orderBy || bucketAggregationConfig.terms.defaultSettings?.orderBy}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
|
||||||
|
<InlineField label="Missing" {...inlineFieldProps}>
|
||||||
|
<Input
|
||||||
|
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'missing', e.target.value!))}
|
||||||
|
defaultValue={bucketAgg.settings?.missing || bucketAggregationConfig.terms.defaultSettings?.missing}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,14 +1,14 @@
|
|||||||
import { InlineField, Input, Select } from '@grafana/ui';
|
import { InlineField, Input } from '@grafana/ui';
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
import { useDispatch } from '../../../../hooks/useStatelessReducer';
|
||||||
import { SettingsEditorContainer } from '../../SettingsEditorContainer';
|
import { SettingsEditorContainer } from '../../SettingsEditorContainer';
|
||||||
import { changeBucketAggregationSetting } from '../state/actions';
|
import { changeBucketAggregationSetting } from '../state/actions';
|
||||||
import { BucketAggregation } from '../aggregations';
|
import { BucketAggregation } from '../aggregations';
|
||||||
import { bucketAggregationConfig, createOrderByOptionsFromMetrics, orderOptions, sizeOptions } from '../utils';
|
import { bucketAggregationConfig } from '../utils';
|
||||||
import { FiltersSettingsEditor } from './FiltersSettingsEditor';
|
import { FiltersSettingsEditor } from './FiltersSettingsEditor';
|
||||||
import { useDescription } from './useDescription';
|
import { useDescription } from './useDescription';
|
||||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
|
||||||
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
|
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
|
||||||
|
import { TermsSettingsEditor } from './TermsSettingsEditor';
|
||||||
|
|
||||||
export const inlineFieldProps: Partial<ComponentProps<typeof InlineField>> = {
|
export const inlineFieldProps: Partial<ComponentProps<typeof InlineField>> = {
|
||||||
labelWidth: 16,
|
labelWidth: 16,
|
||||||
@@ -21,59 +21,13 @@ interface Props {
|
|||||||
export const SettingsEditor = ({ bucketAgg }: Props) => {
|
export const SettingsEditor = ({ bucketAgg }: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { metrics } = useQuery();
|
|
||||||
const settingsDescription = useDescription(bucketAgg);
|
const settingsDescription = useDescription(bucketAgg);
|
||||||
const orderBy = createOrderByOptionsFromMetrics(metrics);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsEditorContainer label={settingsDescription}>
|
<SettingsEditorContainer label={settingsDescription}>
|
||||||
{bucketAgg.type === 'terms' && (
|
{bucketAgg.type === 'terms' && <TermsSettingsEditor bucketAgg={bucketAgg} />}
|
||||||
<>
|
{bucketAgg.type === 'date_histogram' && <DateHistogramSettingsEditor bucketAgg={bucketAgg} />}
|
||||||
<InlineField label="Order" {...inlineFieldProps}>
|
{bucketAgg.type === 'filters' && <FiltersSettingsEditor bucketAgg={bucketAgg} />}
|
||||||
<Select
|
|
||||||
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'order', e.value!))}
|
|
||||||
options={orderOptions}
|
|
||||||
value={bucketAgg.settings?.order || bucketAggregationConfig[bucketAgg.type].defaultSettings?.order}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
|
|
||||||
<InlineField label="Size" {...inlineFieldProps}>
|
|
||||||
<Select
|
|
||||||
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'size', e.value!))}
|
|
||||||
options={sizeOptions}
|
|
||||||
value={bucketAgg.settings?.size || bucketAggregationConfig[bucketAgg.type].defaultSettings?.size}
|
|
||||||
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="Order By" {...inlineFieldProps}>
|
|
||||||
<Select
|
|
||||||
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'orderBy', e.value!))}
|
|
||||||
options={orderBy}
|
|
||||||
value={bucketAgg.settings?.orderBy || bucketAggregationConfig[bucketAgg.type].defaultSettings?.orderBy}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
|
|
||||||
<InlineField label="Missing" {...inlineFieldProps}>
|
|
||||||
<Input
|
|
||||||
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'missing', e.target.value!))}
|
|
||||||
defaultValue={
|
|
||||||
bucketAgg.settings?.missing || bucketAggregationConfig[bucketAgg.type].defaultSettings?.missing
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{bucketAgg.type === 'geohash_grid' && (
|
{bucketAgg.type === 'geohash_grid' && (
|
||||||
<InlineField label="Precision" {...inlineFieldProps}>
|
<InlineField label="Precision" {...inlineFieldProps}>
|
||||||
@@ -86,8 +40,6 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bucketAgg.type === 'date_histogram' && <DateHistogramSettingsEditor bucketAgg={bucketAgg} />}
|
|
||||||
|
|
||||||
{bucketAgg.type === 'histogram' && (
|
{bucketAgg.type === 'histogram' && (
|
||||||
<>
|
<>
|
||||||
<InlineField label="Interval" {...inlineFieldProps}>
|
<InlineField label="Interval" {...inlineFieldProps}>
|
||||||
@@ -110,8 +62,6 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bucketAgg.type === 'filters' && <FiltersSettingsEditor value={bucketAgg} />}
|
|
||||||
</SettingsEditorContainer>
|
</SettingsEditorContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 }) => (
|
||||||
|
<InlineField label="label">
|
||||||
|
<Select
|
||||||
|
inputId="select"
|
||||||
|
{...useCreatableSelectPersistedBehaviour({
|
||||||
|
options: [{ label: 'Option 1', value: 'Option 1' }],
|
||||||
|
onChange() {},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { rerender } = render(<MyComp />);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('label') as HTMLInputElement;
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
|
||||||
|
// we open the menu
|
||||||
|
userEvent.click(input);
|
||||||
|
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// we type in the input 'Option 2', which should prompt an option creation
|
||||||
|
userEvent.type(input, 'Option 2');
|
||||||
|
const creatableOption = screen.getByLabelText('Select option');
|
||||||
|
expect(creatableOption).toHaveTextContent('Create: Option 2');
|
||||||
|
|
||||||
|
// we click on the creatable option to trigger its creation
|
||||||
|
userEvent.click(creatableOption);
|
||||||
|
|
||||||
|
// Forcing a rerender
|
||||||
|
rerender(<MyComp force={true} />);
|
||||||
|
|
||||||
|
// we open the menu again
|
||||||
|
userEvent.click(input);
|
||||||
|
// the created option should be available
|
||||||
|
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should handle onChange properly', () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const MyComp = () => (
|
||||||
|
<InlineField label="label">
|
||||||
|
<Select
|
||||||
|
inputId="select"
|
||||||
|
{...useCreatableSelectPersistedBehaviour({
|
||||||
|
options: [{ label: 'Option 1', value: 'Option 1' }],
|
||||||
|
onChange,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<MyComp />);
|
||||||
|
|
||||||
|
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 }) => (
|
||||||
|
<InlineField label="label">
|
||||||
|
<Select
|
||||||
|
inputId="select"
|
||||||
|
{...useCreatableSelectPersistedBehaviour({
|
||||||
|
options: [{ label: 'Option 1', value: 'Option 1' }],
|
||||||
|
value: 'Option 2',
|
||||||
|
onChange() {},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<MyComp />);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('label') as HTMLInputElement;
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
|
||||||
|
// we open the menu
|
||||||
|
userEvent.click(input);
|
||||||
|
|
||||||
|
// we expect 2 elemnts having "Option 2": the input itself and the option.
|
||||||
|
expect(screen.getAllByText('Option 2')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { Select } from '@grafana/ui';
|
||||||
|
import { ComponentProps, useState } from 'react';
|
||||||
|
|
||||||
|
const hasValue = <T extends SelectableValue>(searchValue: T['value']) => ({ value }: T) => value === searchValue;
|
||||||
|
|
||||||
|
const getInitialState = <T extends SelectableValue>(initialOptions: T[], initialValue?: T['value']): T[] => {
|
||||||
|
if (initialValue === undefined || initialOptions.some(hasValue(initialValue))) {
|
||||||
|
return initialOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialOptions.concat({
|
||||||
|
value: initialValue,
|
||||||
|
label: initialValue,
|
||||||
|
} as T);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Params<T extends SelectableValue> {
|
||||||
|
options: T[];
|
||||||
|
value?: T['value'];
|
||||||
|
onChange: (value: T['value']) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the Props needed by Select to handle custom values and handles custom value creation
|
||||||
|
* and the initial value when it is not present in the option array.
|
||||||
|
*/
|
||||||
|
export const useCreatableSelectPersistedBehaviour = <T extends SelectableValue>({
|
||||||
|
options: initialOptions,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: Params<T>): Pick<
|
||||||
|
ComponentProps<typeof Select>,
|
||||||
|
'onChange' | 'onCreateOption' | 'options' | 'allowCustomValue' | 'value'
|
||||||
|
> => {
|
||||||
|
const [options, setOptions] = useState<T[]>(getInitialState(initialOptions, value));
|
||||||
|
|
||||||
|
const addOption = (newValue: T['value']) => setOptions([...options, { value: newValue, label: newValue } as T]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreateOption: (value) => {
|
||||||
|
addOption(value);
|
||||||
|
onChange(value);
|
||||||
|
},
|
||||||
|
onChange: (e) => {
|
||||||
|
onChange(e.value);
|
||||||
|
},
|
||||||
|
allowCustomValue: true,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { getDefaultTimeRange } from '@grafana/data';
|
||||||
|
import { ElasticDatasource } from '../datasource';
|
||||||
|
import { ElasticsearchProvider } from '../components/QueryEditor/ElasticsearchQueryContext';
|
||||||
|
|
||||||
|
export 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
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user