Ver código fonte

Merge pull request #12874 from grafana/davkal/explore-facetting-filtered

Explore: Filter out existing labels in label suggestions
Marcus Efraimsson 7 anos atrás
pai
commit
c970e1e4b8

+ 19 - 0
public/app/containers/Explore/PromQueryField.test.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(
+        <PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
+      ).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(
         <PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />

+ 11 - 5
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<PromQueryFieldProps, PromQueryField
     const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
     // foo{bar="1"}
     const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
-    const selector = getCleanSelector(selectorString, selectorString.length - 2);
+    const selector = parseSelector(selectorString, selectorString.length - 2).selector;
 
     const labelKeys = this.state.labelKeys[selector];
     if (labelKeys) {
@@ -353,12 +353,15 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
 
     // Get normalized selector
     let selector;
+    let parsedSelector;
     try {
-      selector = getCleanSelector(line, cursorOffset);
+      parsedSelector = parseSelector(line, cursorOffset);
+      selector = parsedSelector.selector;
     } catch {
       selector = EMPTY_SELECTOR;
     }
     const containsMetric = selector.indexOf('__name__=') > -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<PromQueryFieldProps, PromQueryField
       // Label keys
       const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
       if (labelKeys) {
-        context = 'context-labels';
-        suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) });
+        const possibleKeys = _.difference(labelKeys, existingKeys);
+        if (possibleKeys.length > 0) {
+          context = 'context-labels';
+          suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
+        }
       }
     }
 

+ 45 - 17
public/app/containers/Explore/utils/prometheus.test.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"}');
   });
 });

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