mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Filter out existing labels in label suggestions
- a valid selector returns all possible labels from the series API - we only want to suggest the label keys that are not part of the selector yet
This commit is contained in:
parent
1f88bfd2bc
commit
a0fbe3c296
@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => {
|
|||||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
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', () => {
|
it('returns a refresher on label context and unavailable metric', () => {
|
||||||
const instance = shallow(
|
const instance = shallow(
|
||||||
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
|
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
|
||||||
|
@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
|
|||||||
import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
|
import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
|
||||||
import BracesPlugin from './slate-plugins/braces';
|
import BracesPlugin from './slate-plugins/braces';
|
||||||
import RunnerPlugin from './slate-plugins/runner';
|
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, {
|
import TypeaheadField, {
|
||||||
Suggestion,
|
Suggestion,
|
||||||
@ -328,7 +328,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
||||||
// foo{bar="1"}
|
// foo{bar="1"}
|
||||||
const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
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];
|
const labelKeys = this.state.labelKeys[selector];
|
||||||
if (labelKeys) {
|
if (labelKeys) {
|
||||||
@ -353,12 +353,15 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
|
|
||||||
// Get normalized selector
|
// Get normalized selector
|
||||||
let selector;
|
let selector;
|
||||||
|
let parsedSelector;
|
||||||
try {
|
try {
|
||||||
selector = getCleanSelector(line, cursorOffset);
|
parsedSelector = parseSelector(line, cursorOffset);
|
||||||
|
selector = parsedSelector.selector;
|
||||||
} catch {
|
} catch {
|
||||||
selector = EMPTY_SELECTOR;
|
selector = EMPTY_SELECTOR;
|
||||||
}
|
}
|
||||||
const containsMetric = selector.indexOf('__name__=') > -1;
|
const containsMetric = selector.indexOf('__name__=') > -1;
|
||||||
|
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
||||||
|
|
||||||
if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
|
if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
|
||||||
// Label values
|
// Label values
|
||||||
@ -374,8 +377,11 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
// Label keys
|
// Label keys
|
||||||
const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
|
const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
|
||||||
if (labelKeys) {
|
if (labelKeys) {
|
||||||
|
const possibleKeys = _.difference(labelKeys, existingKeys);
|
||||||
|
if (possibleKeys.length > 0) {
|
||||||
context = 'context-labels';
|
context = 'context-labels';
|
||||||
suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) });
|
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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', () => {
|
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', () => {
|
it('throws if selector is broken', () => {
|
||||||
expect(() => getCleanSelector('{foo')).toThrow();
|
expect(() => parseSelector('{foo')).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the selector sorted by label key', () => {
|
it('returns the selector sorted by label key', () => {
|
||||||
expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}');
|
parsed = parseSelector('{foo="bar"}');
|
||||||
expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",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', () => {
|
it('returns a clean selector from an incomplete one', () => {
|
||||||
expect(getCleanSelector('{foo}')).toBe('{}');
|
parsed = parseSelector('{foo}');
|
||||||
expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}');
|
expect(parsed.selector).toBe('{}');
|
||||||
expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}');
|
|
||||||
|
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', () => {
|
it('throws if not inside a selector', () => {
|
||||||
expect(() => getCleanSelector('foo{}', 0)).toThrow();
|
expect(() => parseSelector('foo{}', 0)).toThrow();
|
||||||
expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow();
|
expect(() => parseSelector('foo{} + bar{}', 5)).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the selector nearest to the cursor offset', () => {
|
it('returns the selector nearest to the cursor offset', () => {
|
||||||
expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
|
expect(() => parseSelector('{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"}');
|
parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1);
|
||||||
expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}');
|
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', () => {
|
it('returns a selector with metric if metric is given', () => {
|
||||||
expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}');
|
parsed = parseSelector('bar{foo}', 4);
|
||||||
expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}');
|
expect(parsed.selector).toBe('{__name__="bar"}');
|
||||||
|
|
||||||
|
parsed = parseSelector('baz{foo="bar"}', 12);
|
||||||
|
expect(parsed.selector).toBe('{__name__="baz",foo="bar"}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
|||||||
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
|
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
|
||||||
const selectorRegexp = /\{[^}]*?\}/;
|
const selectorRegexp = /\{[^}]*?\}/;
|
||||||
const labelRegexp = /\b\w+="[^"\n]*?"/g;
|
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)) {
|
if (!query.match(selectorRegexp)) {
|
||||||
// Special matcher for metrics
|
// Special matcher for metrics
|
||||||
if (query.match(/^\w+$/)) {
|
if (query.match(/^\w+$/)) {
|
||||||
return `{__name__="${query}"}`;
|
return {
|
||||||
|
selector: `{__name__="${query}"}`,
|
||||||
|
labelKeys: ['__name__'],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
throw new Error('Query must contain a selector: ' + query);
|
throw new Error('Query must contain a selector: ' + query);
|
||||||
}
|
}
|
||||||
@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build sorted selector
|
// Build sorted selector
|
||||||
const cleanSelector = Object.keys(labels)
|
const labelKeys = Object.keys(labels).sort();
|
||||||
.sort()
|
const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
|
||||||
.map(key => `${key}=${labels[key]}`)
|
|
||||||
.join(',');
|
|
||||||
|
|
||||||
return ['{', cleanSelector, '}'].join('');
|
const selectorString = ['{', cleanSelector, '}'].join('');
|
||||||
|
|
||||||
|
return { labelKeys, selector: selectorString };
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user