grafana/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx

135 lines
4.5 KiB
TypeScript

import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { SemVer } from 'semver';
import { getDefaultTimeRange, GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { Alert, InlineField, InlineLabel, Input, QueryField, useStyles2 } from '@grafana/ui';
import { ElasticDatasource } from '../../datasource';
import { useNextId } from '../../hooks/useNextId';
import { useDispatch } from '../../hooks/useStatelessReducer';
import { ElasticsearchOptions, ElasticsearchQuery } from '../../types';
import { isSupportedVersion, unsupportedVersionMessage } from '../../utils';
import { BucketAggregationsEditor } from './BucketAggregationsEditor';
import { ElasticsearchProvider } from './ElasticsearchQueryContext';
import { MetricAggregationsEditor } from './MetricAggregationsEditor';
import { metricAggregationConfig } from './MetricAggregationsEditor/utils';
import { changeAliasPattern, changeQuery } from './state';
export type ElasticQueryEditorProps = QueryEditorProps<ElasticDatasource, ElasticsearchQuery, ElasticsearchOptions>;
// a react hook that returns the elasticsearch database version,
// or `null`, while loading, or if it is not possible to determine the value.
function useElasticVersion(datasource: ElasticDatasource): SemVer | null {
const [version, setVersion] = useState<SemVer | null>(null);
useEffect(() => {
let canceled = false;
datasource.getDatabaseVersion().then(
(version) => {
if (!canceled) {
setVersion(version);
}
},
(error) => {
// we do nothing
console.log(error);
}
);
return () => {
canceled = true;
};
}, [datasource]);
return version;
}
export const QueryEditor = ({ query, onChange, onRunQuery, datasource, range }: ElasticQueryEditorProps) => {
const elasticVersion = useElasticVersion(datasource);
const showUnsupportedMessage = elasticVersion != null && !isSupportedVersion(elasticVersion);
return (
<ElasticsearchProvider
datasource={datasource}
onChange={onChange}
onRunQuery={onRunQuery}
query={query}
range={range || getDefaultTimeRange()}
>
{showUnsupportedMessage && <Alert title={unsupportedVersionMessage} />}
<QueryEditorForm value={query} />
</ElasticsearchProvider>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
root: css`
display: flex;
`,
queryFieldWrapper: css`
flex-grow: 1;
margin: 0 ${theme.spacing(0.5)} ${theme.spacing(0.5)} 0;
`,
});
interface Props {
value: ElasticsearchQuery;
}
export const ElasticSearchQueryField = ({ value, onChange }: { value?: string; onChange: (v: string) => void }) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.queryFieldWrapper}>
<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"
portalOrigin="elasticsearch"
/>
</div>
);
};
const QueryEditorForm = ({ value }: Props) => {
const dispatch = useDispatch();
const nextId = useNextId();
const styles = useStyles2(getStyles);
// To be considered a time series query, the last bucked aggregation must be a Date Histogram
const isTimeSeriesQuery = value?.bucketAggs?.slice(-1)[0]?.type === 'date_histogram';
const showBucketAggregationsEditor = value.metrics?.every(
(metric) => !metricAggregationConfig[metric.type].isSingleMetric
);
return (
<>
<div className={styles.root}>
<InlineLabel width={17}>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>
</div>
<MetricAggregationsEditor nextId={nextId} />
{showBucketAggregationsEditor && <BucketAggregationsEditor nextId={nextId} />}
</>
);
};