From 2a4a19388f30220fd81586bd70b7e3aa8a1495e5 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sat, 27 Oct 2018 16:54:12 +0800 Subject: [PATCH 1/2] Fix label suggestions for multi-line aggregation queries No label suggestions were being returned for multi-line aggregation contexts because the parsed selector string does not see the full context before a `by` or `without` clause. This solution stitches together all text nodes that comprise the query editor to ensure the selector has sufficient context to generate suggestions. Also, an additional workaround has been included to ensure range vector syntax does not disrupt label suggestions in aggregation contexts. Related: #12890 --- .../prometheus/language_provider.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 3e406a71264..3dd15eb713e 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -162,16 +162,29 @@ export default class PromQlLanguageProvider extends LanguageProvider { let refresher: Promise = null; const suggestions: CompletionItemGroup[] = []; - // sum(foo{bar="1"}) by (|) - const line = value.anchorBlock.getText(); - const cursorOffset: number = value.anchorOffset; - // sum(foo{bar="1"}) by ( - const leftSide = line.slice(0, cursorOffset); + // Stitch all query lines together to support multi-line queries + let queryOffset; + const queryText = value.document.getBlocks().reduce((text, 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; + } + text += blockText; + return text; + }, ''); + + const leftSide = queryText.slice(0, queryOffset); const openParensAggregationIndex = leftSide.lastIndexOf('('); const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; - // foo{bar="1"} - const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + + let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + + // Range vector syntax not accounted for by subsequent parse so discard it if present + selectorString = selectorString.replace(/\[[^\]]+\]$/, ''); + const selector = parseSelector(selectorString, selectorString.length - 2).selector; const labelKeys = this.labelKeys[selector]; From 61843b58db92fbed130f323a5ec0ab4abff62d92 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sat, 27 Oct 2018 17:02:03 +0800 Subject: [PATCH 2/2] Add tests to cover aggregation context cases This should cover use cases involving multi-line queries and range vector syntax inside aggregation contexts. Related: #12890 --- .../specs/language_provider.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 3a46e2efaf3..20e148efd57 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -198,5 +198,76 @@ describe('Language completion provider', () => { expect(result.context).toBe('context-aggregation'); expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); + + it('returns label suggestions inside a multi-line aggregation context', () => { + 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({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); + + it('returns label suggestions inside an aggregation context with a range vector', () => { + 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({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); + + it('returns label suggestions inside an aggregation context with a range vector and label', () => { + 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({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); }); });