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