mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Elasticsearch: Improve query type selection (#63402)
* elastic: display query types explicitly * fixed test * fixed test * updated test names * fix react-key-issue
This commit is contained in:
parent
eb2eb65de8
commit
af9176b3a5
@ -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<{}>) => (
|
||||
<ElasticsearchProvider
|
||||
datasource={{ getDatabaseVersion } as ElasticDatasource}
|
||||
query={query}
|
||||
range={getDefaultTimeRange()}
|
||||
onChange={() => {}}
|
||||
onRunQuery={() => {}}
|
||||
>
|
||||
{children}
|
||||
</ElasticsearchProvider>
|
||||
);
|
||||
|
||||
render(<MetricEditor value={count} />, { 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();
|
||||
});
|
||||
});
|
||||
|
@ -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))
|
||||
|
@ -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) => (
|
||||
<QueryEditorRow
|
||||
key={`${metric.type}-${metric.id}`}
|
||||
label={`Metric (${metric.id})`}
|
||||
hidden={metric.hide}
|
||||
onHideClick={() => dispatch(toggleMetricVisibility(metric.id))}
|
||||
onRemoveClick={totalMetrics > 1 && (() => dispatch(removeMetric(metric.id)))}
|
||||
>
|
||||
<MetricEditor value={metric} />
|
||||
{metrics?.map((metric, index) => {
|
||||
switch (metric.type) {
|
||||
case 'logs':
|
||||
return <QueryEditorSpecialMetricRow key={`${metric.type}-${metric.id}`} name="Logs" metric={metric} />;
|
||||
case 'raw_data':
|
||||
return <QueryEditorSpecialMetricRow key={`${metric.type}-${metric.id}`} name="Raw Data" metric={metric} />;
|
||||
case 'raw_document':
|
||||
return (
|
||||
<QueryEditorSpecialMetricRow
|
||||
key={`${metric.type}-${metric.id}`}
|
||||
name="Raw Document"
|
||||
metric={metric}
|
||||
info="(NOTE: Raw document query type is deprecated)"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<QueryEditorRow
|
||||
key={`${metric.type}-${metric.id}`}
|
||||
label={`Metric (${metric.id})`}
|
||||
hidden={metric.hide}
|
||||
onHideClick={() => dispatch(toggleMetricVisibility(metric.id))}
|
||||
onRemoveClick={totalMetrics > 1 && (() => dispatch(removeMetric(metric.id)))}
|
||||
>
|
||||
<MetricEditor value={metric} />
|
||||
|
||||
{!metricAggregationConfig[metric.type].isSingleMetric && index === 0 && (
|
||||
<IconButton iconName="plus" onClick={() => dispatch(addMetric(nextId))} label="add" />
|
||||
)}
|
||||
</QueryEditorRow>
|
||||
))}
|
||||
{!metricAggregationConfig[metric.type].isSingleMetric && index === 0 && (
|
||||
<IconButton iconName="plus" onClick={() => dispatch(addMetric(nextId))} label="add" />
|
||||
)}
|
||||
</QueryEditorRow>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<InlineFieldRow>
|
||||
<InlineSegmentGroup>
|
||||
<InlineLabel width={17} as="div">
|
||||
<span>{name}</span>
|
||||
</InlineLabel>
|
||||
</InlineSegmentGroup>
|
||||
<SettingsEditor metric={metric} previousMetrics={previousMetrics} />
|
||||
{info != null && (
|
||||
<InlineSegmentGroup>
|
||||
<InlineLabel>{info}</InlineLabel>
|
||||
</InlineSegmentGroup>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
);
|
||||
};
|
@ -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<SelectableValue<QueryType>> = [
|
||||
{ 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 <RadioButtonGroup<QueryType> fullWidth={false} options={OPTIONS} value={queryType} onChange={onChange} />;
|
||||
};
|
@ -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<void, [ElasticsearchQuery]>();
|
||||
@ -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(<QueryEditor query={query} datasource={datasourceMock} onChange={noop} onRunQuery={noop} />);
|
||||
|
||||
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: '',
|
||||
|
@ -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<ElasticDatasource, ElasticsearchQuery, ElasticsearchOptions>;
|
||||
@ -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 (
|
||||
<div className={styles.queryFieldWrapper}>
|
||||
<div className={styles.queryItem}>
|
||||
<QueryField
|
||||
query={value}
|
||||
// By default QueryField calls onChange if onBlur is not defined, this will trigger a rerender
|
||||
// And slate will claim the focus, making it impossible to leave the field.
|
||||
onBlur={() => {}}
|
||||
onChange={onChange}
|
||||
placeholder="Lucene Query"
|
||||
placeholder="Enter a lucene query"
|
||||
portalOrigin="elasticsearch"
|
||||
/>
|
||||
</div>
|
||||
@ -109,22 +110,29 @@ const QueryEditorForm = ({ value }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.root}>
|
||||
<InlineLabel width={17}>Query</InlineLabel>
|
||||
<InlineLabel width={17}>Query type</InlineLabel>
|
||||
<div className={styles.queryItem}>
|
||||
<QueryTypeSelector />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.root}>
|
||||
<InlineLabel width={17}>Lucene Query</InlineLabel>
|
||||
<ElasticSearchQueryField onChange={(query) => dispatch(changeQuery(query))} value={value?.query} />
|
||||
|
||||
<InlineField
|
||||
label="Alias"
|
||||
labelWidth={15}
|
||||
disabled={!isTimeSeriesQuery}
|
||||
tooltip="Aliasing only works for timeseries queries (when the last group is 'Date Histogram'). For all other query types this field is ignored."
|
||||
>
|
||||
<Input
|
||||
id={`ES-query-${value.refId}_alias`}
|
||||
placeholder="Alias Pattern"
|
||||
onBlur={(e) => dispatch(changeAliasPattern(e.currentTarget.value))}
|
||||
defaultValue={value.alias}
|
||||
/>
|
||||
</InlineField>
|
||||
{isTimeSeriesQuery && (
|
||||
<InlineField
|
||||
label="Alias"
|
||||
labelWidth={15}
|
||||
tooltip="Aliasing only works for timeseries queries (when the last group is 'Date Histogram'). For all other query types this field is ignored."
|
||||
>
|
||||
<Input
|
||||
id={`ES-query-${value.refId}_alias`}
|
||||
placeholder="Alias Pattern"
|
||||
onBlur={(e) => dispatch(changeAliasPattern(e.currentTarget.value))}
|
||||
defaultValue={value.alias}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MetricAggregationsEditor nextId={nextId} />
|
||||
|
Loading…
Reference in New Issue
Block a user