diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx index 995ecf3c128..3b844138b92 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx @@ -1,4 +1,5 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React, { PropsWithChildren } from 'react'; import { from } from 'rxjs'; @@ -9,7 +10,7 @@ import { defaultBucketAgg } from '../../../queryDef'; import { ElasticsearchQuery } from '../../../types'; import { ElasticsearchProvider } from '../ElasticsearchQueryContext'; -import { Average, UniqueCount } from './../../../types'; +import { Average, Count, UniqueCount } from './../../../types'; import { MetricEditor } from './MetricEditor'; describe('Metric Editor', () => { @@ -85,4 +86,47 @@ describe('Metric Editor', () => { expect(await screen.findByText('No options found')).toBeInTheDocument(); expect(screen.queryByText('None')).not.toBeInTheDocument(); }); + + it('Should not list special metrics', async () => { + const count: Count = { + id: '1', + type: 'count', + }; + + const query: ElasticsearchQuery = { + refId: 'A', + query: '', + metrics: [count], + bucketAggs: [], + }; + + const getDatabaseVersion: ElasticDatasource['getDatabaseVersion'] = jest.fn(() => Promise.resolve(null)); + + const wrapper = ({ children }: PropsWithChildren<{}>) => ( + {}} + onRunQuery={() => {}} + > + {children} + + ); + + render(, { wrapper }); + + act(() => { + userEvent.click(screen.getByText('Count')); + }); + + // we check if the list-of-options is visible by + // checking for an item to exist + expect(await screen.findByText('Extended Stats')).toBeInTheDocument(); + + // now we make sure the should-not-be-shown items are not shown + expect(screen.queryByText('Logs')).toBeNull(); + expect(screen.queryByText('Raw Data')).toBeNull(); + expect(screen.queryByText('Raw Document (deprecated)')).toBeNull(); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx index 14aad375418..a476ff13069 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx @@ -48,6 +48,7 @@ const getTypeOptions = ( return ( Object.entries(metricAggregationConfig) + .filter(([_, config]) => !config.isSingleMetric) // Only showing metrics type supported by the version of ES. // if we cannot determine the version, we assume it is suitable. .filter(([_, { versionRange = '*' }]) => (esVersion != null ? satisfies(esVersion, versionRange) : true)) diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx index c89cc86eef0..a916ef77ba9 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx @@ -4,6 +4,7 @@ import { useDispatch } from '../../../hooks/useStatelessReducer'; import { IconButton } from '../../IconButton'; import { useQuery } from '../ElasticsearchQueryContext'; import { QueryEditorRow } from '../QueryEditorRow'; +import { QueryEditorSpecialMetricRow } from '../QueryEditorSpecialMetricRow'; import { MetricAggregation } from './../../../types'; import { MetricEditor } from './MetricEditor'; @@ -21,21 +22,39 @@ export const MetricAggregationsEditor = ({ nextId }: Props) => { return ( <> - {metrics?.map((metric, index) => ( - + ); + } + })} ); }; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx new file mode 100644 index 00000000000..67f30d472e7 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { InlineFieldRow, InlineLabel, InlineSegmentGroup } from '@grafana/ui'; + +import { MetricAggregation } from '../../types'; + +import { SettingsEditor } from './MetricAggregationsEditor/SettingsEditor'; + +type Props = { + name: string; + metric: MetricAggregation; + info?: string; +}; + +export const QueryEditorSpecialMetricRow = ({ name, metric, info }: Props) => { + // this widget is only used in scenarios when there is only a single + // metric, so the array of "previousMetrics" (meaning all the metrics + // before the current metric), is an ampty-array + const previousMetrics: MetricAggregation[] = []; + + return ( + + + + {name} + + + + {info != null && ( + + {info} + + )} + + ); +}; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx new file mode 100644 index 00000000000..9b8ade09bfa --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { RadioButtonGroup } from '@grafana/ui'; + +import { useDispatch } from '../../hooks/useStatelessReducer'; +import { MetricAggregation } from '../../types'; + +import { useQuery } from './ElasticsearchQueryContext'; +import { changeMetricType } from './MetricAggregationsEditor/state/actions'; + +type QueryType = 'metrics' | 'logs' | 'raw_data' | 'raw_document'; + +const OPTIONS: Array> = [ + { value: 'metrics', label: 'Metrics' }, + { value: 'logs', label: 'Logs' }, + { value: 'raw_data', label: 'Raw Data' }, + { value: 'raw_document', label: 'Raw Document' }, +]; + +function metricTypeToQueryType(type: MetricAggregation['type']): QueryType { + switch (type) { + case 'logs': + case 'raw_data': + case 'raw_document': + return type; + default: + return 'metrics'; + } +} + +function queryTypeToMetricType(type: QueryType): MetricAggregation['type'] { + switch (type) { + case 'logs': + case 'raw_data': + case 'raw_document': + return type; + case 'metrics': + return 'count'; + default: + // should never happen + throw new Error(`invalid query type: ${type}`); + } +} + +export const QueryTypeSelector = () => { + const query = useQuery(); + const dispatch = useDispatch(); + + const firstMetric = query.metrics?.[0]; + + if (firstMetric == null) { + // not sure if this can really happen, but we should handle it anyway + return null; + } + + const queryType = metricTypeToQueryType(firstMetric.type); + + const onChange = (newQueryType: QueryType) => { + dispatch(changeMetricType({ id: firstMetric.id, type: queryTypeToMetricType(newQueryType) })); + }; + + return fullWidth={false} options={OPTIONS} value={queryType} onChange={onChange} />; +}; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx index b92fc8cfc8d..67c19e51607 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx @@ -22,10 +22,15 @@ describe('QueryEditor', () => { metrics: [ { id: '1', - type: 'raw_data', + type: 'count', + }, + ], + bucketAggs: [ + { + type: 'date_histogram', + id: '2', }, ], - bucketAggs: [], }; const onChange = jest.fn(); @@ -51,7 +56,7 @@ describe('QueryEditor', () => { expect(onChange.mock.calls[0][0].alias).toBe(newAlias); }); - it('Should be disabled if last bucket aggregation is not Date Histogram', () => { + it('Should not be shown if last bucket aggregation is not Date Histogram', () => { const query: ElasticsearchQuery = { refId: 'A', query: '', @@ -66,10 +71,10 @@ describe('QueryEditor', () => { render(); - expect(screen.getByLabelText('Alias')).toBeDisabled(); + expect(screen.queryByLabelText('Alias')).toBeNull(); }); - it('Should be enabled if last bucket aggregation is Date Histogram', () => { + it('Should be shown if last bucket aggregation is Date Histogram', () => { const query: ElasticsearchQuery = { refId: 'A', query: '', diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx index 69a4cb4be14..2e60a71865e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx @@ -15,6 +15,7 @@ import { BucketAggregationsEditor } from './BucketAggregationsEditor'; import { ElasticsearchProvider } from './ElasticsearchQueryContext'; import { MetricAggregationsEditor } from './MetricAggregationsEditor'; import { metricAggregationConfig } from './MetricAggregationsEditor/utils'; +import { QueryTypeSelector } from './QueryTypeSelector'; import { changeAliasPattern, changeQuery } from './state'; export type ElasticQueryEditorProps = QueryEditorProps; @@ -66,7 +67,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ root: css` display: flex; `, - queryFieldWrapper: css` + queryItem: css` flex-grow: 1; margin: 0 ${theme.spacing(0.5)} ${theme.spacing(0.5)} 0; `, @@ -80,14 +81,14 @@ export const ElasticSearchQueryField = ({ value, onChange }: { value?: string; o const styles = useStyles2(getStyles); return ( -
+
{}} onChange={onChange} - placeholder="Lucene Query" + placeholder="Enter a lucene query" portalOrigin="elasticsearch" />
@@ -109,22 +110,29 @@ const QueryEditorForm = ({ value }: Props) => { return ( <>
- Query + Query type +
+ +
+
+
+ Lucene Query dispatch(changeQuery(query))} value={value?.query} /> - - dispatch(changeAliasPattern(e.currentTarget.value))} - defaultValue={value.alias} - /> - + {isTimeSeriesQuery && ( + + dispatch(changeAliasPattern(e.currentTarget.value))} + defaultValue={value.alias} + /> + + )}