mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloudwatch Logs: Add autosuggest to logs query editor (#71800)
* Cloudwatch: Add autosuggest to logs query editor * move getSuggestion to LogsCompletionItemProvider * remove debug stuff * add tests * fix lint
This commit is contained in:
parent
4ece133fce
commit
44a1f10b10
@ -0,0 +1,17 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { LogsTokenTypes } from '../../language/logs/completion/types';
|
||||
import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition';
|
||||
|
||||
export const filterQuery = {
|
||||
query: `filter logGroup `,
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: LogsTokenTypes.Keyword, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 6, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 7, type: LogsTokenTypes.Identifier, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 15, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: { lineNumber: 1, column: 16 },
|
||||
};
|
@ -3,3 +3,6 @@ export { whitespaceOnlyQuery } from './whitespaceQuery';
|
||||
export { commentOnlyQuery } from './commentOnlyQuery';
|
||||
export { singleLineFullQuery } from './singleLineFullQuery';
|
||||
export { multiLineFullQuery } from './multiLineFullQuery';
|
||||
export { filterQuery } from './filterQuery';
|
||||
export { newCommandQuery } from './newCommandQuery';
|
||||
export { sortQuery } from './sortQuery';
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { LogsTokenTypes } from '../../language/logs/completion/types';
|
||||
import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition';
|
||||
|
||||
export const newCommandQuery = {
|
||||
query: `fields @timestamp | `,
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: LogsTokenTypes.Keyword, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 6, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 7, type: LogsTokenTypes.Identifier, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 17, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 18, type: LogsTokenTypes.Delimiter, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 19, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: { lineNumber: 1, column: 21 },
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { LogsTokenTypes } from '../../language/logs/completion/types';
|
||||
import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition';
|
||||
|
||||
export const sortQuery = {
|
||||
query: `fields @timestamp | sort `,
|
||||
tokens: [
|
||||
[
|
||||
{ offset: 0, type: LogsTokenTypes.Keyword, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 6, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 7, type: LogsTokenTypes.Identifier, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 17, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 18, type: LogsTokenTypes.Delimiter, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 19, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 20, type: LogsTokenTypes.Keyword, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
{ offset: 24, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID },
|
||||
],
|
||||
] as monacoTypes.Token[][],
|
||||
position: { lineNumber: 1, column: 26 },
|
||||
};
|
@ -47,6 +47,9 @@ const MonacoMock: Monaco = {
|
||||
[CloudwatchLogsTestData.commentOnlyQuery.query]: CloudwatchLogsTestData.commentOnlyQuery.tokens,
|
||||
[CloudwatchLogsTestData.singleLineFullQuery.query]: CloudwatchLogsTestData.singleLineFullQuery.tokens,
|
||||
[CloudwatchLogsTestData.multiLineFullQuery.query]: CloudwatchLogsTestData.multiLineFullQuery.tokens,
|
||||
[CloudwatchLogsTestData.filterQuery.query]: CloudwatchLogsTestData.filterQuery.tokens,
|
||||
[CloudwatchLogsTestData.newCommandQuery.query]: CloudwatchLogsTestData.newCommandQuery.tokens,
|
||||
[CloudwatchLogsTestData.sortQuery.query]: CloudwatchLogsTestData.sortQuery.tokens,
|
||||
};
|
||||
return TestData[value];
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { CustomVariableModel } from '@grafana/data';
|
||||
import { Monaco, monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { setupMockedTemplateService, logGroupNamesVariable } from '../../../__mocks__/CloudWatchDataSource';
|
||||
import { emptyQuery, filterQuery, newCommandQuery, sortQuery } from '../../../__mocks__/cloudwatch-logs-test-data';
|
||||
import MonacoMock from '../../../__mocks__/monarch/Monaco';
|
||||
import TextModel from '../../../__mocks__/monarch/TextModel';
|
||||
import { ResourcesAPI } from '../../../resources/ResourcesAPI';
|
||||
import cloudWatchLogsLanguageDefinition from '../definition';
|
||||
import { LOGS_COMMANDS, LOGS_FUNCTION_OPERATORS, SORT_DIRECTION_KEYWORDS } from '../language';
|
||||
|
||||
import { LogsCompletionItemProvider } from './CompletionItemProvider';
|
||||
|
||||
jest.mock('monaco-editor/esm/vs/editor/editor.api', () => ({
|
||||
Token: jest.fn((offset, type, language) => ({ offset, type, language })),
|
||||
}));
|
||||
|
||||
const getSuggestions = async (
|
||||
value: string,
|
||||
position: monacoTypes.IPosition,
|
||||
variables: CustomVariableModel[] = []
|
||||
) => {
|
||||
const setup = new LogsCompletionItemProvider(
|
||||
{
|
||||
getActualRegion: () => 'us-east-2',
|
||||
} as ResourcesAPI,
|
||||
setupMockedTemplateService(variables)
|
||||
);
|
||||
const monaco = MonacoMock as Monaco;
|
||||
const provider = setup.getCompletionProvider(monaco, cloudWatchLogsLanguageDefinition);
|
||||
const { suggestions } = await provider.provideCompletionItems(
|
||||
TextModel(value) as monacoTypes.editor.ITextModel,
|
||||
position
|
||||
);
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
describe('LogsCompletionItemProvider', () => {
|
||||
describe('getSuggestions', () => {
|
||||
it('returns commands for an empty query', async () => {
|
||||
const suggestions = await getSuggestions(emptyQuery.query, emptyQuery.position);
|
||||
const suggestionLabels = suggestions.map((s) => s.label);
|
||||
expect(suggestionLabels).toEqual(expect.arrayContaining(LOGS_COMMANDS));
|
||||
});
|
||||
|
||||
it('returns commands for a query when a new command is started', async () => {
|
||||
const suggestions = await getSuggestions(newCommandQuery.query, newCommandQuery.position);
|
||||
const suggestionLabels = suggestions.map((s) => s.label);
|
||||
expect(suggestionLabels).toEqual(expect.arrayContaining(LOGS_COMMANDS));
|
||||
});
|
||||
|
||||
it('returns sort order directions for the sort keyword', async () => {
|
||||
const suggestions = await getSuggestions(sortQuery.query, sortQuery.position);
|
||||
const suggestionLabels = suggestions.map((s) => s.label);
|
||||
expect(suggestionLabels).toEqual(expect.arrayContaining(SORT_DIRECTION_KEYWORDS));
|
||||
});
|
||||
|
||||
it('returns function suggestions after a command', async () => {
|
||||
const suggestions = await getSuggestions(sortQuery.query, sortQuery.position);
|
||||
const suggestionLabels = suggestions.map((s) => s.label);
|
||||
expect(suggestionLabels).toEqual(expect.arrayContaining(LOGS_FUNCTION_OPERATORS));
|
||||
});
|
||||
|
||||
it('returns `in []` snippet for the `in` keyword', async () => {
|
||||
const suggestions = await getSuggestions(filterQuery.query, filterQuery.position);
|
||||
const suggestionLabels = suggestions.map((s) => s.label);
|
||||
expect(suggestionLabels).toEqual(expect.arrayContaining(['in []']));
|
||||
});
|
||||
|
||||
it('returns template variables appended to list of suggestions', async () => {
|
||||
const suggestions = await getSuggestions(newCommandQuery.query, newCommandQuery.position, [
|
||||
logGroupNamesVariable,
|
||||
]);
|
||||
const suggestionLabels = suggestions.map((s) => s.label);
|
||||
const expectedTemplateVariableLabel = `$${logGroupNamesVariable.name}`;
|
||||
const expectedLabels = [...LOGS_COMMANDS, expectedTemplateVariableLabel];
|
||||
expect(suggestionLabels).toEqual(expect.arrayContaining(expectedLabels));
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,100 @@
|
||||
import { getTemplateSrv, type TemplateSrv } from '@grafana/runtime';
|
||||
import { Monaco, monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { type ResourcesAPI } from '../../../resources/ResourcesAPI';
|
||||
import { CompletionItemProvider } from '../../monarch/CompletionItemProvider';
|
||||
import { LinkedToken } from '../../monarch/LinkedToken';
|
||||
import { TRIGGER_SUGGEST } from '../../monarch/commands';
|
||||
import { CompletionItem, CompletionItemPriority, StatementPosition, SuggestionKind } from '../../monarch/types';
|
||||
import { LOGS_COMMANDS, LOGS_FUNCTION_OPERATORS, SORT_DIRECTION_KEYWORDS } from '../language';
|
||||
|
||||
import { getStatementPosition } from './statementPosition';
|
||||
import { getSuggestionKinds } from './suggestionKinds';
|
||||
import { LogsTokenTypes } from './types';
|
||||
|
||||
export class LogsCompletionItemProvider extends CompletionItemProvider {
|
||||
constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) {
|
||||
super(resources, templateSrv);
|
||||
this.getStatementPosition = getStatementPosition;
|
||||
this.getSuggestionKinds = getSuggestionKinds;
|
||||
this.tokenTypes = LogsTokenTypes;
|
||||
}
|
||||
|
||||
async getSuggestions(
|
||||
monaco: Monaco,
|
||||
currentToken: LinkedToken | null,
|
||||
suggestionKinds: SuggestionKind[],
|
||||
statementPosition: StatementPosition,
|
||||
position: monacoTypes.IPosition
|
||||
): Promise<CompletionItem[]> {
|
||||
const suggestions: CompletionItem[] = [];
|
||||
const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis();
|
||||
const range =
|
||||
invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range;
|
||||
|
||||
function toCompletionItem(value: string, rest: Partial<CompletionItem> = {}) {
|
||||
const item: monacoTypes.languages.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.push(toCompletionItem(value, rest));
|
||||
}
|
||||
|
||||
for (const kind of suggestionKinds) {
|
||||
switch (kind) {
|
||||
case SuggestionKind.Command:
|
||||
LOGS_COMMANDS.forEach((command) => {
|
||||
addSuggestion(command, {
|
||||
insertText: `${command} $0`,
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
command: TRIGGER_SUGGEST,
|
||||
});
|
||||
});
|
||||
break;
|
||||
case SuggestionKind.Function:
|
||||
LOGS_FUNCTION_OPERATORS.forEach((f) => {
|
||||
addSuggestion(f, {
|
||||
insertText: `${f}($0)`,
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
command: TRIGGER_SUGGEST,
|
||||
});
|
||||
});
|
||||
break;
|
||||
case SuggestionKind.SortOrderDirectionKeyword:
|
||||
SORT_DIRECTION_KEYWORDS.forEach((direction) => {
|
||||
addSuggestion(direction, { sortText: CompletionItemPriority.High });
|
||||
});
|
||||
break;
|
||||
case SuggestionKind.InKeyword:
|
||||
addSuggestion('in []', {
|
||||
insertText: 'in ["$0"]',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
sortText: CompletionItemPriority.High,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.templateSrv.getVariables().map((v) => {
|
||||
const variable = `$${v.name}`;
|
||||
addSuggestion(variable, {
|
||||
range,
|
||||
label: variable,
|
||||
insertText: variable,
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
sortText: CompletionItemPriority.Low,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { StatementPosition, SuggestionKind } from '../../monarch/types';
|
||||
|
||||
export function getSuggestionKinds(statementPosition: StatementPosition): SuggestionKind[] {
|
||||
switch (statementPosition) {
|
||||
case StatementPosition.NewCommand:
|
||||
return [SuggestionKind.Command];
|
||||
case StatementPosition.AfterSortKeyword:
|
||||
case StatementPosition.SortArg:
|
||||
return [SuggestionKind.SortOrderDirectionKeyword, SuggestionKind.Function];
|
||||
case StatementPosition.AfterDisplayKeyword:
|
||||
case StatementPosition.AfterFieldsKeyword:
|
||||
case StatementPosition.AfterFilterKeyword:
|
||||
case StatementPosition.AfterStatsKeyword:
|
||||
case StatementPosition.AfterLimitKeyword:
|
||||
case StatementPosition.AfterParseKeyword:
|
||||
case StatementPosition.AfterDedupKeyword:
|
||||
case StatementPosition.CommandArg:
|
||||
case StatementPosition.FunctionArg:
|
||||
case StatementPosition.ArithmeticOperatorArg:
|
||||
case StatementPosition.BooleanOperatorArg:
|
||||
case StatementPosition.ComparisonOperatorArg:
|
||||
return [SuggestionKind.Function];
|
||||
case StatementPosition.FilterArg:
|
||||
return [SuggestionKind.InKeyword, SuggestionKind.Function];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
@ -2,6 +2,8 @@ import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { LanguageDefinition } from './register';
|
||||
|
||||
export type CompletionItem = monacoTypes.languages.CompletionItem;
|
||||
|
||||
export interface TokenTypes {
|
||||
Parenthesis: string;
|
||||
Whitespace: string;
|
||||
@ -113,6 +115,11 @@ export enum SuggestionKind {
|
||||
Operators,
|
||||
Statistic,
|
||||
Period,
|
||||
|
||||
// logs
|
||||
Command,
|
||||
Function,
|
||||
InKeyword,
|
||||
}
|
||||
|
||||
export enum CompletionItemPriority {
|
||||
|
Loading…
Reference in New Issue
Block a user