diff --git a/public/app/features/explore/PromQueryField.test.tsx b/public/app/features/explore/PromQueryField.test.tsx
index c82a1cd448f..802bb695f2c 100644
--- a/public/app/features/explore/PromQueryField.test.tsx
+++ b/public/app/features/explore/PromQueryField.test.tsx
@@ -96,11 +96,14 @@ describe('PromQueryField typeahead handling', () => {
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 value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
const range = value.selection.merge({
- anchorOffset: 11,
+ anchorOffset: 36,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
@@ -113,6 +116,33 @@ describe('PromQueryField typeahead handling', () => {
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
+ it('returns label value suggestions inside a label value context after a negated matching operator', () => {
+ const instance = shallow(
+
+ ).instance() as PromQueryField;
+ const value = Plain.deserialize('{label!=}');
+ const range = value.selection.merge({ anchorOffset: 8 });
+ const valueWithSelection = value.change().select(range).value;
+ const result = instance.getTypeahead({
+ text: '!=',
+ prefix: '',
+ wrapperClasses: ['context-labels'],
+ labelKey: 'label',
+ value: valueWithSelection,
+ });
+ expect(result.context).toBe('context-label-values');
+ expect(result.suggestions).toEqual([
+ {
+ items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
+ label: 'Label values for "label"',
+ },
+ ]);
+ });
+
it('returns a refresher on label context and unavailable metric', () => {
const instance = shallow(
diff --git a/public/app/features/explore/PromQueryField.tsx b/public/app/features/explore/PromQueryField.tsx
index fbe58912a81..442e51af987 100644
--- a/public/app/features/explore/PromQueryField.tsx
+++ b/public/app/features/explore/PromQueryField.tsx
@@ -111,7 +111,7 @@ export function willApplySuggestion(
case 'context-label-values': {
// Always add quotes and remove existing ones instead
- if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) {
+ if (!typeaheadText.match(/^(!?=~?"|")/)) {
suggestion = `"${suggestion}`;
}
if (getNextCharacter() !== '"') {
@@ -421,7 +421,7 @@ class PromQueryField extends React.PureComponent -1;
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
- if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
+ if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
// Label values
if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
const labelValues = this.state.labelValues[selector][labelKey];
diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx
index c89893b4f28..c3c41b7ab17 100644
--- a/public/app/features/explore/QueryField.tsx
+++ b/public/app/features/explore/QueryField.tsx
@@ -228,7 +228,13 @@ class QueryField extends React.PureComponent s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
const selectorRegexp = /\{[^}]*?\}/;
-const labelRegexp = /\b\w+="[^"\n]*?"/g;
+const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
if (!query.match(selectorRegexp)) {
// Special matcher for metrics
@@ -66,11 +66,8 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
// Extract clean labels to form clean selector, incomplete labels are dropped
const selector = query.slice(prefixOpen, suffixClose);
const labels = {};
- selector.replace(labelRegexp, match => {
- const delimiterIndex = match.indexOf('=');
- const key = match.slice(0, delimiterIndex);
- const value = match.slice(delimiterIndex + 1, match.length);
- labels[key] = value;
+ selector.replace(labelRegexp, (_, key, operator, value) => {
+ labels[key] = { value, operator };
return '';
});
@@ -78,12 +75,12 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any
const metricPrefix = query.slice(0, prefixOpen);
const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/);
if (metricMatch) {
- labels['__name__'] = `"${metricMatch[0]}"`;
+ labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' };
}
// Build sorted selector
const labelKeys = Object.keys(labels).sort();
- const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
+ const cleanSelector = labelKeys.map(key => `${key}${labels[key].operator}${labels[key].value}`).join(',');
const selectorString = ['{', cleanSelector, '}'].join('');