mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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 { SettingsEditorContainer } from '../../SettingsEditorContainer';
|
||||||
import { changeBucketAggregationSetting } from '../state/actions';
|
import { changeBucketAggregationSetting } from '../state/actions';
|
||||||
import { BucketAggregation } from '../aggregations';
|
import { BucketAggregation } from '../aggregations';
|
||||||
import {
|
import { bucketAggregationConfig, createOrderByOptionsFromMetrics, orderOptions, sizeOptions } from '../utils';
|
||||||
bucketAggregationConfig,
|
|
||||||
createOrderByOptionsFromMetrics,
|
|
||||||
intervalOptions,
|
|
||||||
orderOptions,
|
|
||||||
sizeOptions,
|
|
||||||
} from '../utils';
|
|
||||||
import { FiltersSettingsEditor } from './FiltersSettingsEditor';
|
import { FiltersSettingsEditor } from './FiltersSettingsEditor';
|
||||||
import { useDescription } from './useDescription';
|
import { useDescription } from './useDescription';
|
||||||
import { useQuery } from '../../ElasticsearchQueryContext';
|
import { useQuery } from '../../ElasticsearchQueryContext';
|
||||||
|
import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor';
|
||||||
|
|
||||||
const inlineFieldProps: Partial<ComponentProps<typeof InlineField>> = {
|
export const inlineFieldProps: Partial<ComponentProps<typeof InlineField>> = {
|
||||||
labelWidth: 16,
|
labelWidth: 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,6 +20,7 @@ interface Props {
|
|||||||
|
|
||||||
export const SettingsEditor = ({ bucketAgg }: Props) => {
|
export const SettingsEditor = ({ bucketAgg }: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { metrics } = useQuery();
|
const { metrics } = useQuery();
|
||||||
const settingsDescription = useDescription(bucketAgg);
|
const settingsDescription = useDescription(bucketAgg);
|
||||||
const orderBy = createOrderByOptionsFromMetrics(metrics);
|
const orderBy = createOrderByOptionsFromMetrics(metrics);
|
||||||
@ -90,50 +86,7 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bucketAgg.type === 'date_histogram' && (
|
{bucketAgg.type === 'date_histogram' && <DateHistogramSettingsEditor bucketAgg={bucketAgg} />}
|
||||||
<>
|
|
||||||
<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 === 'histogram' && (
|
{bucketAgg.type === 'histogram' && (
|
||||||
<>
|
<>
|
||||||
|
@ -76,17 +76,6 @@ export const orderByOptions = [
|
|||||||
{ label: 'Doc Count', value: '_count' },
|
{ 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
|
* This returns the valid options for each of the enabled extended stat
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user