Chore: Update Slate to 0.47.8 (#19197)

* Chore: Update Slate to 0.47.8
Closes #17430
This commit is contained in:
kay delaney
2019-09-23 12:26:05 +01:00
committed by GitHub
parent 918cb78092
commit 68d6da77da
56 changed files with 1760 additions and 1412 deletions

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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]);
}
});

View File

@@ -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'],