mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
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:
parent
a4368790d5
commit
0a2a6690d9
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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' && (
|
||||
<>
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user