grafana/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx
Sarah Zinger 58a71c7e91
Cloudwatch: Add syntax highlighting and autocomplete for "Metric Search" (#43985)
* Create a "monarch" folder with everything you need to do syntax highlighting and autocompletion.

* Use this new monarch folder with existing cloudwatch sql.

* Add metric math syntax highlighting and autocomplete.

* Make autocomplete "smarter":
- search always inserts a string as first arg
- strings can't contain predefined functions
- operators follow the last closing )

* Add some tests for Metric Math's CompletionItemProvider.

* Fixes After CR:
- refactor CompletionItemProvider, so that it only requires args that are dynamic or outside of it's responsibility
- Update and add tests with mocked monaco
- Add more autocomplete suggestions for SEARCH expression functions
- sort keywords and give different priority from function to make more visually distinctive.

* Change QueryEditor to auto-resize and look more like the one in Prometheus.

* Add autocomplete for time periods for the third arg of Search.

* More CR fixes:
- fix missing break
- add unit tests for statementPosition
- fix broken time period
- sort time periods

* Bug fix
2022-02-01 22:53:32 -05:00

187 lines
6.1 KiB
TypeScript

import React, { ChangeEvent, PureComponent } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { EditorField, EditorRow, Space } from '@grafana/experimental';
import { Input } from '@grafana/ui';
import { CloudWatchDatasource } from '../datasource';
import { isMetricsQuery } from '../guards';
import {
CloudWatchJsonData,
CloudWatchMetricsQuery,
CloudWatchQuery,
MetricEditorMode,
MetricQueryType,
} from '../types';
import { Alias, MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from './';
import QueryHeader from './QueryHeader';
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
interface State {
sqlCodeEditorIsDirty: boolean;
}
export const normalizeQuery = ({
namespace,
metricName,
expression,
dimensions,
region,
id,
alias,
statistic,
period,
sqlExpression,
metricQueryType,
metricEditorMode,
...rest
}: CloudWatchMetricsQuery): CloudWatchMetricsQuery => {
const normalizedQuery = {
queryMode: 'Metrics' as const,
namespace: namespace ?? '',
metricName: metricName ?? '',
expression: expression ?? '',
dimensions: dimensions ?? {},
region: region ?? 'default',
id: id ?? '',
alias: alias ?? '',
statistic: statistic ?? 'Average',
period: period ?? '',
metricQueryType: metricQueryType ?? MetricQueryType.Search,
metricEditorMode: metricEditorMode ?? MetricEditorMode.Builder,
sqlExpression: sqlExpression ?? '',
...rest,
};
return !rest.hasOwnProperty('matchExact') ? { ...normalizedQuery, matchExact: true } : normalizedQuery;
};
export class MetricsQueryEditor extends PureComponent<Props, State> {
state = {
sqlCodeEditorIsDirty: false,
};
componentDidMount = () => {
const metricsQuery = this.props.query as CloudWatchMetricsQuery;
const query = normalizeQuery(metricsQuery);
this.props.onChange(query);
};
onChange = (query: CloudWatchQuery) => {
const { onChange, onRunQuery } = this.props;
onChange(query);
onRunQuery();
};
render() {
const { onRunQuery, datasource } = this.props;
const metricsQuery = this.props.query as CloudWatchMetricsQuery;
const query = normalizeQuery(metricsQuery);
return (
<>
<QueryHeader
query={query}
onRunQuery={onRunQuery}
datasource={datasource}
onChange={(newQuery) => {
if (isMetricsQuery(newQuery) && newQuery.metricEditorMode !== query.metricEditorMode) {
this.setState({ sqlCodeEditorIsDirty: false });
}
this.onChange(newQuery);
}}
sqlCodeEditorIsDirty={this.state.sqlCodeEditorIsDirty}
/>
<Space v={0.5} />
{query.metricQueryType === MetricQueryType.Search && (
<>
{query.metricEditorMode === MetricEditorMode.Builder && (
<MetricStatEditor {...{ ...this.props, query }}></MetricStatEditor>
)}
{query.metricEditorMode === MetricEditorMode.Code && (
<MathExpressionQueryField
onRunQuery={onRunQuery}
expression={query.expression ?? ''}
onChange={(expression) => this.props.onChange({ ...query, expression })}
datasource={datasource}
></MathExpressionQueryField>
)}
</>
)}
{query.metricQueryType === MetricQueryType.Query && (
<>
{query.metricEditorMode === MetricEditorMode.Code && (
<SQLCodeEditor
region={query.region}
sql={query.sqlExpression ?? ''}
onChange={(sqlExpression) => {
if (!this.state.sqlCodeEditorIsDirty) {
this.setState({ sqlCodeEditorIsDirty: true });
}
this.props.onChange({ ...metricsQuery, sqlExpression });
}}
onRunQuery={onRunQuery}
datasource={datasource}
/>
)}
{query.metricEditorMode === MetricEditorMode.Builder && (
<>
<SQLBuilderEditor
query={query}
onChange={this.props.onChange}
onRunQuery={onRunQuery}
datasource={datasource}
></SQLBuilderEditor>
</>
)}
</>
)}
<Space v={0.5} />
<EditorRow>
<EditorField
label="ID"
width={26}
optional
tooltip="ID can be used to reference other queries in math expressions. The ID can include numbers, letters, and underscore, and must start with a lowercase letter."
>
<Input
id={`${query.refId}-cloudwatch-metric-query-editor-id`}
onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...metricsQuery, id: event.target.value })
}
type="text"
invalid={!!query.id && !/^$|^[a-z][a-zA-Z0-9_]*$/.test(query.id)}
value={query.id}
/>
</EditorField>
<EditorField label="Period" width={26} tooltip="Minimum interval between points in seconds.">
<Input
id={`${query.refId}-cloudwatch-metric-query-editor-period`}
value={query.period || ''}
placeholder="auto"
onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...metricsQuery, period: event.target.value })
}
/>
</EditorField>
<EditorField
label="Alias"
width={26}
optional
tooltip="Change time series legend name using this field. See documentation for replacement variable formats."
>
<Alias
value={metricsQuery.alias ?? ''}
onChange={(value: string) => this.onChange({ ...metricsQuery, alias: value })}
/>
</EditorField>
</EditorRow>
</>
);
}
}