Commit 3667781e by David Committed by GitHub

Loki: fix filter expression suggestions (#21290)

* Loki: fix filter expression suggestions

- dont suggest term completion items in filter expression
- allow at least one character before suggesting term items
- keep logql expression when switching between Metrics/Logs mode
- show only history by default in completion items

* Clear results when changing mode
parent 334b89f3
...@@ -153,7 +153,6 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun ...@@ -153,7 +153,6 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
*/ */
export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> { export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> {
return dispatch => { return dispatch => {
dispatch(clearQueriesAction({ exploreId }));
dispatch(changeModeAction({ exploreId, mode })); dispatch(changeModeAction({ exploreId, mode }));
}; };
} }
......
...@@ -61,13 +61,17 @@ describe('Explore item reducer', () => { ...@@ -61,13 +61,17 @@ describe('Explore item reducer', () => {
}); });
describe('changing datasource', () => { describe('changing datasource', () => {
describe('when changeDataType is dispatched', () => { describe('when changeMode is dispatched', () => {
it('then it should set correct state', () => { it('then it should set correct state', () => {
reducerTester() reducerTester()
.givenReducer(itemReducer, {}) .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, {})
.whenActionIsDispatched(changeModeAction({ exploreId: ExploreId.left, mode: ExploreMode.Logs })) .whenActionIsDispatched(changeModeAction({ exploreId: ExploreId.left, mode: ExploreMode.Logs }))
.thenStateShouldEqual({ .thenStatePredicateShouldEqual((resultingState: ExploreItemState) => {
mode: ExploreMode.Logs, expect(resultingState.mode).toEqual(ExploreMode.Logs);
expect(resultingState.logsResult).toBeNull();
expect(resultingState.graphResult).toBeNull();
expect(resultingState.tableResult).toBeNull();
return true;
}); });
}); });
}); });
......
...@@ -183,8 +183,15 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta ...@@ -183,8 +183,15 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
.addMapper({ .addMapper({
filter: changeModeAction, filter: changeModeAction,
mapper: (state, action): ExploreItemState => { mapper: (state, action): ExploreItemState => {
const mode = action.payload.mode; return {
return { ...state, mode }; ...state,
mode: action.payload.mode,
graphResult: null,
tableResult: null,
logsResult: null,
queryResponse: createEmptyQueryResponse(),
loading: false,
};
}, },
}) })
.addMapper({ .addMapper({
......
...@@ -9,7 +9,6 @@ import { beforeEach } from 'test/lib/common'; ...@@ -9,7 +9,6 @@ import { beforeEach } from 'test/lib/common';
import { makeMockLokiDatasource } from './mocks'; import { makeMockLokiDatasource } from './mocks';
import LokiDatasource from './datasource'; import LokiDatasource from './datasource';
import { FUNCTIONS } from './syntax';
jest.mock('app/store/store', () => ({ jest.mock('app/store/store', () => ({
store: { store: {
...@@ -31,18 +30,17 @@ describe('Language completion provider', () => { ...@@ -31,18 +30,17 @@ describe('Language completion provider', () => {
to: 1560163909000, to: 1560163909000,
}; };
describe('empty query suggestions', () => { describe('query suggestions', () => {
it('returns function suggestions on empty context', async () => { it('returns no suggestions on empty context', async () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const value = Plain.deserialize(''); const value = Plain.deserialize('');
const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
expect(result.context).toBeUndefined(); expect(result.context).toBeUndefined();
expect(result.suggestions.length).toEqual(1); expect(result.suggestions.length).toEqual(0);
expect(result.suggestions[0].label).toEqual('Functions');
}); });
it('returns function suggestions with history on empty context when history was provided', async () => { it('returns history on empty context when history was provided', async () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const value = Plain.deserialize(''); const value = Plain.deserialize('');
const history: LokiHistoryItem[] = [ const history: LokiHistoryItem[] = [
...@@ -66,16 +64,13 @@ describe('Language completion provider', () => { ...@@ -66,16 +64,13 @@ describe('Language completion provider', () => {
}, },
], ],
}, },
{
label: 'Functions',
items: FUNCTIONS,
},
]); ]);
}); });
it('returns function suggestions within regexp', async () => { it('returns function and history suggestions', async () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const input = createTypeaheadInput('{} ()', '', undefined, 4, []); const input = createTypeaheadInput('m', 'm', undefined, 1, [], instance);
// Historic expressions don't have to match input, filtering is done in field
const history: LokiHistoryItem[] = [ const history: LokiHistoryItem[] = [
{ {
query: { refId: '1', expr: '{app="foo"}' }, query: { refId: '1', expr: '{app="foo"}' },
...@@ -84,8 +79,9 @@ describe('Language completion provider', () => { ...@@ -84,8 +79,9 @@ describe('Language completion provider', () => {
]; ];
const result = await instance.provideCompletionItems(input, { history }); const result = await instance.provideCompletionItems(input, { history });
expect(result.context).toBeUndefined(); expect(result.context).toBeUndefined();
expect(result.suggestions.length).toEqual(1); expect(result.suggestions.length).toEqual(2);
expect(result.suggestions[0].label).toEqual('Functions'); expect(result.suggestions[0].label).toEqual('History');
expect(result.suggestions[1].label).toEqual('Functions');
}); });
}); });
...@@ -248,14 +244,15 @@ function createTypeaheadInput( ...@@ -248,14 +244,15 @@ function createTypeaheadInput(
text: string, text: string,
labelKey?: string, labelKey?: string,
anchorOffset?: number, anchorOffset?: number,
wrapperClasses?: string[] wrapperClasses?: string[],
instance?: LanguageProvider
): TypeaheadInput { ): TypeaheadInput {
const deserialized = Plain.deserialize(value); const deserialized = Plain.deserialize(value);
const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1)); const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
const valueWithSelection = deserialized.setSelection(range); const valueWithSelection = deserialized.setSelection(range);
return { return {
text, text,
prefix: '', prefix: instance ? instance.cleanText(text) : '',
wrapperClasses: wrapperClasses || ['context-labels'], wrapperClasses: wrapperClasses || ['context-labels'],
value: valueWithSelection, value: valueWithSelection,
labelKey, labelKey,
......
...@@ -13,7 +13,6 @@ import { RATE_RANGES } from '../prometheus/promql'; ...@@ -13,7 +13,6 @@ import { RATE_RANGES } from '../prometheus/promql';
import LokiDatasource from './datasource'; import LokiDatasource from './datasource';
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui'; import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import { ExploreMode } from 'app/types/explore';
const DEFAULT_KEYS = ['job', 'namespace']; const DEFAULT_KEYS = ['job', 'namespace'];
const EMPTY_SELECTOR = '{}'; const EMPTY_SELECTOR = '{}';
...@@ -130,8 +129,8 @@ export default class LokiLanguageProvider extends LanguageProvider { ...@@ -130,8 +129,8 @@ export default class LokiLanguageProvider extends LanguageProvider {
// Prevent suggestions in `function(|suffix)` // Prevent suggestions in `function(|suffix)`
const noSuffix = !nextCharacter || nextCharacter === ')'; const noSuffix = !nextCharacter || nextCharacter === ')';
// Empty prefix is safe if it does not immediately follow a complete expression and has no text after it // Prefix is safe if it does not immediately follow a complete expression and has no text after it
const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix; const safePrefix = prefix && !text.match(/^['"~=\]})\s]+$/) && noSuffix;
// About to type next operand if preceded by binary operator // About to type next operand if preceded by binary operator
const operatorsPattern = /[+\-*/^%]/; const operatorsPattern = /[+\-*/^%]/;
...@@ -145,8 +144,12 @@ export default class LokiLanguageProvider extends LanguageProvider { ...@@ -145,8 +144,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
// Suggestions for {|} and {foo=|} // Suggestions for {|} and {foo=|}
return await this.getLabelCompletionItems(input, context); return await this.getLabelCompletionItems(input, context);
} else if (empty) { } else if (empty) {
return this.getEmptyCompletionItems(context || {}, ExploreMode.Metrics); // Suggestions for empty query field
} else if ((prefixUnrecognized && noSuffix) || safeEmptyPrefix || isNextOperand) { return this.getEmptyCompletionItems(context);
} else if (prefixUnrecognized && noSuffix && !isNextOperand) {
// Show term suggestions in a couple of scenarios
return this.getBeginningCompletionItems(context);
} else if (prefixUnrecognized && safePrefix) {
// Show term suggestions in a couple of scenarios // Show term suggestions in a couple of scenarios
return this.getTermCompletionItems(); return this.getTermCompletionItems();
} }
...@@ -156,8 +159,14 @@ export default class LokiLanguageProvider extends LanguageProvider { ...@@ -156,8 +159,14 @@ export default class LokiLanguageProvider extends LanguageProvider {
}; };
} }
getEmptyCompletionItems(context: TypeaheadContext, mode?: ExploreMode): TypeaheadOutput { getBeginningCompletionItems = (context: TypeaheadContext): TypeaheadOutput => {
const { history } = context; return {
suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions],
};
};
getEmptyCompletionItems(context: TypeaheadContext): TypeaheadOutput {
const history = context?.history;
const suggestions = []; const suggestions = [];
if (history && history.length) { if (history && history.length) {
...@@ -178,11 +187,6 @@ export default class LokiLanguageProvider extends LanguageProvider { ...@@ -178,11 +187,6 @@ export default class LokiLanguageProvider extends LanguageProvider {
}); });
} }
if (mode === ExploreMode.Metrics) {
const termCompletionItems = this.getTermCompletionItems();
suggestions.push(...termCompletionItems.suggestions);
}
return { suggestions }; return { suggestions };
} }
......
...@@ -146,7 +146,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { ...@@ -146,7 +146,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
// Prevent suggestions in `function(|suffix)` // Prevent suggestions in `function(|suffix)`
const noSuffix = !nextCharacter || nextCharacter === ')'; const noSuffix = !nextCharacter || nextCharacter === ')';
// Empty prefix is safe if it does not immediately follow a complete expression and has no text after it // Prefix is safe if it does not immediately follow a complete expression and has no text after it
const safePrefix = prefix && !text.match(/^[\]})\s]+$/) && noSuffix; const safePrefix = prefix && !text.match(/^[\]})\s]+$/) && noSuffix;
// About to type next operand if preceded by binary operator // About to type next operand if preceded by binary operator
......
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