From 34c1433ba448fd9f3cc75ef021a279f63372e649 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 18 Dec 2019 13:35:31 +0100 Subject: [PATCH] Prometheus: Fix typehead after binary operators (#21152) - no longer do right-trim to prevent suggestions after space - rely on splitting on syntax elements to identify relevant prefix - added tests --- .../prometheus/language_provider.test.ts | 53 ++++++++++++++++++- .../prometheus/language_provider.ts | 22 ++++---- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts index 08df3ba0d5a..43c4ce543ac 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts @@ -12,6 +12,56 @@ describe('Language completion provider', () => { getTimeRange: () => ({ start: 0, end: 1 }), } as any) as PrometheusDatasource; + describe('cleanText', () => { + const cleanText = new LanguageProvider(datasource).cleanText; + it('does not remove metric or label keys', () => { + expect(cleanText('foo')).toBe('foo'); + expect(cleanText('foo_bar')).toBe('foo_bar'); + }); + + it('keeps trailing space but removes leading', () => { + expect(cleanText('foo ')).toBe('foo '); + expect(cleanText(' foo')).toBe('foo'); + }); + + it('removes label syntax', () => { + expect(cleanText('foo="bar')).toBe('bar'); + expect(cleanText('foo!="bar')).toBe('bar'); + expect(cleanText('foo=~"bar')).toBe('bar'); + expect(cleanText('foo!~"bar')).toBe('bar'); + expect(cleanText('{bar')).toBe('bar'); + }); + + it('removes previous operators', () => { + expect(cleanText('foo + bar')).toBe('bar'); + expect(cleanText('foo+bar')).toBe('bar'); + expect(cleanText('foo - bar')).toBe('bar'); + expect(cleanText('foo * bar')).toBe('bar'); + expect(cleanText('foo / bar')).toBe('bar'); + expect(cleanText('foo % bar')).toBe('bar'); + expect(cleanText('foo ^ bar')).toBe('bar'); + expect(cleanText('foo and bar')).toBe('bar'); + expect(cleanText('foo or bar')).toBe('bar'); + expect(cleanText('foo unless bar')).toBe('bar'); + expect(cleanText('foo == bar')).toBe('bar'); + expect(cleanText('foo != bar')).toBe('bar'); + expect(cleanText('foo > bar')).toBe('bar'); + expect(cleanText('foo < bar')).toBe('bar'); + expect(cleanText('foo >= bar')).toBe('bar'); + expect(cleanText('foo <= bar')).toBe('bar'); + }); + + it('removes aggregation syntax', () => { + expect(cleanText('(bar')).toBe('bar'); + expect(cleanText('(foo,bar')).toBe('bar'); + expect(cleanText('(foo, bar')).toBe('bar'); + }); + + it('removes range syntax', () => { + expect(cleanText('[1m')).toBe('1m'); + }); + }); + describe('empty query suggestions', () => { it('returns no suggestions on empty context', async () => { const instance = new LanguageProvider(datasource); @@ -96,8 +146,9 @@ describe('Language completion provider', () => { query: { refId: '1', expr: 'metric' }, }, ]; - let value = Plain.deserialize('a'); + let value = Plain.deserialize('m'); value = value.setSelection({ anchor: { offset: 1 }, focus: { offset: 1 } }); + // Even though no metric with `m` is present, we still get metric completion items, filtering is done by the consumer const result = await instance.provideCompletionItems( { text: 'm', prefix: 'm', value, wrapperClasses: [] }, { history } diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index a3170731805..28e0761e416 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import LRU from 'lru-cache'; +import { Value } from 'slate'; import { dateTime, LanguageProvider, HistoryItem } from '@grafana/data'; import { CompletionItem, TypeaheadInput, TypeaheadOutput, CompletionItemGroup } from '@grafana/ui'; @@ -50,6 +51,8 @@ function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): Com return item; } +const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|and|or|unless|==|>=|!=|<=|>|<|=|~|,)/; + export default class PromQlLanguageProvider extends LanguageProvider { histogramMetrics?: string[]; timeRange?: { start: number; end: number }; @@ -81,15 +84,12 @@ export default class PromQlLanguageProvider extends LanguageProvider { Object.assign(this, initialValues); } - // Strip syntax chars - cleanText = (s: string) => - s - .replace(/[{}[\]="(),!]/g, '') - .replace(/^\s*[~+\-*/^%]/, '') - .trim() - .split(' ') - .pop() - .trim(); + // Strip syntax chars so that typeahead suggestions can work on clean inputs + cleanText(s: string) { + const parts = s.split(PREFIX_DELIMITER_REGEX); + const last = parts.pop(); + return last.trimLeft().replace(/"$/, ''); + } get syntax() { return PromqlSyntax; @@ -159,7 +159,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { return this.getLabelCompletionItems({ prefix, text, value, labelKey, wrapperClasses }); } else if (wrapperClasses.includes('context-aggregation')) { // Suggestions for sum(metric) by (|) - return this.getAggregationCompletionItems({ prefix, text, value, labelKey, wrapperClasses }); + return this.getAggregationCompletionItems(value); } else if (empty) { // Suggestions for empty query field return this.getEmptyCompletionItems(context); @@ -239,7 +239,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; } - getAggregationCompletionItems = async ({ value }: TypeaheadInput): Promise => { + getAggregationCompletionItems = async (value: Value): Promise => { const suggestions: CompletionItemGroup[] = []; // Stitch all query lines together to support multi-line queries