mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Update Slate to 0.47.8 (#19197)
* Chore: Update Slate to 0.47.8 Closes #17430
This commit is contained in:
@@ -2,20 +2,22 @@ import _ from 'lodash';
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import Cascader from 'rc-cascader';
|
||||
// @ts-ignore
|
||||
import PluginPrism from 'slate-prism';
|
||||
// @ts-ignore
|
||||
|
||||
import { SlatePrism } from '@grafana/ui';
|
||||
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||
// dom also includes Element polyfills
|
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
import QueryField, { TypeaheadInput } from 'app/features/explore/QueryField';
|
||||
import { PromQuery, PromContext, PromOptions } from '../types';
|
||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
||||
import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, DOMUtil } from '@grafana/ui';
|
||||
import { isDataFrame, toLegacyResponseData } from '@grafana/data';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import PromQlLanguageProvider from '../language_provider';
|
||||
import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const METRIC_MARK = 'metric';
|
||||
@@ -67,7 +69,7 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
|
||||
return [...options, ...metricsOptions];
|
||||
}
|
||||
|
||||
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
|
||||
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string {
|
||||
// Modify suggestion based on context
|
||||
switch (typeaheadContext) {
|
||||
case 'context-labels': {
|
||||
@@ -102,7 +104,7 @@ interface CascaderOption {
|
||||
}
|
||||
|
||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
|
||||
history: HistoryItem[];
|
||||
history: Array<HistoryItem<PromQuery>>;
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
@@ -113,7 +115,7 @@ interface PromQueryFieldState {
|
||||
|
||||
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
|
||||
plugins: any[];
|
||||
languageProvider: any;
|
||||
languageProvider: PromQlLanguageProvider;
|
||||
languageProviderInitializationPromise: CancelablePromise<any>;
|
||||
|
||||
constructor(props: PromQueryFieldProps, context: React.Context<any>) {
|
||||
@@ -125,7 +127,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
PluginPrism({
|
||||
SlatePrism({
|
||||
onlyIn: (node: any) => node.type === 'code_block',
|
||||
getSyntax: (node: any) => 'promql',
|
||||
}),
|
||||
@@ -250,7 +252,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
return;
|
||||
}
|
||||
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.syntax;
|
||||
Prism.languages[PRISM_SYNTAX][METRIC_MARK] = {
|
||||
alias: 'variable',
|
||||
pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
|
||||
@@ -270,26 +272,20 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
this.setState({ metricsOptions, syntaxLoaded: true });
|
||||
};
|
||||
|
||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
|
||||
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
if (!this.languageProvider) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
const { history } = this.props;
|
||||
const { prefix, text, value, wrapperNode } = typeahead;
|
||||
const { prefix, text, value, wrapperClasses, labelKey } = typeahead;
|
||||
|
||||
// Get DOM-dependent context
|
||||
const wrapperClasses = Array.from(wrapperNode.classList);
|
||||
const labelKeyNode = DOMUtil.getPreviousCousin(wrapperNode, '.attr-name');
|
||||
const labelKey = labelKeyNode && labelKeyNode.textContent;
|
||||
const nextChar = DOMUtil.getNextCharacter();
|
||||
|
||||
const result = this.languageProvider.provideCompletionItems(
|
||||
const result = await this.languageProvider.provideCompletionItems(
|
||||
{ text, value, prefix, wrapperClasses, labelKey },
|
||||
{ history }
|
||||
);
|
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
||||
// console.log('handleTypeahead', wrapperClasses, text, prefix, labelKey, result.context);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
import {
|
||||
CompletionItem,
|
||||
CompletionItemGroup,
|
||||
LanguageProvider,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
HistoryItem,
|
||||
} from 'app/types/explore';
|
||||
|
||||
import { parseSelector, processLabels, processHistogramLabels } from './language_utils';
|
||||
import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql';
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import { PromQuery } from './types';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'instance'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
const HISTORY_ITEM_COUNT = 5;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
|
||||
const wrapLabel = (label: string) => ({ label });
|
||||
const wrapLabel = (label: string): CompletionItem => ({ label });
|
||||
|
||||
const setFunctionKind = (suggestion: CompletionItem): CompletionItem => {
|
||||
suggestion.kind = 'function';
|
||||
@@ -30,10 +35,12 @@ export function addHistoryMetadata(item: CompletionItem, history: any[]): Comple
|
||||
const count = historyForItem.length;
|
||||
const recent = historyForItem[0];
|
||||
let hint = `Queried ${count} times in the last 24h.`;
|
||||
|
||||
if (recent) {
|
||||
const lastQueried = dateTime(recent.ts).fromNow();
|
||||
hint = `${hint} Last queried ${lastQueried}.`;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
documentation: hint,
|
||||
@@ -47,8 +54,9 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
metrics?: string[];
|
||||
startTask: Promise<any>;
|
||||
datasource: PrometheusDatasource;
|
||||
|
||||
constructor(datasource: any, initialValues?: any) {
|
||||
constructor(datasource: PrometheusDatasource, initialValues?: any) {
|
||||
super();
|
||||
|
||||
this.datasource = datasource;
|
||||
@@ -60,10 +68,11 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
}
|
||||
|
||||
// Strip syntax chars
|
||||
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||
|
||||
getSyntax() {
|
||||
get syntax() {
|
||||
return PromqlSyntax;
|
||||
}
|
||||
|
||||
@@ -106,39 +115,46 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput {
|
||||
provideCompletionItems = async (
|
||||
{ prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput,
|
||||
context: { history: Array<HistoryItem<PromQuery>> } = { history: [] }
|
||||
): Promise<TypeaheadOutput> => {
|
||||
// Local text properties
|
||||
const empty = value.document.text.length === 0;
|
||||
const selectedLines = value.document.getTextsAtRangeAsArray(value.selection);
|
||||
const currentLine = selectedLines.length === 1 ? selectedLines[0] : null;
|
||||
const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null;
|
||||
const selectedLines = value.document.getTextsAtRange(value.selection);
|
||||
const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null;
|
||||
|
||||
const nextCharacter = currentLine ? currentLine[value.selection.anchor.offset] : null;
|
||||
|
||||
// Syntax spans have 3 classes by default. More indicate a recognized token
|
||||
const tokenRecognized = wrapperClasses.length > 3;
|
||||
// Non-empty prefix, but not inside known token
|
||||
const prefixUnrecognized = prefix && !tokenRecognized;
|
||||
|
||||
// Prevent suggestions in `function(|suffix)`
|
||||
const noSuffix = !nextCharacter || nextCharacter === ')';
|
||||
// Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it
|
||||
|
||||
// Empty prefix is safe if it does not immediately follow a complete expression and has no text after it
|
||||
const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix;
|
||||
|
||||
// About to type next operand if preceded by binary operator
|
||||
const isNextOperand = text.match(/[+\-*/^%]/);
|
||||
const operatorsPattern = /[+\-*/^%]/;
|
||||
const isNextOperand = text.match(operatorsPattern);
|
||||
|
||||
// Determine candidates by CSS context
|
||||
if (_.includes(wrapperClasses, 'context-range')) {
|
||||
if (wrapperClasses.includes('context-range')) {
|
||||
// Suggestions for metric[|]
|
||||
return this.getRangeCompletionItems();
|
||||
} else if (_.includes(wrapperClasses, 'context-labels')) {
|
||||
} else if (wrapperClasses.includes('context-labels')) {
|
||||
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
|
||||
return this.getLabelCompletionItems.apply(this, arguments);
|
||||
} else if (_.includes(wrapperClasses, 'context-aggregation')) {
|
||||
return this.getLabelCompletionItems({ prefix, text, value, labelKey, wrapperClasses });
|
||||
} else if (wrapperClasses.includes('context-aggregation')) {
|
||||
// Suggestions for sum(metric) by (|)
|
||||
return this.getAggregationCompletionItems.apply(this, arguments);
|
||||
return this.getAggregationCompletionItems({ prefix, text, value, labelKey, wrapperClasses });
|
||||
} else if (empty) {
|
||||
// Suggestions for empty query field
|
||||
return this.getEmptyCompletionItems(context || {});
|
||||
} else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) {
|
||||
return this.getEmptyCompletionItems(context);
|
||||
} else if ((prefixUnrecognized && noSuffix) || safeEmptyPrefix || isNextOperand) {
|
||||
// Show term suggestions in a couple of scenarios
|
||||
return this.getTermCompletionItems();
|
||||
}
|
||||
@@ -146,20 +162,20 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
return {
|
||||
suggestions: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getEmptyCompletionItems(context: any): TypeaheadOutput {
|
||||
getEmptyCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => {
|
||||
const { history } = context;
|
||||
let suggestions: CompletionItemGroup[] = [];
|
||||
const suggestions = [];
|
||||
|
||||
if (history && history.length > 0) {
|
||||
if (history && history.length) {
|
||||
const historyItems = _.chain(history)
|
||||
.map((h: any) => h.query.expr)
|
||||
.map(h => h.query.expr)
|
||||
.filter()
|
||||
.uniq()
|
||||
.take(HISTORY_ITEM_COUNT)
|
||||
.map(wrapLabel)
|
||||
.map((item: CompletionItem) => addHistoryMetadata(item, history))
|
||||
.map(item => addHistoryMetadata(item, history))
|
||||
.value();
|
||||
|
||||
suggestions.push({
|
||||
@@ -171,14 +187,14 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
|
||||
const termCompletionItems = this.getTermCompletionItems();
|
||||
suggestions = [...suggestions, ...termCompletionItems.suggestions];
|
||||
suggestions.push(...termCompletionItems.suggestions);
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
};
|
||||
|
||||
getTermCompletionItems(): TypeaheadOutput {
|
||||
getTermCompletionItems = (): TypeaheadOutput => {
|
||||
const { metrics } = this;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
const suggestions = [];
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
@@ -186,14 +202,15 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
items: FUNCTIONS.map(setFunctionKind),
|
||||
});
|
||||
|
||||
if (metrics && metrics.length > 0) {
|
||||
if (metrics && metrics.length) {
|
||||
suggestions.push({
|
||||
label: 'Metrics',
|
||||
items: metrics.map(wrapLabel),
|
||||
});
|
||||
}
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
};
|
||||
|
||||
getRangeCompletionItems(): TypeaheadOutput {
|
||||
return {
|
||||
@@ -219,21 +236,21 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
);
|
||||
}
|
||||
|
||||
getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
|
||||
getAggregationCompletionItems = ({ value }: TypeaheadInput): TypeaheadOutput => {
|
||||
const refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
// Stitch all query lines together to support multi-line queries
|
||||
let queryOffset;
|
||||
const queryText = value.document.getBlocks().reduce((text: string, block: any) => {
|
||||
const queryText = value.document.getBlocks().reduce((text: string, block) => {
|
||||
const blockText = block.getText();
|
||||
if (value.anchorBlock.key === block.key) {
|
||||
// Newline characters are not accounted for but this is irrelevant
|
||||
// for the purpose of extracting the selector string
|
||||
queryOffset = value.anchorOffset + text.length;
|
||||
queryOffset = value.selection.anchor.offset + text.length;
|
||||
}
|
||||
text += blockText;
|
||||
return text;
|
||||
|
||||
return text + blockText;
|
||||
}, '');
|
||||
|
||||
// Try search for selector part on the left-hand side, such as `sum (m) by (l)`
|
||||
@@ -259,10 +276,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
let selectorString = queryText.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
||||
|
||||
// Range vector syntax not accounted for by subsequent parse so discard it if present
|
||||
selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
|
||||
const selectorString = queryText
|
||||
.slice(openParensSelectorIndex + 1, closeParensSelectorIndex)
|
||||
.replace(/\[[^\]]+\]$/, '');
|
||||
|
||||
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
|
||||
|
||||
@@ -274,14 +291,16 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||
let context: string;
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
getLabelCompletionItems = async ({
|
||||
text,
|
||||
wrapperClasses,
|
||||
labelKey,
|
||||
value,
|
||||
}: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
const cursorOffset = value.selection.anchor.offset;
|
||||
|
||||
// Get normalized selector
|
||||
let selector;
|
||||
@@ -292,10 +311,23 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
} catch {
|
||||
selector = EMPTY_SELECTOR;
|
||||
}
|
||||
const containsMetric = selector.indexOf('__name__=') > -1;
|
||||
|
||||
const containsMetric = selector.includes('__name__=');
|
||||
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
||||
|
||||
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
|
||||
// Query labels for selector
|
||||
if (selector && (!this.labelValues[selector] || this.timeRangeChanged())) {
|
||||
if (selector === EMPTY_SELECTOR) {
|
||||
// Query label values for default labels
|
||||
await Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
|
||||
} else {
|
||||
await this.fetchSeriesLabels(selector, !containsMetric);
|
||||
}
|
||||
}
|
||||
|
||||
const suggestions = [];
|
||||
let context: string;
|
||||
if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) {
|
||||
// Label values
|
||||
if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) {
|
||||
const labelValues = this.labelValues[selector][labelKey];
|
||||
@@ -308,27 +340,20 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
} else {
|
||||
// Label keys
|
||||
const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
|
||||
|
||||
if (labelKeys) {
|
||||
const possibleKeys = _.difference(labelKeys, existingKeys);
|
||||
if (possibleKeys.length > 0) {
|
||||
if (possibleKeys.length) {
|
||||
context = 'context-labels';
|
||||
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
||||
const newItems = possibleKeys.map(key => ({ label: key }));
|
||||
const newSuggestion: CompletionItemGroup = { label: `Labels`, items: newItems };
|
||||
suggestions.push(newSuggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Query labels for selector
|
||||
if (selector && (!this.labelValues[selector] || this.timeRangeChanged())) {
|
||||
if (selector === EMPTY_SELECTOR) {
|
||||
// Query label values for default labels
|
||||
refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
|
||||
} else {
|
||||
refresher = this.fetchSeriesLabels(selector, !containsMetric);
|
||||
}
|
||||
}
|
||||
|
||||
return { context, refresher, suggestions };
|
||||
}
|
||||
return { context, suggestions };
|
||||
};
|
||||
|
||||
fetchLabelValues = async (key: string) => {
|
||||
try {
|
||||
|
||||
@@ -16,13 +16,13 @@ export const processHistogramLabels = (labels: string[]) => {
|
||||
return { values: { __name__: result } };
|
||||
};
|
||||
|
||||
export function processLabels(labels: any, withName = false) {
|
||||
export function processLabels(labels: Array<{ [key: string]: string }>, withName = false) {
|
||||
const values: { [key: string]: string[] } = {};
|
||||
labels.forEach((l: any) => {
|
||||
labels.forEach(l => {
|
||||
const { __name__, ...rest } = l;
|
||||
if (withName) {
|
||||
values['__name__'] = values['__name__'] || [];
|
||||
if (values['__name__'].indexOf(__name__) === -1) {
|
||||
if (!values['__name__'].includes(__name__)) {
|
||||
values['__name__'].push(__name__);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function processLabels(labels: any, withName = false) {
|
||||
if (!values[key]) {
|
||||
values[key] = [];
|
||||
}
|
||||
if (values[key].indexOf(rest[key]) === -1) {
|
||||
if (!values[key].includes(rest[key])) {
|
||||
values[key].push(rest[key]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
// @ts-ignore
|
||||
import Plain from 'slate-plain-serializer';
|
||||
|
||||
import { Editor as SlateEditor } from 'slate';
|
||||
import LanguageProvider from '../language_provider';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { HistoryItem } from 'app/types';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
describe('Language completion provider', () => {
|
||||
const datasource = {
|
||||
const datasource: PrometheusDatasource = ({
|
||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||
getTimeRange: () => ({ start: 0, end: 1 }),
|
||||
};
|
||||
} as any) as PrometheusDatasource;
|
||||
|
||||
describe('empty query suggestions', () => {
|
||||
it('returns default suggestions on emtpty context', () => {
|
||||
it('returns default suggestions on empty context', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('');
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
@@ -23,12 +24,11 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns default suggestions with metrics on emtpty context when metrics were provided', () => {
|
||||
it('returns default suggestions with metrics on empty context when metrics were provided', async () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const value = Plain.deserialize('');
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
@@ -39,17 +39,21 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns default suggestions with history on emtpty context when history was provided', () => {
|
||||
it('returns default suggestions with history on empty context when history was provided', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('');
|
||||
const history = [
|
||||
const history: Array<HistoryItem<PromQuery>> = [
|
||||
{
|
||||
ts: 0,
|
||||
query: { refId: '1', expr: 'metric' },
|
||||
},
|
||||
];
|
||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history });
|
||||
const result = await instance.provideCompletionItems(
|
||||
{ text: '', prefix: '', value, wrapperClasses: [] },
|
||||
{ history }
|
||||
);
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'History',
|
||||
@@ -67,17 +71,16 @@ describe('Language completion provider', () => {
|
||||
});
|
||||
|
||||
describe('range suggestions', () => {
|
||||
it('returns range suggestions in range context', () => {
|
||||
it('returns range suggestions in range context', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('1');
|
||||
const result = instance.provideCompletionItems({
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '1',
|
||||
prefix: '1',
|
||||
value,
|
||||
wrapperClasses: ['context-range'],
|
||||
});
|
||||
expect(result.context).toBe('context-range');
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
items: [
|
||||
@@ -96,12 +99,12 @@ describe('Language completion provider', () => {
|
||||
});
|
||||
|
||||
describe('metric suggestions', () => {
|
||||
it('returns metrics and function suggestions in an unknown context', () => {
|
||||
it('returns metrics and function suggestions in an unknown context', async () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const value = Plain.deserialize('a');
|
||||
const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] });
|
||||
let value = Plain.deserialize('a');
|
||||
value = value.setSelection({ anchor: { offset: 1 }, focus: { offset: 1 } });
|
||||
const result = await instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
@@ -112,12 +115,11 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns metrics and function suggestions after a binary operator', () => {
|
||||
it('returns metrics and function suggestions after a binary operator', async () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const value = Plain.deserialize('*');
|
||||
const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
|
||||
const result = await instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions).toMatchObject([
|
||||
{
|
||||
label: 'Functions',
|
||||
@@ -128,34 +130,30 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no suggestions at the beginning of a non-empty function', () => {
|
||||
it('returns no suggestions at the beginning of a non-empty function', async () => {
|
||||
const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
|
||||
const value = Plain.deserialize('sum(up)');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 4,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
|
||||
const valueWithSelection = ed.moveForward(4).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
value: valueWithSelection,
|
||||
wrapperClasses: [],
|
||||
});
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeUndefined();
|
||||
expect(result.suggestions.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('label suggestions', () => {
|
||||
it('returns default label suggestions on label context and no metric', () => {
|
||||
it('returns default label suggestions on label context and no metric', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('{}');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 1,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(1).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
@@ -165,14 +163,16 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
|
||||
});
|
||||
|
||||
it('returns label suggestions on label context and metric', () => {
|
||||
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
|
||||
it('returns label suggestions on label context and metric', async () => {
|
||||
const datasources: PrometheusDatasource = ({
|
||||
metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }),
|
||||
getTimeRange: () => ({ start: 0, end: 1 }),
|
||||
} as any) as PrometheusDatasource;
|
||||
const instance = new LanguageProvider(datasources, { labelKeys: { '{__name__="metric"}': ['bar'] } });
|
||||
const value = Plain.deserialize('metric{}');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 7,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(7).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
@@ -182,16 +182,32 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
||||
});
|
||||
|
||||
it('returns label suggestions on label context but leaves out labels that already exist', () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] },
|
||||
it('returns label suggestions on label context but leaves out labels that already exist', async () => {
|
||||
const datasources: PrometheusDatasource = ({
|
||||
metadataRequest: () => ({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
__name__: 'metric',
|
||||
bar: 'asdasd',
|
||||
job1: 'dsadsads',
|
||||
job2: 'fsfsdfds',
|
||||
job3: 'dsadsad',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
getTimeRange: () => ({ start: 0, end: 1 }),
|
||||
} as any) as PrometheusDatasource;
|
||||
const instance = new LanguageProvider(datasources, {
|
||||
labelKeys: {
|
||||
'{job1="foo",job2!="foo",job3=~"foo",__name__="metric"}': ['bar', 'job1', 'job2', 'job3', '__name__'],
|
||||
},
|
||||
});
|
||||
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 36,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}');
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(54).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
@@ -201,15 +217,15 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
||||
});
|
||||
|
||||
it('returns label value suggestions inside a label value context after a negated matching operator', () => {
|
||||
it('returns label value suggestions inside a label value context after a negated matching operator', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{}': ['label'] },
|
||||
labelValues: { '{}': { label: ['a', 'b', 'c'] } },
|
||||
});
|
||||
const value = Plain.deserialize('{label!=}');
|
||||
const range = value.selection.merge({ anchorOffset: 8 });
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(8).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '!=',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
@@ -225,35 +241,30 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a refresher on label context and unavailable metric', () => {
|
||||
it('returns a refresher on label context and unavailable metric', async () => {
|
||||
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } });
|
||||
const value = Plain.deserialize('metric{}');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 7,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(7).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-labels'],
|
||||
value: valueWithSelection,
|
||||
});
|
||||
expect(result.context).toBeUndefined();
|
||||
expect(result.refresher).toBeInstanceOf(Promise);
|
||||
expect(result.suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns label values on label context when given a metric and a label key', () => {
|
||||
it('returns label values on label context when given a metric and a label key', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['bar'] },
|
||||
labelValues: { '{__name__="metric"}': { bar: ['baz'] } },
|
||||
});
|
||||
const value = Plain.deserialize('metric{bar=ba}');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 13,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(13).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '=ba',
|
||||
prefix: 'ba',
|
||||
wrapperClasses: ['context-labels'],
|
||||
@@ -264,14 +275,12 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
|
||||
});
|
||||
|
||||
it('returns label suggestions on aggregation context and metric w/ selector', () => {
|
||||
it('returns label suggestions on aggregation context and metric w/ selector', async () => {
|
||||
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } });
|
||||
const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 26,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(26).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
@@ -281,14 +290,12 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
||||
});
|
||||
|
||||
it('returns label suggestions on aggregation context and metric w/o selector', () => {
|
||||
it('returns label suggestions on aggregation context and metric w/o selector', async () => {
|
||||
const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
|
||||
const value = Plain.deserialize('sum(metric) by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 16,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(16).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
@@ -298,15 +305,16 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside a multi-line aggregation context', () => {
|
||||
it('returns label suggestions inside a multi-line aggregation context', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
|
||||
const aggregationTextBlock = value.document.getBlocksAsArray()[3];
|
||||
const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 });
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const aggregationTextBlock = value.document.getBlocks().get(3);
|
||||
const ed = new SlateEditor({ value });
|
||||
ed.moveToStartOfNode(aggregationTextBlock);
|
||||
const valueWithSelection = ed.moveForward(4).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
@@ -321,16 +329,14 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside an aggregation context with a range vector', () => {
|
||||
it('returns label suggestions inside an aggregation context with a range vector', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 26,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(26).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
@@ -345,16 +351,14 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside an aggregation context with a range vector and label', () => {
|
||||
it('returns label suggestions inside an aggregation context with a range vector and label', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 42,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(42).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
@@ -369,16 +373,14 @@ describe('Language completion provider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => {
|
||||
it('returns no suggestions inside an unclear aggregation context using alternate syntax', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 8,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(8).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
@@ -388,16 +390,14 @@ describe('Language completion provider', () => {
|
||||
expect(result.suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside an aggregation context using alternate syntax', () => {
|
||||
it('returns label suggestions inside an aggregation context using alternate syntax', async () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum by () (metric)');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 8,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
const ed = new SlateEditor({ value });
|
||||
const valueWithSelection = ed.moveForward(8).value;
|
||||
const result = await instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
|
||||
Reference in New Issue
Block a user