diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx index 350a529c89e..c82a1cd448f 100644 --- a/public/app/containers/Explore/PromQueryField.jest.tsx +++ b/public/app/containers/Explore/PromQueryField.jest.tsx @@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => { 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 = shallow( + + ).instance() as PromQueryField; + const value = Plain.deserialize('{job="foo",}'); + const range = value.selection.merge({ + anchorOffset: 11, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.getTypeahead({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + it('returns a refresher on label context and unavailable metric', () => { const instance = shallow( diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 1b3ff33971d..0991f08429a 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; import BracesPlugin from './slate-plugins/braces'; import RunnerPlugin from './slate-plugins/runner'; -import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus'; +import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus'; import TypeaheadField, { Suggestion, @@ -328,7 +328,7 @@ class PromQueryField extends React.Component -1; + const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { // Label values @@ -374,8 +377,11 @@ class PromQueryField extends React.Component 0) { + context = 'context-labels'; + suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) }); + } } } diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.jest.ts index febaecc29b5..d12d28c6bc9 100644 --- a/public/app/containers/Explore/utils/prometheus.jest.ts +++ b/public/app/containers/Explore/utils/prometheus.jest.ts @@ -1,33 +1,61 @@ -import { getCleanSelector } from './prometheus'; +import { parseSelector } from './prometheus'; + +describe('parseSelector()', () => { + let parsed; -describe('getCleanSelector()', () => { it('returns a clean selector from an empty selector', () => { - expect(getCleanSelector('{}', 1)).toBe('{}'); + parsed = parseSelector('{}', 1); + expect(parsed.selector).toBe('{}'); + expect(parsed.labelKeys).toEqual([]); }); + it('throws if selector is broken', () => { - expect(() => getCleanSelector('{foo')).toThrow(); + expect(() => parseSelector('{foo')).toThrow(); }); + it('returns the selector sorted by label key', () => { - expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}'); + parsed = parseSelector('{foo="bar"}'); + expect(parsed.selector).toBe('{foo="bar"}'); + expect(parsed.labelKeys).toEqual(['foo']); + + parsed = parseSelector('{foo="bar",baz="xx"}'); + expect(parsed.selector).toBe('{baz="xx",foo="bar"}'); }); + it('returns a clean selector from an incomplete one', () => { - expect(getCleanSelector('{foo}')).toBe('{}'); - expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}'); + parsed = parseSelector('{foo}'); + expect(parsed.selector).toBe('{}'); + + parsed = parseSelector('{foo="bar",baz}'); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{foo="bar",baz="}'); + expect(parsed.selector).toBe('{foo="bar"}'); }); + it('throws if not inside a selector', () => { - expect(() => getCleanSelector('foo{}', 0)).toThrow(); - expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow(); + expect(() => parseSelector('foo{}', 0)).toThrow(); + expect(() => parseSelector('foo{} + bar{}', 5)).toThrow(); }); + it('returns the selector nearest to the cursor offset', () => { - expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); - expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}'); - expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}'); - expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}'); + expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); + + parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1); + expect(parsed.selector).toBe('{foo="bar"}'); + + parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16); + expect(parsed.selector).toBe('{foo="bar"}'); }); + it('returns a selector with metric if metric is given', () => { - expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}'); - expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}'); + parsed = parseSelector('bar{foo}', 4); + expect(parsed.selector).toBe('{__name__="bar"}'); + + parsed = parseSelector('baz{foo="bar"}', 12); + expect(parsed.selector).toBe('{__name__="baz",foo="bar"}'); }); }); diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index ab77271076d..f5ccb848f2f 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; const selectorRegexp = /\{[^}]*?\}/; const labelRegexp = /\b\w+="[^"\n]*?"/g; -export function getCleanSelector(query: string, cursorOffset = 1): string { +export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics if (query.match(/^\w+$/)) { - return `{__name__="${query}"}`; + return { + selector: `{__name__="${query}"}`, + labelKeys: ['__name__'], + }; } throw new Error('Query must contain a selector: ' + query); } @@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string { } // Build sorted selector - const cleanSelector = Object.keys(labels) - .sort() - .map(key => `${key}=${labels[key]}`) - .join(','); + const labelKeys = Object.keys(labels).sort(); + const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(','); - return ['{', cleanSelector, '}'].join(''); + const selectorString = ['{', cleanSelector, '}'].join(''); + + return { labelKeys, selector: selectorString }; }