diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts new file mode 100644 index 00000000000..45a016f2e89 --- /dev/null +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts @@ -0,0 +1,290 @@ +import { M } from 'msw/lib/glossary-dc3fd077'; + +import LokiLanguageProvider from '../../../LanguageProvider'; +import { LokiDatasource } from '../../../datasource'; +import { createLokiDatasource } from '../../../mocks'; + +import { CompletionDataProvider } from './CompletionDataProvider'; +import { getCompletions } from './completions'; +import { Label, Situation } from './situation'; + +const history = [ + { + ts: 12345678, + query: { + refId: 'test-1', + expr: '{test: unit}', + }, + }, + { + ts: 87654321, + query: { + refId: 'test-1', + expr: '{test: unit}', + }, + }, +]; + +const labelNames = ['place', 'source']; +const labelValues = ['moon', 'luna']; +const extractedLabelKeys = ['extracted', 'label']; +const otherLabels: Label[] = [ + { + name: 'place', + value: 'luna', + op: '=', + }, +]; +const afterSelectorCompletions = [ + { + insertText: '|= "$0"', + isSnippet: true, + label: '|= "something"', + type: 'LINE_FILTER', + }, + { + insertText: '!= "$0"', + isSnippet: true, + label: '!= "something"', + type: 'LINE_FILTER', + }, + { + insertText: '|~ "$0"', + isSnippet: true, + label: '|~ "something"', + type: 'LINE_FILTER', + }, + { + insertText: '!~ "$0"', + isSnippet: true, + label: '!~ "something"', + type: 'LINE_FILTER', + }, + { + insertText: '', + label: '// Placeholder for the detected parser', + type: 'DETECTED_PARSER_PLACEHOLDER', + }, + { + insertText: '', + label: '// Placeholder for logfmt or json', + type: 'OPPOSITE_PARSER_PLACEHOLDER', + }, + { + insertText: 'pattern', + label: 'pattern', + type: 'PARSER', + }, + { + insertText: 'regexp', + label: 'regexp', + type: 'PARSER', + }, + { + insertText: 'unpack', + label: 'unpack', + type: 'PARSER', + }, + { + insertText: 'unwrap extracted', + label: 'unwrap extracted (detected)', + type: 'LINE_FILTER', + }, + { + insertText: 'unwrap label', + label: 'unwrap label (detected)', + type: 'LINE_FILTER', + }, + { + insertText: 'unwrap', + label: 'unwrap', + type: 'LINE_FILTER', + }, + { + insertText: 'line_format "{{.$0}}"', + isSnippet: true, + label: 'line_format', + type: 'LINE_FORMAT', + }, +]; + +function buildAfterSelectorCompletions( + detectedParser: string, + detectedParserType: string, + otherParser: string, + explanation = '(detected)' +) { + return afterSelectorCompletions.map((completion) => { + if (completion.type === 'DETECTED_PARSER_PLACEHOLDER') { + return { + ...completion, + type: detectedParserType, + label: `${detectedParser} ${explanation}`, + insertText: detectedParser, + }; + } else if (completion.type === 'OPPOSITE_PARSER_PLACEHOLDER') { + return { + ...completion, + type: 'PARSER', + label: otherParser, + insertText: otherParser, + }; + } + + return { ...completion }; + }); +} + +describe('getCompletions', () => { + let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource; + beforeEach(() => { + datasource = createLokiDatasource(); + languageProvider = new LokiLanguageProvider(datasource); + completionProvider = new CompletionDataProvider(languageProvider, history); + + jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames); + jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues); + jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ + extractedLabelKeys, + hasJSON: false, + hasLogfmt: false, + }); + }); + + test.each(['EMPTY', 'AT_ROOT'])(`Returns completion options when the situation is %s`, async (type) => { + const situation = { type } as Situation; + const completions = await getCompletions(situation, completionProvider); + + expect(completions).toHaveLength(25); + }); + + test('Returns completion options when the situation is IN_DURATION', async () => { + const situation: Situation = { type: 'IN_DURATION' }; + const completions = await getCompletions(situation, completionProvider); + + expect(completions).toHaveLength(9); + }); + + test('Returns completion options when the situation is IN_GROUPING', async () => { + const situation: Situation = { type: 'IN_GROUPING', otherLabels }; + const completions = await getCompletions(situation, completionProvider); + + expect(completions).toEqual([ + { + insertText: 'place', + label: 'place', + triggerOnInsert: false, + type: 'LABEL_NAME', + }, + { + insertText: 'source', + label: 'source', + triggerOnInsert: false, + type: 'LABEL_NAME', + }, + { + insertText: 'extracted', + label: 'extracted (parsed)', + triggerOnInsert: false, + type: 'LABEL_NAME', + }, + { + insertText: 'label', + label: 'label (parsed)', + triggerOnInsert: false, + type: 'LABEL_NAME', + }, + ]); + }); + + test('Returns completion options when the situation is IN_LABEL_SELECTOR_NO_LABEL_NAME', async () => { + const situation: Situation = { type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME', otherLabels }; + const completions = await getCompletions(situation, completionProvider); + + expect(completions).toEqual([ + { + insertText: 'place=', + label: 'place', + triggerOnInsert: true, + type: 'LABEL_NAME', + }, + { + insertText: 'source=', + label: 'source', + triggerOnInsert: true, + type: 'LABEL_NAME', + }, + ]); + }); + + test('Returns completion options when the situation is IN_LABEL_SELECTOR_WITH_LABEL_NAME', async () => { + const situation: Situation = { + type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME', + otherLabels, + labelName: '', + betweenQuotes: false, + }; + let completions = await getCompletions(situation, completionProvider); + + expect(completions).toEqual([ + { + insertText: '"moon"', + label: 'moon', + type: 'LABEL_VALUE', + }, + { + insertText: '"luna"', + label: 'luna', + type: 'LABEL_VALUE', + }, + ]); + + completions = await getCompletions({ ...situation, betweenQuotes: true }, completionProvider); + + expect(completions).toEqual([ + { + insertText: 'moon', + label: 'moon', + type: 'LABEL_VALUE', + }, + { + insertText: 'luna', + label: 'luna', + type: 'LABEL_VALUE', + }, + ]); + }); + + test('Returns completion options when the situation is AFTER_SELECTOR and JSON parser', async () => { + jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ + extractedLabelKeys, + hasJSON: true, + hasLogfmt: false, + }); + const situation: Situation = { type: 'AFTER_SELECTOR', labels: [], afterPipe: true }; + const completions = await getCompletions(situation, completionProvider); + + const expected = buildAfterSelectorCompletions('json', 'PARSER', 'logfmt'); + expect(completions).toEqual(expected); + }); + + test('Returns completion options when the situation is AFTER_SELECTOR and Logfmt parser', async () => { + jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ + extractedLabelKeys, + hasJSON: false, + hasLogfmt: true, + }); + const situation: Situation = { type: 'AFTER_SELECTOR', labels: [], afterPipe: true }; + const completions = await getCompletions(situation, completionProvider); + + const expected = buildAfterSelectorCompletions('logfmt', 'DURATION', 'json'); + expect(completions).toEqual(expected); + }); + + test('Returns completion options when the situation is IN_AGGREGATION', async () => { + const situation: Situation = { type: 'IN_AGGREGATION' }; + const completions = await getCompletions(situation, completionProvider); + + expect(completions).toHaveLength(22); + }); +}); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts index cb07ce16bb6..3008ba14ddc 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts @@ -214,15 +214,12 @@ export async function getCompletions( dataProvider: CompletionDataProvider ): Promise { switch (situation.type) { + case 'EMPTY': case 'AT_ROOT': const historyCompletions = await getAllHistoryCompletions(dataProvider); return [...historyCompletions, ...LOG_COMPLETIONS, ...AGGREGATION_COMPLETIONS, ...FUNCTION_COMPLETIONS]; case 'IN_DURATION': return DURATION_COMPLETIONS; - case 'EMPTY': { - const historyCompletions = await getAllHistoryCompletions(dataProvider); - return [...historyCompletions, ...LOG_COMPLETIONS, ...AGGREGATION_COMPLETIONS, ...FUNCTION_COMPLETIONS]; - } case 'IN_GROUPING': return getInGroupingCompletions(situation.otherLabels, dataProvider); case 'IN_LABEL_SELECTOR_NO_LABEL_NAME':