Commit a0fbe3c2 by David Kaltschmidt

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
parent 1f88bfd2
...@@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => { ...@@ -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'; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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) {
context = 'context-labels'; const possibleKeys = _.difference(labelKeys, existingKeys);
suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) }); if (possibleKeys.length > 0) {
context = 'context-labels';
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
}
} }
} }
......
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(); ...@@ -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 { ...@@ -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 };
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment