mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import { Monaco } from '../../cloudwatch-sql/completion/types';
|
||||
import {
|
||||
multiLineFullQuery,
|
||||
singleLineFullQuery,
|
||||
singleLineEmptyQuery,
|
||||
singleLineTwoQueries,
|
||||
multiLineIncompleteQueryWithoutNamespace,
|
||||
} from './test-data';
|
||||
|
||||
const TestData = {
|
||||
[multiLineFullQuery.query]: multiLineFullQuery.tokens,
|
||||
[singleLineFullQuery.query]: singleLineFullQuery.tokens,
|
||||
[singleLineEmptyQuery.query]: singleLineEmptyQuery.tokens,
|
||||
[singleLineTwoQueries.query]: singleLineTwoQueries.tokens,
|
||||
[multiLineIncompleteQueryWithoutNamespace.query]: multiLineIncompleteQueryWithoutNamespace.tokens,
|
||||
};
|
||||
|
||||
// Stub for the Monaco instance. Only implements the parts that are used in cloudwatch sql
|
||||
const MonacoMock: Monaco = {
|
||||
editor: {
|
||||
tokenize: (value: string, languageId: string) => {
|
||||
return TestData[value];
|
||||
},
|
||||
},
|
||||
Range: {
|
||||
containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => {
|
||||
return (
|
||||
position.lineNumber >= range.startLineNumber &&
|
||||
position.lineNumber <= range.endLineNumber &&
|
||||
position.column >= range.startColumn &&
|
||||
position.column <= range.endColumn
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default MonacoMock;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export const afterFunctionQuery = {
|
||||
query: 'AVG() ',
|
||||
tokens: [
|
||||
[
|
||||
{
|
||||
offset: 0,
|
||||
type: 'predefined.cloudwatch-MetricMath',
|
||||
language: 'cloudwatch-MetricMath',
|
||||
},
|
||||
{
|
||||
offset: 3,
|
||||
type: 'delimiter.parenthesis.cloudwatch-MetricMath',
|
||||
language: 'cloudwatch-MetricMath',
|
||||
},
|
||||
{
|
||||
offset: 5,
|
||||
type: 'white.cloudwatch-MetricMath',
|
||||
language: 'cloudwatch-MetricMath',
|
||||
},
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 7,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export { singleLineEmptyQuery } from './singleLineEmptyQuery';
|
||||
export { afterFunctionQuery } from './afterFunctionQuery';
|
||||
export { secondArgQuery } from './secondArgQuery';
|
||||
export { secondArgAfterSearchQuery } from './secondArgAfterSearchQuery';
|
||||
export { thirdArgAfterSearchQuery } from './thirdArgAfterSearchQuery';
|
||||
export { withinStringQuery } from './withinStringQuery';
|
||||
@@ -0,0 +1,19 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export const secondArgAfterSearchQuery = {
|
||||
query: "SEARCH('stuff', )",
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 6, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 7, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 14, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 15, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 16, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 18,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export const secondArgQuery = {
|
||||
query: 'FILL($first, )',
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 4, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 5, type: 'variable.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 11, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 12, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 13, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 14,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export const singleLineEmptyQuery = {
|
||||
query: '',
|
||||
tokens: [] as monacoTypes.Token[][],
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 1,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export const thirdArgAfterSearchQuery = {
|
||||
query: "SEARCH('stuff', 'Average', )",
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 6, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 7, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 14, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 15, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 16, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 25, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 26, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 27, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 28,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export const withinStringQuery = {
|
||||
query: `SEARCH(' {"Custom-Namespace", "Dimension Name With Spaces"}, `,
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: 'predefined.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 6, type: 'delimiter.parenthesis.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 7, type: 'string.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 8, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 9, type: 'delimiter.curly.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 10, type: 'type.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 28, type: 'source.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 30, type: 'type.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 58, type: 'delimiter.curly.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 59, type: 'delimiter.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
{ offset: 60, type: 'white.cloudwatch-MetricMath', language: 'cloudwatch-MetricMath' },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 62,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import { Monaco } from '../../monarch/types';
|
||||
import * as SQLTestData from '../cloudwatch-sql-test-data';
|
||||
import * as MetricMathTestData from '../metric-math-test-data';
|
||||
|
||||
// Stub for the Monaco instance.
|
||||
const MonacoMock: Monaco = {
|
||||
editor: {
|
||||
tokenize: (value: string, languageId: string) => {
|
||||
if (languageId === 'cloudwatch-sql') {
|
||||
const TestData = {
|
||||
[SQLTestData.multiLineFullQuery.query]: SQLTestData.multiLineFullQuery.tokens,
|
||||
[SQLTestData.singleLineFullQuery.query]: SQLTestData.singleLineFullQuery.tokens,
|
||||
[SQLTestData.singleLineEmptyQuery.query]: SQLTestData.singleLineEmptyQuery.tokens,
|
||||
[SQLTestData.singleLineTwoQueries.query]: SQLTestData.singleLineTwoQueries.tokens,
|
||||
[SQLTestData.multiLineIncompleteQueryWithoutNamespace.query]:
|
||||
SQLTestData.multiLineIncompleteQueryWithoutNamespace.tokens,
|
||||
};
|
||||
return TestData[value];
|
||||
}
|
||||
if (languageId === 'cloudwatch-MetricMath') {
|
||||
const TestData = {
|
||||
[MetricMathTestData.singleLineEmptyQuery.query]: MetricMathTestData.singleLineEmptyQuery.tokens,
|
||||
[MetricMathTestData.afterFunctionQuery.query]: MetricMathTestData.afterFunctionQuery.tokens,
|
||||
[MetricMathTestData.secondArgQuery.query]: MetricMathTestData.secondArgQuery.tokens,
|
||||
[MetricMathTestData.secondArgAfterSearchQuery.query]: MetricMathTestData.secondArgAfterSearchQuery.tokens,
|
||||
[MetricMathTestData.withinStringQuery.query]: MetricMathTestData.withinStringQuery.tokens,
|
||||
[MetricMathTestData.thirdArgAfterSearchQuery.query]: MetricMathTestData.thirdArgAfterSearchQuery.tokens,
|
||||
};
|
||||
return TestData[value];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
Range: {
|
||||
containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => {
|
||||
return (
|
||||
position.lineNumber >= range.startLineNumber &&
|
||||
position.lineNumber <= range.endLineNumber &&
|
||||
position.column >= range.startColumn &&
|
||||
position.column <= range.endColumn
|
||||
);
|
||||
},
|
||||
fromPositions: (start: monacoTypes.IPosition, end?: monacoTypes.IPosition) => {
|
||||
return ({} as any) as monacoTypes.Range;
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
CompletionItemInsertTextRule: {
|
||||
InsertAsSnippet: 4,
|
||||
},
|
||||
CompletionItemKind: {
|
||||
Function: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default MonacoMock;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
// Stub for monacoTypes.editor.ITextModel. Only implements the parts that are used in cloudwatch sql
|
||||
// Stub for monacoTypes.editor.ITextModel
|
||||
function TextModel(value: string) {
|
||||
return {
|
||||
getValue: function (eol?: monacoTypes.editor.EndOfLinePreference, preserveBOM?: boolean): string {
|
||||
@@ -13,7 +13,7 @@ function TextModel(value: string) {
|
||||
},
|
||||
getLineLength: function (lineNumber: number): number {
|
||||
const lines = value.split('\n');
|
||||
return lines[lineNumber - 1].trim().length;
|
||||
return lines[lineNumber - 1].length;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { uniq } from 'lodash';
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||
import { getSuggestionKinds } from './suggestionKind';
|
||||
import { getStatementPosition } from './statementPosition';
|
||||
import { TRIGGER_SUGGEST } from './commands';
|
||||
import { TokenType, SuggestionKind, CompletionItemPriority, StatementPosition } from './types';
|
||||
import { LinkedToken } from './LinkedToken';
|
||||
import { TRIGGER_SUGGEST } from '../../monarch/commands';
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types';
|
||||
import { SQLTokenTypes } from './types';
|
||||
import {
|
||||
BY,
|
||||
FROM,
|
||||
@@ -24,45 +20,30 @@ import {
|
||||
STATISTICS,
|
||||
} from '../language';
|
||||
import { getMetricNameToken, getNamespaceToken } from './tokenUtils';
|
||||
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { getStatementPosition } from './statementPosition';
|
||||
import { getSuggestionKinds } from './suggestionKind';
|
||||
|
||||
type CompletionItem = monacoTypes.languages.CompletionItem;
|
||||
|
||||
export class CompletionItemProvider {
|
||||
export class SQLCompletionItemProvider extends CompletionItemProvider {
|
||||
region: string;
|
||||
templateVariables: string[];
|
||||
|
||||
constructor(private datasource: CloudWatchDatasource, private templateSrv: TemplateSrv = getTemplateSrv()) {
|
||||
this.templateVariables = this.datasource.getVariables();
|
||||
constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
|
||||
super(datasource, templateSrv);
|
||||
this.region = datasource.getActualRegion();
|
||||
this.getStatementPosition = getStatementPosition;
|
||||
this.getSuggestionKinds = getSuggestionKinds;
|
||||
this.tokenTypes = SQLTokenTypes;
|
||||
}
|
||||
|
||||
setRegion(region: string) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
getCompletionProvider(monaco: Monaco) {
|
||||
return {
|
||||
triggerCharacters: [' ', '$', ',', '(', "'"],
|
||||
provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => {
|
||||
const currentToken = linkedTokenBuilder(monaco, model, position);
|
||||
const statementPosition = getStatementPosition(currentToken);
|
||||
const suggestionKinds = getSuggestionKinds(statementPosition);
|
||||
const suggestions = await this.getSuggestions(
|
||||
monaco,
|
||||
currentToken,
|
||||
suggestionKinds,
|
||||
statementPosition,
|
||||
position
|
||||
);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getSuggestions(
|
||||
async getSuggestions(
|
||||
monaco: Monaco,
|
||||
currentToken: LinkedToken | null,
|
||||
suggestionKinds: SuggestionKind[],
|
||||
@@ -182,14 +163,14 @@ export class CompletionItemProvider {
|
||||
let dimensionFilter = {};
|
||||
let labelKeyTokens;
|
||||
if (statementPosition === StatementPosition.SchemaFuncExtraArgument) {
|
||||
labelKeyTokens = namespaceToken?.getNextUntil(TokenType.Parenthesis, [
|
||||
TokenType.Delimiter,
|
||||
TokenType.Whitespace,
|
||||
labelKeyTokens = namespaceToken?.getNextUntil(this.tokenTypes.Parenthesis, [
|
||||
this.tokenTypes.Delimiter,
|
||||
this.tokenTypes.Whitespace,
|
||||
]);
|
||||
} else if (statementPosition === StatementPosition.AfterGroupByKeywords) {
|
||||
labelKeyTokens = currentToken?.getPreviousUntil(TokenType.Keyword, [
|
||||
TokenType.Delimiter,
|
||||
TokenType.Whitespace,
|
||||
labelKeyTokens = currentToken?.getPreviousUntil(this.tokenTypes.Keyword, [
|
||||
this.tokenTypes.Delimiter,
|
||||
this.tokenTypes.Whitespace,
|
||||
]);
|
||||
}
|
||||
dimensionFilter = (labelKeyTokens || []).reduce((acc, curr) => {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import MonacoMock from '../../__mocks__/cloudwatch-sql/Monaco';
|
||||
import TextModel from '../../__mocks__/cloudwatch-sql/TextModel';
|
||||
import { multiLineFullQuery, singleLineFullQuery } from '../../__mocks__/cloudwatch-sql/test-data';
|
||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||
import { TokenType } from './types';
|
||||
import { DESC, SELECT } from '../language';
|
||||
|
||||
describe('linkedTokenBuilder', () => {
|
||||
describe('singleLineFullQuery', () => {
|
||||
const testModel = TextModel(singleLineFullQuery.query);
|
||||
|
||||
it('should add correct references to next LinkedToken', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 };
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
expect(current?.is(TokenType.Keyword, SELECT)).toBeTruthy();
|
||||
expect(current?.getNextNonWhiteSpaceToken()?.is(TokenType.Function, 'AVG')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add correct references to previous LinkedToken', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 1, column: singleLineFullQuery.query.length };
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
expect(current?.is(TokenType.Number, '10')).toBeTruthy();
|
||||
expect(current?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, 'LIMIT')).toBeTruthy();
|
||||
expect(
|
||||
current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, DESC)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiLineFullQuery', () => {
|
||||
const testModel = TextModel(multiLineFullQuery.query);
|
||||
|
||||
it('should add LinkedToken with whitespace in case empty lines', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 3, column: 0 };
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
expect(current).not.toBeNull();
|
||||
expect(current?.isWhiteSpace()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add correct references to next LinkedToken', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 };
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
expect(current?.is(TokenType.Keyword, SELECT)).toBeTruthy();
|
||||
expect(current?.getNextNonWhiteSpaceToken()?.is(TokenType.Function, 'AVG')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add correct references to previous LinkedToken even when references spans over multiple lines', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 6, column: 7 };
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
expect(current?.is(TokenType.Number, '10')).toBeTruthy();
|
||||
expect(current?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, 'LIMIT')).toBeTruthy();
|
||||
expect(
|
||||
current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, DESC)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,28 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import MonacoMock from '../../__mocks__/cloudwatch-sql/Monaco';
|
||||
import TextModel from '../../__mocks__/cloudwatch-sql/TextModel';
|
||||
import MonacoMock from '../../__mocks__/monarch/Monaco';
|
||||
import TextModel from '../../__mocks__/monarch/TextModel';
|
||||
import {
|
||||
multiLineFullQuery,
|
||||
singleLineFullQuery,
|
||||
singleLineEmptyQuery,
|
||||
singleLineTwoQueries,
|
||||
} from '../../__mocks__/cloudwatch-sql/test-data';
|
||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||
import { StatementPosition } from './types';
|
||||
} from '../../__mocks__/cloudwatch-sql-test-data';
|
||||
import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder';
|
||||
import { StatementPosition } from '../../monarch/types';
|
||||
import { getStatementPosition } from './statementPosition';
|
||||
import cloudWatchSqlLanguageDefinition from '../definition';
|
||||
import { SQLTokenTypes } from './types';
|
||||
|
||||
describe('statementPosition', () => {
|
||||
function assertPosition(query: string, position: monacoTypes.IPosition, expected: StatementPosition) {
|
||||
const testModel = TextModel(query);
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
const statementPosition = getStatementPosition(current);
|
||||
expect(statementPosition).toBe(expected);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { AND, ASC, BY, DESC, EQUALS, FROM, GROUP, NOT_EQUALS, ORDER, SCHEMA, SELECT, WHERE } from '../language';
|
||||
import { LinkedToken } from './LinkedToken';
|
||||
import { StatementPosition, TokenType } from './types';
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import { StatementPosition } from '../../monarch/types';
|
||||
import { SQLTokenTypes } from './types';
|
||||
|
||||
export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition {
|
||||
const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken();
|
||||
const previousKeyword = currentToken?.getPreviousKeyword();
|
||||
|
||||
const previousIsSlash = currentToken?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Operator, '/');
|
||||
const previousIsSlash = currentToken?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Operator, '/');
|
||||
if (
|
||||
currentToken === null ||
|
||||
(currentToken.isWhiteSpace() && currentToken.previous === null) ||
|
||||
(currentToken.is(TokenType.Keyword, SELECT) && currentToken.previous === null) ||
|
||||
(currentToken.is(SQLTokenTypes.Keyword, SELECT) && currentToken.previous === null) ||
|
||||
previousIsSlash ||
|
||||
(currentToken.isIdentifier() && (previousIsSlash || currentToken?.previous === null))
|
||||
) {
|
||||
@@ -22,7 +23,7 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen
|
||||
}
|
||||
|
||||
if (
|
||||
(previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()')) &&
|
||||
(previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '(') || currentToken?.is(SQLTokenTypes.Parenthesis, '()')) &&
|
||||
previousKeyword?.value === SELECT
|
||||
) {
|
||||
return StatementPosition.AfterSelectFuncFirstArgument;
|
||||
@@ -37,20 +38,20 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen
|
||||
}
|
||||
|
||||
if (
|
||||
(previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()')) &&
|
||||
(previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '(') || currentToken?.is(SQLTokenTypes.Parenthesis, '()')) &&
|
||||
previousKeyword?.value === SCHEMA
|
||||
) {
|
||||
return StatementPosition.SchemaFuncFirstArgument;
|
||||
}
|
||||
|
||||
if (previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(TokenType.Delimiter, ',')) {
|
||||
if (previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ',')) {
|
||||
return StatementPosition.SchemaFuncExtraArgument;
|
||||
}
|
||||
|
||||
if (
|
||||
(previousKeyword?.value === FROM && previousNonWhiteSpace?.isDoubleQuotedString()) ||
|
||||
(previousKeyword?.value === FROM && previousNonWhiteSpace?.isVariable()) ||
|
||||
(previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(TokenType.Parenthesis, ')'))
|
||||
(previousKeyword?.value === SCHEMA && previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')'))
|
||||
) {
|
||||
return StatementPosition.AfterFrom;
|
||||
}
|
||||
@@ -58,8 +59,8 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen
|
||||
if (
|
||||
previousKeyword?.value === WHERE &&
|
||||
(previousNonWhiteSpace?.isKeyword() ||
|
||||
previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') ||
|
||||
previousNonWhiteSpace?.is(TokenType.Operator, AND))
|
||||
previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, '(') ||
|
||||
previousNonWhiteSpace?.is(SQLTokenTypes.Operator, AND))
|
||||
) {
|
||||
return StatementPosition.WhereKey;
|
||||
}
|
||||
@@ -73,51 +74,52 @@ export function getStatementPosition(currentToken: LinkedToken | null): Statemen
|
||||
|
||||
if (
|
||||
previousKeyword?.value === WHERE &&
|
||||
(previousNonWhiteSpace?.is(TokenType.Operator, EQUALS) || previousNonWhiteSpace?.is(TokenType.Operator, NOT_EQUALS))
|
||||
(previousNonWhiteSpace?.is(SQLTokenTypes.Operator, EQUALS) ||
|
||||
previousNonWhiteSpace?.is(SQLTokenTypes.Operator, NOT_EQUALS))
|
||||
) {
|
||||
return StatementPosition.WhereValue;
|
||||
}
|
||||
|
||||
if (
|
||||
previousKeyword?.value === WHERE &&
|
||||
(previousNonWhiteSpace?.isString() || previousNonWhiteSpace?.is(TokenType.Parenthesis, ')'))
|
||||
(previousNonWhiteSpace?.isString() || previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis, ')'))
|
||||
) {
|
||||
return StatementPosition.AfterWhereValue;
|
||||
}
|
||||
|
||||
if (
|
||||
previousKeyword?.is(TokenType.Keyword, BY) &&
|
||||
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) &&
|
||||
(previousNonWhiteSpace?.is(TokenType.Keyword, BY) || previousNonWhiteSpace?.is(TokenType.Delimiter, ','))
|
||||
previousKeyword?.is(SQLTokenTypes.Keyword, BY) &&
|
||||
previousKeyword?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, GROUP) &&
|
||||
(previousNonWhiteSpace?.is(SQLTokenTypes.Keyword, BY) || previousNonWhiteSpace?.is(SQLTokenTypes.Delimiter, ','))
|
||||
) {
|
||||
return StatementPosition.AfterGroupByKeywords;
|
||||
}
|
||||
|
||||
if (
|
||||
previousKeyword?.is(TokenType.Keyword, BY) &&
|
||||
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) &&
|
||||
previousKeyword?.is(SQLTokenTypes.Keyword, BY) &&
|
||||
previousKeyword?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, GROUP) &&
|
||||
(previousNonWhiteSpace?.isIdentifier() || previousNonWhiteSpace?.isDoubleQuotedString())
|
||||
) {
|
||||
return StatementPosition.AfterGroupBy;
|
||||
}
|
||||
|
||||
if (
|
||||
previousNonWhiteSpace?.is(TokenType.Keyword, BY) &&
|
||||
previousNonWhiteSpace?.getPreviousKeyword()?.is(TokenType.Keyword, ORDER)
|
||||
previousNonWhiteSpace?.is(SQLTokenTypes.Keyword, BY) &&
|
||||
previousNonWhiteSpace?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, ORDER)
|
||||
) {
|
||||
return StatementPosition.AfterOrderByKeywords;
|
||||
}
|
||||
|
||||
if (
|
||||
previousKeyword?.is(TokenType.Keyword, BY) &&
|
||||
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, ORDER) &&
|
||||
previousNonWhiteSpace?.is(TokenType.Parenthesis) &&
|
||||
previousNonWhiteSpace?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Function)
|
||||
previousKeyword?.is(SQLTokenTypes.Keyword, BY) &&
|
||||
previousKeyword?.getPreviousKeyword()?.is(SQLTokenTypes.Keyword, ORDER) &&
|
||||
previousNonWhiteSpace?.is(SQLTokenTypes.Parenthesis) &&
|
||||
previousNonWhiteSpace?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Function)
|
||||
) {
|
||||
return StatementPosition.AfterOrderByFunction;
|
||||
}
|
||||
|
||||
if (previousKeyword?.is(TokenType.Keyword, DESC) || previousKeyword?.is(TokenType.Keyword, ASC)) {
|
||||
if (previousKeyword?.is(SQLTokenTypes.Keyword, DESC) || previousKeyword?.is(SQLTokenTypes.Keyword, ASC)) {
|
||||
return StatementPosition.AfterOrderByDirection;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StatementPosition, SuggestionKind } from './types';
|
||||
import { StatementPosition, SuggestionKind } from '../../monarch/types';
|
||||
|
||||
export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] {
|
||||
switch (statementPosition) {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import { LinkedToken } from './LinkedToken';
|
||||
import MonacoMock from '../../__mocks__/cloudwatch-sql/Monaco';
|
||||
import TextModel from '../../__mocks__/cloudwatch-sql/TextModel';
|
||||
import MonacoMock from '../../__mocks__/monarch/Monaco';
|
||||
import TextModel from '../../__mocks__/monarch/TextModel';
|
||||
import {
|
||||
multiLineFullQuery,
|
||||
singleLineFullQuery,
|
||||
singleLineTwoQueries,
|
||||
multiLineIncompleteQueryWithoutNamespace,
|
||||
} from '../../__mocks__/cloudwatch-sql/test-data';
|
||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||
import { TokenType } from './types';
|
||||
} from '../../__mocks__/cloudwatch-sql-test-data';
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder';
|
||||
import { SQLTokenTypes } from './types';
|
||||
import { getMetricNameToken, getNamespaceToken, getSelectStatisticToken, getSelectToken } from './tokenUtils';
|
||||
import { SELECT } from '../language';
|
||||
import cloudWatchSqlLanguageDefinition from '../definition';
|
||||
|
||||
const getToken = (
|
||||
query: string,
|
||||
@@ -19,7 +20,13 @@ const getToken = (
|
||||
invokeFunction: (token: LinkedToken | null) => LinkedToken | null
|
||||
) => {
|
||||
const testModel = TextModel(query);
|
||||
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
return invokeFunction(current);
|
||||
};
|
||||
|
||||
@@ -33,7 +40,7 @@ describe('tokenUtils', () => {
|
||||
const token = getToken(query, position, getSelectToken);
|
||||
expect(token).not.toBeNull();
|
||||
expect(token?.value).toBe(SELECT);
|
||||
expect(token?.type).toBe(TokenType.Keyword);
|
||||
expect(token?.type).toBe(SQLTokenTypes.Keyword);
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -44,7 +51,7 @@ describe('tokenUtils', () => {
|
||||
])('getSelectToken should return the right token', (query: string, position: monacoTypes.IPosition) => {
|
||||
const token = getToken(query, position, getSelectStatisticToken);
|
||||
expect(token).not.toBeNull();
|
||||
expect(token?.type).toBe(TokenType.Function);
|
||||
expect(token?.type).toBe(SQLTokenTypes.Function);
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -58,7 +65,7 @@ describe('tokenUtils', () => {
|
||||
const token = getToken(query, position, getSelectStatisticToken);
|
||||
expect(token).not.toBeNull();
|
||||
expect(token?.value).toBe(value);
|
||||
expect(token?.type).toBe(TokenType.Function);
|
||||
expect(token?.type).toBe(SQLTokenTypes.Function);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -73,19 +80,19 @@ describe('tokenUtils', () => {
|
||||
const token = getToken(query, position, getMetricNameToken);
|
||||
expect(token).not.toBeNull();
|
||||
expect(token?.value).toBe(value);
|
||||
expect(token?.type).toBe(TokenType.Identifier);
|
||||
expect(token?.type).toBe(SQLTokenTypes.Identifier);
|
||||
}
|
||||
);
|
||||
|
||||
test.each([
|
||||
[singleLineFullQuery.query, '"AWS/EC2"', TokenType.Type, { lineNumber: 1, column: 50 }],
|
||||
[multiLineFullQuery.query, '"AWS/ECS"', TokenType.Type, { lineNumber: 5, column: 10 }],
|
||||
[singleLineTwoQueries.query, '"AWS/EC2"', TokenType.Type, { lineNumber: 1, column: 30 }],
|
||||
[singleLineTwoQueries.query, '"AWS/ECS"', TokenType.Type, { lineNumber: 1, column: 185 }],
|
||||
[singleLineFullQuery.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 50 }],
|
||||
[multiLineFullQuery.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 5, column: 10 }],
|
||||
[singleLineTwoQueries.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 30 }],
|
||||
[singleLineTwoQueries.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 1, column: 185 }],
|
||||
[multiLineIncompleteQueryWithoutNamespace.query, undefined, undefined, { lineNumber: 2, column: 5 }],
|
||||
])(
|
||||
'getNamespaceToken should return the right token',
|
||||
(query: string, value: string | undefined, tokenType: TokenType | undefined, position: monacoTypes.IPosition) => {
|
||||
(query: string, value: string | undefined, tokenType: string | undefined, position: monacoTypes.IPosition) => {
|
||||
const token = getToken(query, position, getNamespaceToken);
|
||||
expect(token?.value).toBe(value);
|
||||
expect(token?.type).toBe(tokenType);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LinkedToken } from './LinkedToken';
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import { FROM, SCHEMA, SELECT } from '../language';
|
||||
import { TokenType } from './types';
|
||||
import { SQLTokenTypes } from './types';
|
||||
|
||||
export const getSelectToken = (currentToken: LinkedToken | null) =>
|
||||
currentToken?.getPreviousOfType(TokenType.Keyword, SELECT) ?? null;
|
||||
currentToken?.getPreviousOfType(SQLTokenTypes.Keyword, SELECT) ?? null;
|
||||
|
||||
export const getSelectStatisticToken = (currentToken: LinkedToken | null) => {
|
||||
const assumedStatisticToken = getSelectToken(currentToken)?.getNextNonWhiteSpaceToken();
|
||||
@@ -18,7 +18,7 @@ export const getMetricNameToken = (currentToken: LinkedToken | null) => {
|
||||
|
||||
export const getFromKeywordToken = (currentToken: LinkedToken | null) => {
|
||||
const selectToken = getSelectToken(currentToken);
|
||||
return selectToken?.getNextOfType(TokenType.Keyword, FROM);
|
||||
return selectToken?.getNextOfType(SQLTokenTypes.Keyword, FROM);
|
||||
};
|
||||
|
||||
export const getNamespaceToken = (currentToken: LinkedToken | null) => {
|
||||
@@ -30,7 +30,7 @@ export const getNamespaceToken = (currentToken: LinkedToken | null) => {
|
||||
) {
|
||||
// schema is not used
|
||||
return nextNonWhiteSpace;
|
||||
} else if (nextNonWhiteSpace?.isKeyword() && nextNonWhiteSpace.next?.is(TokenType.Parenthesis, '(')) {
|
||||
} else if (nextNonWhiteSpace?.isKeyword() && nextNonWhiteSpace.next?.is(SQLTokenTypes.Parenthesis, '(')) {
|
||||
// schema is specified
|
||||
const assumedNamespaceToken = nextNonWhiteSpace.next?.next;
|
||||
if (assumedNamespaceToken?.isDoubleQuotedString() || assumedNamespaceToken?.isVariable()) {
|
||||
|
||||
@@ -1,76 +1,15 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import { TokenTypes } from '../../monarch/types';
|
||||
|
||||
export enum TokenType {
|
||||
Parenthesis = 'delimiter.parenthesis.sql',
|
||||
Whitespace = 'white.sql',
|
||||
Keyword = 'keyword.sql',
|
||||
Delimiter = 'delimiter.sql',
|
||||
Operator = 'operator.sql',
|
||||
Identifier = 'identifier.sql',
|
||||
Type = 'type.sql',
|
||||
Function = 'predefined.sql',
|
||||
Number = 'number.sql',
|
||||
String = 'string.sql',
|
||||
Variable = 'variable.sql',
|
||||
}
|
||||
|
||||
export enum StatementPosition {
|
||||
Unknown,
|
||||
SelectKeyword,
|
||||
AfterSelectKeyword,
|
||||
AfterSelectFuncFirstArgument,
|
||||
AfterFromKeyword,
|
||||
SchemaFuncFirstArgument,
|
||||
SchemaFuncExtraArgument,
|
||||
FromKeyword,
|
||||
AfterFrom,
|
||||
WhereKey,
|
||||
WhereComparisonOperator,
|
||||
WhereValue,
|
||||
AfterWhereValue,
|
||||
AfterGroupByKeywords,
|
||||
AfterGroupBy,
|
||||
AfterOrderByKeywords,
|
||||
AfterOrderByFunction,
|
||||
AfterOrderByDirection,
|
||||
}
|
||||
|
||||
export enum SuggestionKind {
|
||||
SelectKeyword,
|
||||
FunctionsWithArguments,
|
||||
Metrics,
|
||||
FromKeyword,
|
||||
SchemaKeyword,
|
||||
Namespaces,
|
||||
LabelKeys,
|
||||
WhereKeyword,
|
||||
GroupByKeywords,
|
||||
OrderByKeywords,
|
||||
FunctionsWithoutArguments,
|
||||
LimitKeyword,
|
||||
SortOrderDirectionKeyword,
|
||||
ComparisonOperators,
|
||||
LabelValues,
|
||||
LogicalOperators,
|
||||
}
|
||||
|
||||
export enum CompletionItemPriority {
|
||||
High = 'a',
|
||||
MediumHigh = 'd',
|
||||
Medium = 'g',
|
||||
MediumLow = 'k',
|
||||
Low = 'q',
|
||||
}
|
||||
|
||||
export interface Editor {
|
||||
tokenize: (value: string, languageId: string) => monacoTypes.Token[][];
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => boolean;
|
||||
}
|
||||
|
||||
export interface Monaco {
|
||||
editor: Editor;
|
||||
Range: Range;
|
||||
}
|
||||
export const SQLTokenTypes: TokenTypes = {
|
||||
Parenthesis: 'delimiter.parenthesis.sql',
|
||||
Whitespace: 'white.sql',
|
||||
Keyword: 'keyword.sql',
|
||||
Delimiter: 'delimiter.sql',
|
||||
Operator: 'operator.sql',
|
||||
Identifier: 'identifier.sql',
|
||||
Type: 'type.sql',
|
||||
Function: 'predefined.sql',
|
||||
Number: 'number.sql',
|
||||
String: 'string.sql',
|
||||
Variable: 'variable.sql',
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export default {
|
||||
import { LanguageDefinition } from '../monarch/register';
|
||||
|
||||
const cloudWatchSqlLanguageDefinition: LanguageDefinition = {
|
||||
id: 'cloudwatch-sql',
|
||||
extensions: ['.cloudwatchSql'],
|
||||
aliases: ['CloudWatch', 'cloudwatch', 'CloudWatchSQL'],
|
||||
mimetypes: [],
|
||||
loader: () => import('./language'),
|
||||
};
|
||||
export default cloudWatchSqlLanguageDefinition;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Monaco } from '@grafana/ui';
|
||||
import { CompletionItemProvider } from './completion/CompletionItemProvider';
|
||||
import language from './definition';
|
||||
|
||||
export const registerLanguage = (monaco: Monaco, sqlCompletionItemProvider: CompletionItemProvider) => {
|
||||
const { id, loader } = language;
|
||||
|
||||
const languages = monaco.languages.getLanguages();
|
||||
if (languages.find((l) => l.id === id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
monaco.languages.register({ id });
|
||||
loader().then((monarch) => {
|
||||
monaco.languages.setMonarchTokensProvider(id, monarch.language);
|
||||
monaco.languages.setLanguageConfiguration(id, monarch.conf);
|
||||
monaco.languages.registerCompletionItemProvider(id, sqlCompletionItemProvider.getCompletionProvider(monaco));
|
||||
});
|
||||
};
|
||||
@@ -1,28 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Input } from '@grafana/ui';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { CodeEditor, Monaco } from '@grafana/ui';
|
||||
import language from '../metric-math/definition';
|
||||
import { registerLanguage } from '../monarch/register';
|
||||
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { TRIGGER_SUGGEST } from '../monarch/commands';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: string) => void;
|
||||
onRunQuery: () => void;
|
||||
expression: string;
|
||||
datasource: CloudWatchDatasource;
|
||||
}
|
||||
|
||||
export function MathExpressionQueryField({ expression: query, onChange, onRunQuery }: React.PropsWithChildren<Props>) {
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && (event.shiftKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
onRunQuery();
|
||||
}
|
||||
};
|
||||
export function MathExpressionQueryField({
|
||||
expression: query,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
datasource,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const onEditorMount = useCallback(
|
||||
(editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {}));
|
||||
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
|
||||
const text = editor.getValue();
|
||||
onChange(text);
|
||||
onRunQuery();
|
||||
});
|
||||
|
||||
// auto resizes the editor to be the height of the content it holds
|
||||
// this code comes from the Prometheus query editor.
|
||||
// We may wish to consider abstracting it into the grafana/ui repo in the future
|
||||
const updateElementHeight = () => {
|
||||
const containerDiv = containerRef.current;
|
||||
if (containerDiv !== null && editor.getContentHeight() < 200) {
|
||||
const pixelHeight = editor.getContentHeight();
|
||||
containerDiv.style.height = `${pixelHeight}px`;
|
||||
containerDiv.style.width = '100%';
|
||||
const pixelWidth = containerDiv.clientWidth;
|
||||
editor.layout({ width: pixelWidth, height: pixelHeight });
|
||||
}
|
||||
};
|
||||
|
||||
editor.onDidContentSizeChange(updateElementHeight);
|
||||
updateElementHeight();
|
||||
},
|
||||
[onChange, onRunQuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
name="Query"
|
||||
value={query}
|
||||
placeholder="Enter a math expression"
|
||||
onBlur={onRunQuery}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<div ref={containerRef}>
|
||||
<CodeEditor
|
||||
monacoOptions={{
|
||||
// without this setting, the auto-resize functionality causes an infinite loop, don't remove it!
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
// These additional options are style focused and are a subset of those in the query editor in Prometheus
|
||||
fontSize: 14,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
},
|
||||
suggestFontSize: 12,
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
language={language.id}
|
||||
value={query}
|
||||
onBlur={(value) => {
|
||||
if (value !== query) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
onBeforeEditorMount={(monaco: Monaco) =>
|
||||
registerLanguage(monaco, language, datasource.metricMathCompletionItemProvider)
|
||||
}
|
||||
onEditorDidMount={onEditorMount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
|
||||
onRunQuery={onRunQuery}
|
||||
expression={query.expression ?? ''}
|
||||
onChange={(expression) => this.props.onChange({ ...query, expression })}
|
||||
datasource={datasource}
|
||||
></MathExpressionQueryField>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -3,8 +3,8 @@ import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { CodeEditor, Monaco } from '@grafana/ui';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import language from '../cloudwatch-sql/definition';
|
||||
import { TRIGGER_SUGGEST } from '../cloudwatch-sql/completion/commands';
|
||||
import { registerLanguage } from '../cloudwatch-sql/register';
|
||||
import { TRIGGER_SUGGEST } from '../monarch/commands';
|
||||
import { registerLanguage } from '../monarch/register';
|
||||
|
||||
export interface Props {
|
||||
region: string;
|
||||
@@ -43,7 +43,7 @@ export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange,
|
||||
}}
|
||||
showMiniMap={false}
|
||||
showLineNumbers={true}
|
||||
onBeforeEditorMount={(monaco: Monaco) => registerLanguage(monaco, datasource.sqlCompletionItemProvider)}
|
||||
onBeforeEditorMount={(monaco: Monaco) => registerLanguage(monaco, language, datasource.sqlCompletionItemProvider)}
|
||||
onEditorDidMount={onEditorMount}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,8 @@ import { increasingInterval } from './utils/rxjs/increasingInterval';
|
||||
import { toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
|
||||
import { addDataLinksToLogsResponse } from './utils/datalinks';
|
||||
import { runWithRetry } from './utils/logsRetry';
|
||||
import { CompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider';
|
||||
import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider';
|
||||
import { MetricMathCompletionItemProvider } from './metric-math/completion/CompletionItemProvider';
|
||||
|
||||
const DS_QUERY_ENDPOINT = '/api/ds/query';
|
||||
|
||||
@@ -89,7 +90,10 @@ export class CloudWatchDatasource
|
||||
defaultRegion: any;
|
||||
datasourceName: string;
|
||||
languageProvider: CloudWatchLanguageProvider;
|
||||
sqlCompletionItemProvider: CompletionItemProvider;
|
||||
sqlCompletionItemProvider: SQLCompletionItemProvider;
|
||||
|
||||
metricMathCompletionItemProvider: MetricMathCompletionItemProvider;
|
||||
|
||||
tracingDataSourceUid?: string;
|
||||
logsTimeout: string;
|
||||
|
||||
@@ -118,7 +122,8 @@ export class CloudWatchDatasource
|
||||
this.languageProvider = new CloudWatchLanguageProvider(this);
|
||||
this.tracingDataSourceUid = instanceSettings.jsonData.tracingDatasourceUid;
|
||||
this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m';
|
||||
this.sqlCompletionItemProvider = new CompletionItemProvider(this);
|
||||
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv);
|
||||
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv);
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<CloudWatchQuery>): Observable<DataQueryResponse> {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import MonacoMock from '../../__mocks__/monarch/Monaco';
|
||||
import TextModel from '../../__mocks__/monarch/TextModel';
|
||||
import { MetricMathCompletionItemProvider } from './CompletionItemProvider';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import cloudWatchMetricMathLanguageDefinition from '../definition';
|
||||
import { Monaco, monacoTypes } from '@grafana/ui';
|
||||
import { IPosition } from 'monaco-editor';
|
||||
import {
|
||||
METRIC_MATH_FNS,
|
||||
METRIC_MATH_KEYWORDS,
|
||||
METRIC_MATH_OPERATORS,
|
||||
METRIC_MATH_PERIODS,
|
||||
METRIC_MATH_STATISTIC_KEYWORD_STRINGS,
|
||||
} from '../language';
|
||||
import * as MetricMathTestData from '../../__mocks__/metric-math-test-data';
|
||||
|
||||
const getSuggestions = async (value: string, position: IPosition) => {
|
||||
const setup = new MetricMathCompletionItemProvider(
|
||||
({
|
||||
getVariables: () => [],
|
||||
getActualRegion: () => 'us-east-2',
|
||||
} as any) as CloudWatchDatasource,
|
||||
getTemplateSrv()
|
||||
);
|
||||
const monaco = MonacoMock as Monaco;
|
||||
const provider = setup.getCompletionProvider(monaco, cloudWatchMetricMathLanguageDefinition);
|
||||
const { suggestions } = await provider.provideCompletionItems(
|
||||
TextModel(value) as monacoTypes.editor.ITextModel,
|
||||
position
|
||||
);
|
||||
return suggestions;
|
||||
};
|
||||
describe('MetricMath: CompletionItemProvider', () => {
|
||||
describe('getSuggestions', () => {
|
||||
it('returns a suggestion for every metric math function when the input field is empty', async () => {
|
||||
const { query, position } = MetricMathTestData.singleLineEmptyQuery;
|
||||
const suggestions = await getSuggestions(query, position);
|
||||
expect(suggestions.length).toEqual(METRIC_MATH_FNS.length);
|
||||
});
|
||||
|
||||
it('returns a suggestion for every metric math operator when at the end of a function', async () => {
|
||||
const { query, position } = MetricMathTestData.afterFunctionQuery;
|
||||
const suggestions = await getSuggestions(query, position);
|
||||
expect(suggestions.length).toEqual(METRIC_MATH_OPERATORS.length);
|
||||
});
|
||||
|
||||
it('returns a suggestion for every metric math function and keyword if at the start of the second argument of a function', async () => {
|
||||
const { query, position } = MetricMathTestData.secondArgQuery;
|
||||
const suggestions = await getSuggestions(query, position);
|
||||
expect(suggestions.length).toEqual(METRIC_MATH_FNS.length + METRIC_MATH_KEYWORDS.length);
|
||||
});
|
||||
|
||||
it('does not have any particular suggestions if within a string', async () => {
|
||||
const { query, position } = MetricMathTestData.withinStringQuery;
|
||||
const suggestions = await getSuggestions(query, position);
|
||||
expect(suggestions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns a suggestion for every statistic if the second arg of a search function', async () => {
|
||||
const { query, position } = MetricMathTestData.secondArgAfterSearchQuery;
|
||||
const suggestions = await getSuggestions(query, position);
|
||||
expect(suggestions.length).toEqual(METRIC_MATH_STATISTIC_KEYWORD_STRINGS.length);
|
||||
});
|
||||
|
||||
it('returns a suggestion for every period if the third arg of a search function', async () => {
|
||||
const { query, position } = MetricMathTestData.thirdArgAfterSearchQuery;
|
||||
const suggestions = await getSuggestions(query, position);
|
||||
expect(suggestions.length).toEqual(METRIC_MATH_PERIODS.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||
import { TRIGGER_SUGGEST } from '../../monarch/commands';
|
||||
import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types';
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import {
|
||||
METRIC_MATH_FNS,
|
||||
METRIC_MATH_KEYWORDS,
|
||||
METRIC_MATH_OPERATORS,
|
||||
METRIC_MATH_PERIODS,
|
||||
METRIC_MATH_STATISTIC_KEYWORD_STRINGS,
|
||||
} from '../language';
|
||||
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
|
||||
import { MetricMathTokenTypes } from './types';
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { getStatementPosition } from './statementPosition';
|
||||
import { getSuggestionKinds } from './suggestionKind';
|
||||
|
||||
type CompletionItem = monacoTypes.languages.CompletionItem;
|
||||
|
||||
export class MetricMathCompletionItemProvider extends CompletionItemProvider {
|
||||
constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
|
||||
super(datasource, templateSrv);
|
||||
this.getStatementPosition = getStatementPosition;
|
||||
this.getSuggestionKinds = getSuggestionKinds;
|
||||
this.tokenTypes = MetricMathTokenTypes;
|
||||
}
|
||||
|
||||
async getSuggestions(
|
||||
monaco: Monaco,
|
||||
currentToken: LinkedToken | null,
|
||||
suggestionKinds: SuggestionKind[],
|
||||
statementPosition: StatementPosition,
|
||||
position: monacoTypes.IPosition
|
||||
): Promise<CompletionItem[]> {
|
||||
let suggestions: CompletionItem[] = [];
|
||||
const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis();
|
||||
const range =
|
||||
invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range;
|
||||
|
||||
const toCompletionItem = (value: string, rest: Partial<CompletionItem> = {}) => {
|
||||
const item: CompletionItem = {
|
||||
label: value,
|
||||
insertText: value,
|
||||
kind: monaco.languages.CompletionItemKind.Field,
|
||||
range,
|
||||
sortText: CompletionItemPriority.Medium,
|
||||
...rest,
|
||||
};
|
||||
return item;
|
||||
};
|
||||
|
||||
function addSuggestion(value: string, rest: Partial<CompletionItem> = {}) {
|
||||
suggestions = [...suggestions, toCompletionItem(value, rest)];
|
||||
}
|
||||
|
||||
for (const suggestion of suggestionKinds) {
|
||||
switch (suggestion) {
|
||||
case SuggestionKind.FunctionsWithArguments:
|
||||
METRIC_MATH_FNS.map((f) =>
|
||||
addSuggestion(f, {
|
||||
insertText: f === 'SEARCH' ? `${f}('$0')` : `${f}($0)`,
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
command: TRIGGER_SUGGEST,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case SuggestionKind.KeywordArguments:
|
||||
METRIC_MATH_KEYWORDS.map((s) =>
|
||||
addSuggestion(s, {
|
||||
insertText: s,
|
||||
command: TRIGGER_SUGGEST,
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
sortText: CompletionItemPriority.MediumHigh,
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case SuggestionKind.Statistic:
|
||||
METRIC_MATH_STATISTIC_KEYWORD_STRINGS.map((s) =>
|
||||
addSuggestion(s, {
|
||||
insertText: `'${s}', `,
|
||||
command: TRIGGER_SUGGEST,
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case SuggestionKind.Operators:
|
||||
METRIC_MATH_OPERATORS.map((s) =>
|
||||
addSuggestion(s, {
|
||||
insertText: `${s} `,
|
||||
command: TRIGGER_SUGGEST,
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case SuggestionKind.Period:
|
||||
METRIC_MATH_PERIODS.map((s, idx) =>
|
||||
addSuggestion(s.toString(), {
|
||||
kind: monaco.languages.CompletionItemKind.Value,
|
||||
sortText: String.fromCharCode(97 + idx), // converts index 0, 1 to "a", "b", etc needed to show the time periods in numerical order
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// always suggest template variables
|
||||
this.templateVariables.map((v) => {
|
||||
addSuggestion(v, {
|
||||
range,
|
||||
label: v,
|
||||
insertText: v,
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
sortText: CompletionItemPriority.Low,
|
||||
});
|
||||
});
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import MonacoMock from '../../__mocks__/monarch/Monaco';
|
||||
import TextModel from '../../__mocks__/monarch/TextModel';
|
||||
import * as MetricMathTestQueries from '../../__mocks__/metric-math-test-data';
|
||||
import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder';
|
||||
import { StatementPosition } from '../../monarch/types';
|
||||
import { getStatementPosition } from './statementPosition';
|
||||
import cloudWatchSqlLanguageDefinition from '../definition';
|
||||
import { MetricMathTokenTypes } from './types';
|
||||
|
||||
describe('statementPosition', () => {
|
||||
function createToken(query: string, position: monacoTypes.IPosition) {
|
||||
const testModel = TextModel(query);
|
||||
return linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
MetricMathTokenTypes
|
||||
);
|
||||
}
|
||||
|
||||
it('returns PredefinedFunction when at the beginning of an empty query', () => {
|
||||
const token = createToken(
|
||||
MetricMathTestQueries.singleLineEmptyQuery.query,
|
||||
MetricMathTestQueries.singleLineEmptyQuery.position
|
||||
);
|
||||
expect(getStatementPosition(token)).toEqual(StatementPosition.PredefinedFunction);
|
||||
});
|
||||
|
||||
it('returns PredefinedFuncSecondArg when in the second arg of a predefined function', () => {
|
||||
const token = createToken(
|
||||
MetricMathTestQueries.secondArgQuery.query,
|
||||
MetricMathTestQueries.secondArgQuery.position
|
||||
);
|
||||
expect(getStatementPosition(token)).toEqual(StatementPosition.PredefinedFuncSecondArg);
|
||||
});
|
||||
|
||||
it('returns SearchFuncSecondArg when in the second arg of a Search function', () => {
|
||||
const token = createToken(
|
||||
MetricMathTestQueries.secondArgAfterSearchQuery.query,
|
||||
MetricMathTestQueries.secondArgAfterSearchQuery.position
|
||||
);
|
||||
expect(getStatementPosition(token)).toEqual(StatementPosition.SearchFuncSecondArg);
|
||||
});
|
||||
|
||||
it('returns SearchFuncThirdArg when in the third arg of a Search function', () => {
|
||||
const token = createToken(
|
||||
MetricMathTestQueries.thirdArgAfterSearchQuery.query,
|
||||
MetricMathTestQueries.thirdArgAfterSearchQuery.position
|
||||
);
|
||||
expect(getStatementPosition(token)).toEqual(StatementPosition.SearchFuncThirdArg);
|
||||
});
|
||||
it('returns AfterFunction when after a function', () => {
|
||||
const token = createToken(
|
||||
MetricMathTestQueries.afterFunctionQuery.query,
|
||||
MetricMathTestQueries.afterFunctionQuery.position
|
||||
);
|
||||
expect(getStatementPosition(token)).toEqual(StatementPosition.AfterFunction);
|
||||
});
|
||||
|
||||
it('returns WithinString when within a string', () => {
|
||||
const token = createToken(
|
||||
MetricMathTestQueries.withinStringQuery.query,
|
||||
MetricMathTestQueries.withinStringQuery.position
|
||||
);
|
||||
expect(getStatementPosition(token)).toEqual(StatementPosition.WithinString);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import { StatementPosition } from '../../monarch/types';
|
||||
import { MetricMathTokenTypes } from './types';
|
||||
|
||||
export function getStatementPosition(currentToken: LinkedToken | null): StatementPosition {
|
||||
const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken();
|
||||
|
||||
if (currentToken && currentToken.isString()) {
|
||||
return StatementPosition.WithinString;
|
||||
}
|
||||
|
||||
if (currentToken && previousNonWhiteSpace) {
|
||||
const currentFunction = currentToken.getPreviousOfType(MetricMathTokenTypes.Function);
|
||||
const isAfterComma = previousNonWhiteSpace.is(MetricMathTokenTypes.Delimiter, ',');
|
||||
const isWithinSearch = currentFunction && currentFunction.value === 'SEARCH';
|
||||
const allTokensAfterStartOfSearch =
|
||||
currentToken.getPreviousUntil(MetricMathTokenTypes.Function, [], 'SEARCH') || [];
|
||||
|
||||
if (isWithinSearch) {
|
||||
// if there's only one ' then we're still within the first arg
|
||||
if (allTokensAfterStartOfSearch.filter(({ value }) => value === "'").length === 1) {
|
||||
return StatementPosition.WithinString;
|
||||
}
|
||||
|
||||
// if there was a , before the last , and it happened after the start of SEARCH
|
||||
const lastComma = previousNonWhiteSpace.getPreviousOfType(MetricMathTokenTypes.Delimiter, ',');
|
||||
if (lastComma) {
|
||||
const lastCommaIsAfterSearch =
|
||||
lastComma.range.startColumn > currentFunction.range.startColumn &&
|
||||
lastComma.range.startLineNumber >= currentFunction.range.startLineNumber;
|
||||
if (lastCommaIsAfterSearch) {
|
||||
return StatementPosition.SearchFuncThirdArg;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise assume it's the second arg
|
||||
return StatementPosition.SearchFuncSecondArg;
|
||||
}
|
||||
|
||||
if (!isWithinSearch && isAfterComma) {
|
||||
return StatementPosition.PredefinedFuncSecondArg;
|
||||
}
|
||||
}
|
||||
|
||||
if (previousNonWhiteSpace?.endsWith(')')) {
|
||||
return StatementPosition.AfterFunction;
|
||||
}
|
||||
|
||||
if (!currentToken || !currentToken.isString()) {
|
||||
return StatementPosition.PredefinedFunction;
|
||||
}
|
||||
|
||||
return StatementPosition.Unknown;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { StatementPosition, SuggestionKind } from '../../monarch/types';
|
||||
|
||||
export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] {
|
||||
switch (statementPosition) {
|
||||
case StatementPosition.PredefinedFunction:
|
||||
return [SuggestionKind.FunctionsWithArguments];
|
||||
case StatementPosition.PredefinedFuncSecondArg:
|
||||
return [SuggestionKind.FunctionsWithArguments, SuggestionKind.KeywordArguments];
|
||||
case StatementPosition.AfterFunction:
|
||||
return [SuggestionKind.Operators];
|
||||
case StatementPosition.SearchFuncSecondArg:
|
||||
return [SuggestionKind.Statistic];
|
||||
case StatementPosition.SearchFuncThirdArg:
|
||||
return [SuggestionKind.Period];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { TokenTypes } from '../../monarch/types';
|
||||
|
||||
export const MetricMathTokenTypes: TokenTypes = {
|
||||
Parenthesis: 'delimiter.parenthesis.cloudwatch-MetricMath',
|
||||
Whitespace: 'white.cloudwatch-MetricMath',
|
||||
Keyword: 'keyword.cloudwatch-MetricMath',
|
||||
Delimiter: 'delimiter.cloudwatch-MetricMath',
|
||||
Operator: 'operator.cloudwatch-MetricMath',
|
||||
Identifier: 'identifier.cloudwatch-MetricMath',
|
||||
Type: 'type.cloudwatch-MetricMath',
|
||||
Function: 'predefined.cloudwatch-MetricMath',
|
||||
Number: 'number.cloudwatch-MetricMath',
|
||||
String: 'string.cloudwatch-MetricMath',
|
||||
Variable: 'variable.cloudwatch-MetricMath',
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LanguageDefinition } from '../monarch/register';
|
||||
|
||||
const cloudWatchMetricMathLanguageDefinition: LanguageDefinition = {
|
||||
id: 'cloudwatch-MetricMath',
|
||||
extensions: [],
|
||||
aliases: [],
|
||||
mimetypes: [],
|
||||
loader: () => import('./language'),
|
||||
};
|
||||
export default cloudWatchMetricMathLanguageDefinition;
|
||||
156
public/app/plugins/datasource/cloudwatch/metric-math/language.ts
Normal file
156
public/app/plugins/datasource/cloudwatch/metric-math/language.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
// Metric Math: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html
|
||||
export const METRIC_MATH_FNS = [
|
||||
'ABS',
|
||||
'ANOMALY_DETECTION_BAND',
|
||||
'AVG',
|
||||
'CEIL',
|
||||
'DATAPOINT_COUNT',
|
||||
'DIFF',
|
||||
'DIFF_TIME',
|
||||
'FILL',
|
||||
'FIRST',
|
||||
'LAST',
|
||||
'FLOOR',
|
||||
'IF',
|
||||
'INSIGHT_RULE_METRIC',
|
||||
'LOG',
|
||||
'LOG10',
|
||||
'MAX',
|
||||
'METRIC_COUNT',
|
||||
'METRICS',
|
||||
'MIN',
|
||||
'MINUTE',
|
||||
'HOUR',
|
||||
'DAY',
|
||||
'DATE',
|
||||
'MONTH',
|
||||
'YEAR',
|
||||
'EPOCH',
|
||||
'PERIOD',
|
||||
'RATE',
|
||||
'REMOVE_EMPTY',
|
||||
'RUNNING_SUM',
|
||||
'SEARCH',
|
||||
'SERVICE_QUOTA',
|
||||
'SLICE',
|
||||
'SORT',
|
||||
'STDDEV',
|
||||
'SUM',
|
||||
'TIME_SERIES',
|
||||
];
|
||||
|
||||
export const METRIC_MATH_STATISTIC_KEYWORD_STRINGS = ['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount']; // second arguments to SEARCH function
|
||||
|
||||
export const METRIC_MATH_KEYWORDS = ['REPEAT', 'LINEAR', 'ASC', 'DSC']; // standalone magic arguments to functions
|
||||
|
||||
export const METRIC_MATH_OPERATORS = [
|
||||
'+',
|
||||
'-',
|
||||
'*',
|
||||
'/',
|
||||
'^',
|
||||
'==',
|
||||
'!=',
|
||||
'<=',
|
||||
'>=',
|
||||
'<',
|
||||
'>',
|
||||
'AND',
|
||||
'&&',
|
||||
'OR',
|
||||
'||',
|
||||
];
|
||||
|
||||
export const METRIC_MATH_PERIODS = [10, 60, 300, 900, 3000, 21600, 86400];
|
||||
|
||||
export const language: monacoType.languages.IMonarchLanguage = {
|
||||
id: 'metricMath',
|
||||
ignoreCase: false,
|
||||
brackets: [
|
||||
{ open: '[', close: ']', token: 'delimiter.square' },
|
||||
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
|
||||
{ open: '{', close: '}', token: 'delimiter.curly' },
|
||||
],
|
||||
tokenizer: {
|
||||
root: [{ include: '@nonNestableStates' }, { include: '@strings' }],
|
||||
nonNestableStates: [
|
||||
{ include: '@variables' },
|
||||
{ include: '@whitespace' },
|
||||
{ include: '@numbers' },
|
||||
{ include: '@assignment' },
|
||||
{ include: '@keywords' },
|
||||
{ include: '@operators' },
|
||||
{ include: '@builtInFunctions' },
|
||||
[/[;,.]/, 'delimiter'],
|
||||
[/[(){}\[\]]/, '@brackets'], // [], (), {} are all brackets
|
||||
],
|
||||
keywords: [[METRIC_MATH_KEYWORDS.map(escapeRegExp).join('|'), 'keyword']],
|
||||
operators: [[METRIC_MATH_OPERATORS.map(escapeRegExp).join('|'), 'operator']],
|
||||
builtInFunctions: [[METRIC_MATH_FNS.map(escapeRegExp).join('|'), 'predefined']],
|
||||
variables: [
|
||||
[/\$[a-zA-Z0-9-_]+/, 'variable'], // $ followed by any letter/number we assume could be grafana template variable
|
||||
],
|
||||
whitespace: [[/\s+/, 'white']],
|
||||
assignment: [[/=/, 'tag']],
|
||||
numbers: [
|
||||
[/0[xX][0-9a-fA-F]*/, 'number'],
|
||||
[/[$][+-]*\d*(\.\d*)?/, 'number'],
|
||||
[/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'],
|
||||
],
|
||||
// states that start other states (aka nested states):
|
||||
strings: [
|
||||
[/'/, { token: 'string', next: '@string' }],
|
||||
[/"/, { token: 'type', next: '@string_double' }],
|
||||
],
|
||||
string: [
|
||||
[/{/, { token: 'delimiter.curly', next: '@nestedCurly' }], // escape out of string and into nestedCurly
|
||||
[/\(/, { token: 'delimiter.parenthesis', next: '@nestedParens' }], // escape out of string and into nestedCurly
|
||||
[/"/, { token: 'type', next: '@string_double' }], // jump into double string
|
||||
[/'/, { token: 'string', next: '@pop' }], // stop being a string
|
||||
{ include: '@nonNestableStates' },
|
||||
[/[^']/, 'string'], // anything that is not a quote, is marked as string
|
||||
],
|
||||
string_double: [
|
||||
[/[^"]/, 'type'], // mark anything not a quote as a "type" (different type of string for visual difference)
|
||||
[/"/, { token: 'type', next: '@pop' }], // mark also as a type and stop being in the double string state
|
||||
],
|
||||
nestedCurly: [
|
||||
[/}/, { token: 'delimiter.curly', next: '@pop' }], // escape out of string and into braces
|
||||
[/'/, { token: 'string', next: '@string' }], // go to string if see start of string
|
||||
[/"/, { token: 'type', next: '@string_double' }], // go to string_double if see start of double string
|
||||
],
|
||||
nestedParens: [
|
||||
[/\)/, { token: 'delimiter.parenthesis', next: '@pop' }], // escape out of string and into braces
|
||||
[/'/, { token: 'string', next: '@string' }], // go to string if see start of string
|
||||
[/"/, { token: 'type', next: '@string_double' }], // go to string_double if see start of double string
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const conf: monacoType.languages.LanguageConfiguration = {
|
||||
brackets: [
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')'],
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" },
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" },
|
||||
],
|
||||
};
|
||||
|
||||
function escapeRegExp(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||
|
||||
import { LinkedToken } from './LinkedToken';
|
||||
import { LanguageDefinition } from './register';
|
||||
import { StatementPosition, SuggestionKind, TokenTypes } from './types';
|
||||
|
||||
type CompletionItem = monacoTypes.languages.CompletionItem;
|
||||
|
||||
/*
|
||||
CompletionItemProvider is an extendable class which needs to implement :
|
||||
- tokenTypes
|
||||
- getStatementPosition
|
||||
- getSuggestionKinds
|
||||
- getSuggestions
|
||||
*/
|
||||
export class CompletionItemProvider {
|
||||
templateVariables: string[];
|
||||
datasource: CloudWatchDatasource;
|
||||
templateSrv: TemplateSrv;
|
||||
tokenTypes: TokenTypes;
|
||||
|
||||
constructor(datasource: CloudWatchDatasource, templateSrv: TemplateSrv = getTemplateSrv()) {
|
||||
this.datasource = datasource;
|
||||
this.templateSrv = templateSrv;
|
||||
this.templateVariables = this.datasource.getVariables();
|
||||
this.templateSrv = templateSrv;
|
||||
|
||||
// implement with more specific tokens when extending this class
|
||||
this.tokenTypes = {
|
||||
Parenthesis: 'delimiter.parenthesis',
|
||||
Whitespace: 'white',
|
||||
Keyword: 'keyword',
|
||||
Delimiter: 'delimiter',
|
||||
Operator: 'operator',
|
||||
Identifier: 'identifier',
|
||||
Type: 'type',
|
||||
Function: 'predefined',
|
||||
Number: 'number',
|
||||
String: 'string',
|
||||
Variable: 'variable',
|
||||
};
|
||||
}
|
||||
|
||||
// implemented by subclasses, given a token, returns a lexical position in a query
|
||||
getStatementPosition(currentToken: LinkedToken | null): StatementPosition {
|
||||
return StatementPosition.Unknown;
|
||||
}
|
||||
|
||||
// implemented by subclasses, given a lexical statement position, returns potential kinds of suggestions
|
||||
getSuggestionKinds(position: StatementPosition): SuggestionKind[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
// implemented by subclasses, given potential suggestions kinds, returns suggestion objects for monaco aka "CompletionItem"
|
||||
getSuggestions(
|
||||
monaco: Monaco,
|
||||
currentToken: LinkedToken | null,
|
||||
suggestionKinds: SuggestionKind[],
|
||||
statementPosition: StatementPosition,
|
||||
position: monacoTypes.IPosition
|
||||
): Promise<CompletionItem[]> {
|
||||
return Promise.reject([]);
|
||||
}
|
||||
|
||||
// called by registerLanguage and passed to monaco with registerCompletionItemProvider
|
||||
// returns an object that implements https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionItemProvider.html
|
||||
getCompletionProvider(monaco: Monaco, languageDefinition: LanguageDefinition) {
|
||||
return {
|
||||
triggerCharacters: [' ', '$', ',', '(', "'"], // one of these characters indicates that it is time to look for a suggestion
|
||||
provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => {
|
||||
const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes);
|
||||
const statementPosition = this.getStatementPosition(currentToken);
|
||||
const suggestionKinds = this.getSuggestionKinds(statementPosition);
|
||||
const suggestions = await this.getSuggestions(
|
||||
monaco,
|
||||
currentToken,
|
||||
suggestionKinds,
|
||||
statementPosition,
|
||||
position
|
||||
);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import { TokenType } from './types';
|
||||
import { TokenTypes } from './types';
|
||||
|
||||
export class LinkedToken {
|
||||
constructor(
|
||||
@@ -7,46 +7,55 @@ export class LinkedToken {
|
||||
public value: string,
|
||||
public range: monacoTypes.IRange,
|
||||
public previous: LinkedToken | null,
|
||||
public next: LinkedToken | null
|
||||
public next: LinkedToken | null,
|
||||
public tokenTypes: TokenTypes
|
||||
) {}
|
||||
|
||||
isKeyword(): boolean {
|
||||
return this.type === TokenType.Keyword;
|
||||
return this.type === this.tokenTypes.Keyword;
|
||||
}
|
||||
|
||||
isWhiteSpace(): boolean {
|
||||
return this.type === TokenType.Whitespace;
|
||||
return this.type === this.tokenTypes.Whitespace;
|
||||
}
|
||||
|
||||
isParenthesis(): boolean {
|
||||
return this.type === TokenType.Parenthesis;
|
||||
return this.type === this.tokenTypes.Parenthesis;
|
||||
}
|
||||
|
||||
isIdentifier(): boolean {
|
||||
return this.type === TokenType.Identifier;
|
||||
return this.type === this.tokenTypes.Identifier;
|
||||
}
|
||||
|
||||
isString(): boolean {
|
||||
return this.type === TokenType.String;
|
||||
return this.type === this.tokenTypes.String;
|
||||
}
|
||||
|
||||
isDoubleQuotedString(): boolean {
|
||||
return this.type === TokenType.Type;
|
||||
return this.type === this.tokenTypes.Type;
|
||||
}
|
||||
|
||||
isVariable(): boolean {
|
||||
return this.type === TokenType.Variable;
|
||||
return this.type === this.tokenTypes.Variable;
|
||||
}
|
||||
|
||||
isFunction(): boolean {
|
||||
return this.type === TokenType.Function;
|
||||
return this.type === this.tokenTypes.Function;
|
||||
}
|
||||
|
||||
is(type: TokenType, value?: string | number | boolean): boolean {
|
||||
isNumber(): boolean {
|
||||
return this.type === this.tokenTypes.Number;
|
||||
}
|
||||
|
||||
is(type: string, value?: string | number | boolean): boolean {
|
||||
const isType = this.type === type;
|
||||
return value !== undefined ? isType && this.value === value : isType;
|
||||
}
|
||||
|
||||
endsWith(value: string | number | boolean): boolean {
|
||||
return this.value === value || this.value[this.value.length - 1] === value;
|
||||
}
|
||||
|
||||
getPreviousNonWhiteSpaceToken(): LinkedToken | null {
|
||||
let curr = this.previous;
|
||||
while (curr != null) {
|
||||
@@ -58,7 +67,7 @@ export class LinkedToken {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPreviousOfType(type: TokenType, value?: string): LinkedToken | null {
|
||||
getPreviousOfType(type: string, value?: string): LinkedToken | null {
|
||||
let curr = this.previous;
|
||||
while (curr != null) {
|
||||
const isType = curr.type === type;
|
||||
@@ -70,7 +79,7 @@ export class LinkedToken {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPreviousUntil(type: TokenType, ignoreTypes: TokenType[], value?: string): LinkedToken[] | null {
|
||||
getPreviousUntil(type: string, ignoreTypes: string[], value?: string): LinkedToken[] | null {
|
||||
let tokens: LinkedToken[] = [];
|
||||
let curr = this.previous;
|
||||
while (curr != null) {
|
||||
@@ -92,7 +101,7 @@ export class LinkedToken {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
getNextUntil(type: TokenType, ignoreTypes: TokenType[], value?: string): LinkedToken[] | null {
|
||||
getNextUntil(type: string, ignoreTypes: string[], value?: string): LinkedToken[] | null {
|
||||
let tokens: LinkedToken[] = [];
|
||||
let curr = this.next;
|
||||
while (curr != null) {
|
||||
@@ -136,7 +145,7 @@ export class LinkedToken {
|
||||
return null;
|
||||
}
|
||||
|
||||
getNextOfType(type: TokenType, value?: string): LinkedToken | null {
|
||||
getNextOfType(type: string, value?: string): LinkedToken | null {
|
||||
let curr = this.next;
|
||||
while (curr != null) {
|
||||
const isType = curr.type === type;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
import MonacoMock from '../__mocks__/monarch/Monaco';
|
||||
import TextModel from '../__mocks__/monarch/TextModel';
|
||||
import { multiLineFullQuery, singleLineFullQuery } from '../__mocks__/cloudwatch-sql-test-data';
|
||||
import { linkedTokenBuilder } from './linkedTokenBuilder';
|
||||
import { DESC, SELECT } from '../cloudwatch-sql/language';
|
||||
import cloudWatchSqlLanguageDefinition from '../cloudwatch-sql/definition';
|
||||
import { SQLTokenTypes } from '../cloudwatch-sql/completion/types';
|
||||
|
||||
describe('linkedTokenBuilder', () => {
|
||||
describe('singleLineFullQuery', () => {
|
||||
const testModel = TextModel(singleLineFullQuery.query);
|
||||
|
||||
it('should add correct references to next LinkedToken', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 };
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
|
||||
expect(current?.is(SQLTokenTypes.Keyword, SELECT)).toBeTruthy();
|
||||
expect(current?.getNextNonWhiteSpaceToken()?.is(SQLTokenTypes.Function, 'AVG')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add correct references to previous LinkedToken', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 1, column: singleLineFullQuery.query.length };
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
expect(current?.is(SQLTokenTypes.Number, '10')).toBeTruthy();
|
||||
expect(current?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, 'LIMIT')).toBeTruthy();
|
||||
expect(
|
||||
current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, DESC)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiLineFullQuery', () => {
|
||||
const testModel = TextModel(multiLineFullQuery.query);
|
||||
|
||||
it('should add LinkedToken with whitespace in case empty lines', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 3, column: 0 };
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
expect(current).not.toBeNull();
|
||||
expect(current?.isWhiteSpace()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add correct references to next LinkedToken', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 };
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
expect(current?.is(SQLTokenTypes.Keyword, SELECT)).toBeTruthy();
|
||||
expect(current?.getNextNonWhiteSpaceToken()?.is(SQLTokenTypes.Function, 'AVG')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add correct references to previous LinkedToken even when references spans over multiple lines', () => {
|
||||
const position: monacoTypes.IPosition = { lineNumber: 6, column: 7 };
|
||||
const current = linkedTokenBuilder(
|
||||
MonacoMock,
|
||||
cloudWatchSqlLanguageDefinition,
|
||||
testModel as monacoTypes.editor.ITextModel,
|
||||
position,
|
||||
SQLTokenTypes
|
||||
);
|
||||
expect(current?.is(SQLTokenTypes.Number, '10')).toBeTruthy();
|
||||
expect(current?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, 'LIMIT')).toBeTruthy();
|
||||
expect(
|
||||
current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(SQLTokenTypes.Keyword, DESC)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { monacoTypes } from '@grafana/ui';
|
||||
|
||||
import language from '../definition';
|
||||
import { LinkedToken } from './LinkedToken';
|
||||
import { Monaco, TokenType } from './types';
|
||||
import { Monaco, TokenTypes } from './types';
|
||||
import { LanguageDefinition } from './register';
|
||||
|
||||
export function linkedTokenBuilder(
|
||||
monaco: Monaco,
|
||||
language: LanguageDefinition,
|
||||
model: monacoTypes.editor.ITextModel,
|
||||
position: monacoTypes.IPosition
|
||||
position: monacoTypes.IPosition,
|
||||
tokenTypes: TokenTypes
|
||||
) {
|
||||
let current: LinkedToken | null = null;
|
||||
let previous: LinkedToken | null = null;
|
||||
@@ -19,7 +21,7 @@ export function linkedTokenBuilder(
|
||||
if (!tokens.length && previous) {
|
||||
const token: monacoTypes.Token = {
|
||||
offset: 0,
|
||||
type: TokenType.Whitespace,
|
||||
type: tokenTypes.Whitespace,
|
||||
language: language.id,
|
||||
_tokenBrand: undefined,
|
||||
};
|
||||
@@ -39,17 +41,18 @@ export function linkedTokenBuilder(
|
||||
};
|
||||
|
||||
const value = model.getValueInRange(range);
|
||||
const sqlToken: LinkedToken = new LinkedToken(token.type, value, range, previous, null);
|
||||
const newToken: LinkedToken = new LinkedToken(token.type, value, range, previous, null, tokenTypes);
|
||||
|
||||
if (monaco.Range.containsPosition(range, position)) {
|
||||
current = sqlToken;
|
||||
current = newToken;
|
||||
}
|
||||
|
||||
if (previous) {
|
||||
previous.next = sqlToken;
|
||||
previous.next = newToken;
|
||||
}
|
||||
previous = sqlToken;
|
||||
previous = newToken;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
34
public/app/plugins/datasource/cloudwatch/monarch/register.ts
Normal file
34
public/app/plugins/datasource/cloudwatch/monarch/register.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Monaco } from '@grafana/ui';
|
||||
import { CompletionItemProvider } from './CompletionItemProvider';
|
||||
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
export type LanguageDefinition = {
|
||||
id: string;
|
||||
extensions: string[];
|
||||
aliases: string[];
|
||||
mimetypes: string[];
|
||||
loader: () => Promise<{
|
||||
language: monacoType.languages.IMonarchLanguage;
|
||||
conf: monacoType.languages.LanguageConfiguration;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const registerLanguage = (
|
||||
monaco: Monaco,
|
||||
language: LanguageDefinition,
|
||||
completionItemProvider: CompletionItemProvider
|
||||
) => {
|
||||
const { id, loader } = language;
|
||||
|
||||
const languages = monaco.languages.getLanguages();
|
||||
if (languages.find((l) => l.id === id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
monaco.languages.register({ id });
|
||||
loader().then((monarch) => {
|
||||
monaco.languages.setMonarchTokensProvider(id, monarch.language);
|
||||
monaco.languages.setLanguageConfiguration(id, monarch.conf);
|
||||
monaco.languages.registerCompletionItemProvider(id, completionItemProvider.getCompletionProvider(monaco, language));
|
||||
});
|
||||
};
|
||||
100
public/app/plugins/datasource/cloudwatch/monarch/types.ts
Normal file
100
public/app/plugins/datasource/cloudwatch/monarch/types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
export interface TokenTypes {
|
||||
Parenthesis: string;
|
||||
Whitespace: string;
|
||||
Keyword: string;
|
||||
Delimiter: string;
|
||||
Operator: string;
|
||||
Identifier: string;
|
||||
Type: string;
|
||||
Function: string;
|
||||
Number: string;
|
||||
String: string;
|
||||
Variable: string;
|
||||
}
|
||||
|
||||
export enum StatementPosition {
|
||||
Unknown,
|
||||
// sql
|
||||
SelectKeyword,
|
||||
AfterSelectKeyword,
|
||||
AfterSelectFuncFirstArgument,
|
||||
AfterFromKeyword,
|
||||
SchemaFuncFirstArgument,
|
||||
SchemaFuncExtraArgument,
|
||||
FromKeyword,
|
||||
AfterFrom,
|
||||
WhereKey,
|
||||
WhereComparisonOperator,
|
||||
WhereValue,
|
||||
AfterWhereValue,
|
||||
AfterGroupByKeywords,
|
||||
AfterGroupBy,
|
||||
AfterOrderByKeywords,
|
||||
AfterOrderByFunction,
|
||||
AfterOrderByDirection,
|
||||
// metric math
|
||||
PredefinedFunction,
|
||||
SearchFuncSecondArg,
|
||||
SearchFuncThirdArg,
|
||||
PredefinedFuncSecondArg,
|
||||
AfterFunction,
|
||||
WithinString,
|
||||
}
|
||||
|
||||
export enum SuggestionKind {
|
||||
SelectKeyword,
|
||||
FunctionsWithArguments,
|
||||
Metrics,
|
||||
FromKeyword,
|
||||
SchemaKeyword,
|
||||
Namespaces,
|
||||
LabelKeys,
|
||||
WhereKeyword,
|
||||
GroupByKeywords,
|
||||
OrderByKeywords,
|
||||
FunctionsWithoutArguments,
|
||||
LimitKeyword,
|
||||
SortOrderDirectionKeyword,
|
||||
ComparisonOperators,
|
||||
LabelValues,
|
||||
LogicalOperators,
|
||||
|
||||
// metricmath,
|
||||
KeywordArguments,
|
||||
Operators,
|
||||
Statistic,
|
||||
Period,
|
||||
}
|
||||
|
||||
export enum CompletionItemPriority {
|
||||
High = 'a',
|
||||
MediumHigh = 'd',
|
||||
Medium = 'g',
|
||||
MediumLow = 'k',
|
||||
Low = 'q',
|
||||
}
|
||||
|
||||
export interface Editor {
|
||||
tokenize: (value: string, languageId: string) => monacoTypes.Token[][];
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => boolean;
|
||||
fromPositions: (start: monacoTypes.IPosition, end?: monacoTypes.IPosition) => monacoTypes.Range;
|
||||
}
|
||||
|
||||
export interface Languages {
|
||||
CompletionItemInsertTextRule: {
|
||||
InsertAsSnippet: 4;
|
||||
};
|
||||
CompletionItemKind: {
|
||||
Function: 1;
|
||||
};
|
||||
}
|
||||
export interface Monaco {
|
||||
editor: Editor;
|
||||
Range: Range;
|
||||
languages: Languages;
|
||||
}
|
||||
Reference in New Issue
Block a user