You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
11 KiB
298 lines
11 KiB
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 {
|
|
BY,
|
|
FROM,
|
|
GROUP,
|
|
LIMIT,
|
|
ORDER,
|
|
SCHEMA,
|
|
SELECT,
|
|
ASC,
|
|
DESC,
|
|
WHERE,
|
|
COMPARISON_OPERATORS,
|
|
LOGICAL_OPERATORS,
|
|
STATISTICS,
|
|
} from '../language';
|
|
import { getMetricNameToken, getNamespaceToken } from './tokenUtils';
|
|
|
|
type CompletionItem = monacoTypes.languages.CompletionItem;
|
|
|
|
export class CompletionItemProvider {
|
|
region: string;
|
|
templateVariables: string[];
|
|
|
|
constructor(private datasource: CloudWatchDatasource, private templateSrv: TemplateSrv = getTemplateSrv()) {
|
|
this.templateVariables = this.datasource.getVariables();
|
|
this.region = datasource.getActualRegion();
|
|
}
|
|
|
|
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(
|
|
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.SelectKeyword:
|
|
addSuggestion(SELECT, {
|
|
insertText: `${SELECT} $0`,
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
command: TRIGGER_SUGGEST,
|
|
});
|
|
break;
|
|
|
|
case SuggestionKind.FunctionsWithArguments:
|
|
STATISTICS.map((s) =>
|
|
addSuggestion(s, {
|
|
insertText: `${s}($0)`,
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
command: TRIGGER_SUGGEST,
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
})
|
|
);
|
|
break;
|
|
|
|
case SuggestionKind.FunctionsWithoutArguments:
|
|
STATISTICS.map((s) =>
|
|
addSuggestion(s, {
|
|
insertText: `${s}() `,
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
command: TRIGGER_SUGGEST,
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
})
|
|
);
|
|
break;
|
|
|
|
case SuggestionKind.Metrics:
|
|
{
|
|
const namespaceToken = getNamespaceToken(currentToken);
|
|
if (namespaceToken?.value) {
|
|
// if a namespace is specified, only suggest metrics for the namespace
|
|
const metrics = await this.datasource.getMetrics(
|
|
this.templateSrv.replace(namespaceToken?.value.replace(/\"/g, '')),
|
|
this.templateSrv.replace(this.region)
|
|
);
|
|
metrics.map((m) => addSuggestion(m.value));
|
|
} else {
|
|
// If no namespace is specified in the query, just list all metrics
|
|
const metrics = await this.datasource.getAllMetrics(this.templateSrv.replace(this.region));
|
|
uniq(metrics.map((m) => m.metricName)).map((m) => addSuggestion(m, { insertText: m }));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SuggestionKind.FromKeyword:
|
|
addSuggestion(FROM, {
|
|
insertText: `${FROM} `,
|
|
command: TRIGGER_SUGGEST,
|
|
});
|
|
break;
|
|
|
|
case SuggestionKind.SchemaKeyword:
|
|
addSuggestion(SCHEMA, {
|
|
sortText: CompletionItemPriority.High,
|
|
insertText: `${SCHEMA}($0)`,
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
command: TRIGGER_SUGGEST,
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
});
|
|
break;
|
|
|
|
case SuggestionKind.Namespaces:
|
|
const metricNameToken = getMetricNameToken(currentToken);
|
|
let namespaces = [];
|
|
if (metricNameToken?.value) {
|
|
// if a metric is specified, only suggest namespaces that actually have that metric
|
|
const metrics = await this.datasource.getAllMetrics(this.region);
|
|
const metricName = this.templateSrv.replace(metricNameToken.value);
|
|
namespaces = metrics.filter((m) => m.metricName === metricName).map((m) => m.namespace);
|
|
} else {
|
|
// if no metric is specified, just suggest all namespaces
|
|
const ns = await this.datasource.getNamespaces();
|
|
namespaces = ns.map((n) => n.value);
|
|
}
|
|
namespaces.map((n) => addSuggestion(`"${n}"`, { insertText: `"${n}"` }));
|
|
break;
|
|
|
|
case SuggestionKind.LabelKeys:
|
|
{
|
|
const metricNameToken = getMetricNameToken(currentToken);
|
|
const namespaceToken = getNamespaceToken(currentToken);
|
|
if (namespaceToken?.value) {
|
|
let dimensionFilter = {};
|
|
let labelKeyTokens;
|
|
if (statementPosition === StatementPosition.SchemaFuncExtraArgument) {
|
|
labelKeyTokens = namespaceToken?.getNextUntil(TokenType.Parenthesis, [
|
|
TokenType.Delimiter,
|
|
TokenType.Whitespace,
|
|
]);
|
|
} else if (statementPosition === StatementPosition.AfterGroupByKeywords) {
|
|
labelKeyTokens = currentToken?.getPreviousUntil(TokenType.Keyword, [
|
|
TokenType.Delimiter,
|
|
TokenType.Whitespace,
|
|
]);
|
|
}
|
|
dimensionFilter = (labelKeyTokens || []).reduce((acc, curr) => {
|
|
return { ...acc, [curr.value]: null };
|
|
}, {});
|
|
const keys = await this.datasource.getDimensionKeys(
|
|
this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
|
|
this.templateSrv.replace(this.region),
|
|
dimensionFilter,
|
|
metricNameToken?.value ?? ''
|
|
);
|
|
keys.map((m) => {
|
|
const key = /[\s\.-]/.test(m.value) ? `"${m.value}"` : m.value;
|
|
addSuggestion(key);
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SuggestionKind.LabelValues:
|
|
{
|
|
const namespaceToken = getNamespaceToken(currentToken);
|
|
const metricNameToken = getMetricNameToken(currentToken);
|
|
const labelKey = currentToken?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken();
|
|
if (namespaceToken?.value && labelKey?.value && metricNameToken?.value) {
|
|
const values = await this.datasource.getDimensionValues(
|
|
this.templateSrv.replace(this.region),
|
|
this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')),
|
|
this.templateSrv.replace(metricNameToken.value),
|
|
this.templateSrv.replace(labelKey.value),
|
|
{}
|
|
);
|
|
values.map((o) =>
|
|
addSuggestion(`'${o.value}'`, { insertText: `'${o.value}' `, command: TRIGGER_SUGGEST })
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SuggestionKind.LogicalOperators:
|
|
LOGICAL_OPERATORS.map((o) =>
|
|
addSuggestion(`${o}`, {
|
|
insertText: `${o} `,
|
|
command: TRIGGER_SUGGEST,
|
|
sortText: CompletionItemPriority.MediumHigh,
|
|
})
|
|
);
|
|
break;
|
|
|
|
case SuggestionKind.WhereKeyword:
|
|
addSuggestion(`${WHERE}`, {
|
|
insertText: `${WHERE} `,
|
|
command: TRIGGER_SUGGEST,
|
|
sortText: CompletionItemPriority.High,
|
|
});
|
|
break;
|
|
|
|
case SuggestionKind.ComparisonOperators:
|
|
COMPARISON_OPERATORS.map((o) => addSuggestion(`${o}`, { insertText: `${o} `, command: TRIGGER_SUGGEST }));
|
|
break;
|
|
|
|
case SuggestionKind.GroupByKeywords:
|
|
addSuggestion(`${GROUP} ${BY}`, {
|
|
insertText: `${GROUP} ${BY} `,
|
|
command: TRIGGER_SUGGEST,
|
|
sortText: CompletionItemPriority.MediumHigh,
|
|
});
|
|
break;
|
|
|
|
case SuggestionKind.OrderByKeywords:
|
|
addSuggestion(`${ORDER} ${BY}`, {
|
|
insertText: `${ORDER} ${BY} `,
|
|
command: TRIGGER_SUGGEST,
|
|
sortText: CompletionItemPriority.Medium,
|
|
});
|
|
break;
|
|
|
|
case SuggestionKind.LimitKeyword:
|
|
addSuggestion(LIMIT, { insertText: `${LIMIT} `, sortText: CompletionItemPriority.MediumLow });
|
|
break;
|
|
|
|
case SuggestionKind.SortOrderDirectionKeyword:
|
|
[ASC, DESC].map((s) =>
|
|
addSuggestion(s, {
|
|
insertText: `${s} `,
|
|
command: TRIGGER_SUGGEST,
|
|
})
|
|
);
|
|
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;
|
|
}
|
|
}
|
|
|