123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- import Plain from 'slate-plain-serializer';
- import { AbstractLabelOperator } from '@grafana/data';
- import { TypeaheadInput } from '@grafana/ui';
- import { LokiDatasource } from './datasource';
- import LanguageProvider, { LokiHistoryItem } from './language_provider';
- import { makeMockLokiDatasource } from './mocks';
- import { LokiQueryType } from './types';
- jest.mock('app/store/store', () => ({
- store: {
- getState: jest.fn().mockReturnValue({
- explore: {
- left: {
- mode: 'Logs',
- },
- },
- }),
- },
- }));
- describe('Language completion provider', () => {
- const datasource = makeMockLokiDatasource({});
- describe('query suggestions', () => {
- it('returns no suggestions on empty context', async () => {
- const instance = new LanguageProvider(datasource);
- const value = Plain.deserialize('');
- const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
- expect(result.context).toBeUndefined();
- expect(result.suggestions.length).toEqual(0);
- });
- it('returns history on empty context when history was provided', async () => {
- const instance = new LanguageProvider(datasource);
- const value = Plain.deserialize('');
- const history: LokiHistoryItem[] = [
- {
- query: { refId: '1', expr: '{app="foo"}' },
- ts: 1,
- },
- ];
- const result = await instance.provideCompletionItems(
- { text: '', prefix: '', value, wrapperClasses: [] },
- { history }
- );
- expect(result.context).toBeUndefined();
- expect(result.suggestions).toMatchObject([
- {
- label: 'History',
- items: [
- {
- label: '{app="foo"}',
- },
- ],
- },
- ]);
- });
- it('returns function and history suggestions', async () => {
- const instance = new LanguageProvider(datasource);
- const input = createTypeaheadInput('m', 'm', undefined, 1, [], instance);
- // Historic expressions don't have to match input, filtering is done in field
- const history: LokiHistoryItem[] = [
- {
- query: { refId: '1', expr: '{app="foo"}' },
- ts: 1,
- },
- ];
- const result = await instance.provideCompletionItems(input, { history });
- expect(result.context).toBeUndefined();
- expect(result.suggestions.length).toEqual(2);
- expect(result.suggestions[0].label).toEqual('History');
- expect(result.suggestions[1].label).toEqual('Functions');
- });
- it('returns pipe operations on pipe context', async () => {
- const instance = new LanguageProvider(datasource);
- const input = createTypeaheadInput('{app="test"} | ', ' ', '', 15, ['context-pipe']);
- const result = await instance.provideCompletionItems(input);
- expect(result.context).toBeUndefined();
- expect(result.suggestions.length).toEqual(2);
- expect(result.suggestions[0].label).toEqual('Operators');
- expect(result.suggestions[1].label).toEqual('Parsers');
- });
- });
- describe('fetchSeries', () => {
- it('should use match[] parameter', () => {
- const datasource = makeMockLokiDatasource({}, { '{foo="bar"}': [{ label1: 'label_val1' }] });
- const languageProvider = new LanguageProvider(datasource);
- const fetchSeries = languageProvider.fetchSeries;
- const requestSpy = jest.spyOn(languageProvider, 'request');
- fetchSeries('{job="grafana"}');
- expect(requestSpy).toHaveBeenCalledWith('series', {
- end: 1560163909000,
- 'match[]': '{job="grafana"}',
- start: 1560153109000,
- });
- });
- });
- describe('fetchSeriesLabels', () => {
- it('should interpolate variable in series', () => {
- const datasource: LokiDatasource = {
- metadataRequest: () => ({ data: { data: [] as any[] } }),
- getTimeRangeParams: () => ({ start: 0, end: 1 }),
- interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
- } as any as LokiDatasource;
- const languageProvider = new LanguageProvider(datasource);
- const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
- const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue([]);
- fetchSeriesLabels('$stream');
- expect(requestSpy).toHaveBeenCalled();
- expect(requestSpy).toHaveBeenCalledWith('series', {
- end: 1,
- 'match[]': 'interpolated-stream',
- start: 0,
- });
- });
- });
- describe('label key suggestions', () => {
- it('returns all label suggestions on empty selector', async () => {
- const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const input = createTypeaheadInput('{}', '', '', 1);
- const result = await provider.provideCompletionItems(input);
- expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([
- {
- items: [
- { label: 'label1', filterText: '"label1"' },
- { label: 'label2', filterText: '"label2"' },
- ],
- label: 'Labels',
- },
- ]);
- });
- it('returns all label suggestions on selector when starting to type', async () => {
- const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const input = createTypeaheadInput('{l}', '', '', 2);
- const result = await provider.provideCompletionItems(input);
- expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([
- {
- items: [
- { label: 'label1', filterText: '"label1"' },
- { label: 'label2', filterText: '"label2"' },
- ],
- label: 'Labels',
- },
- ]);
- });
- });
- describe('label suggestions facetted', () => {
- it('returns facetted label suggestions based on selector', async () => {
- const datasource = makeMockLokiDatasource(
- { label1: [], label2: [] },
- { '{foo="bar"}': [{ label1: 'label_val1' }] }
- );
- const provider = await getLanguageProvider(datasource);
- const input = createTypeaheadInput('{foo="bar",}', '', '', 11);
- const result = await provider.provideCompletionItems(input);
- expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([{ items: [{ label: 'label1' }], label: 'Labels' }]);
- });
- it('returns facetted label suggestions for multipule selectors', async () => {
- const datasource = makeMockLokiDatasource(
- { label1: [], label2: [] },
- { '{baz="42",foo="bar"}': [{ label2: 'label_val2' }] }
- );
- const provider = await getLanguageProvider(datasource);
- const input = createTypeaheadInput('{baz="42",foo="bar",}', '', '', 20);
- const result = await provider.provideCompletionItems(input);
- expect(result.context).toBe('context-labels');
- expect(result.suggestions).toEqual([{ items: [{ label: 'label2' }], label: 'Labels' }]);
- });
- });
- describe('label suggestions', () => {
- it('returns label values suggestions from Loki', async () => {
- const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const input = createTypeaheadInput('{label1=}', '=', 'label1');
- let result = await provider.provideCompletionItems(input);
- result = await provider.provideCompletionItems(input);
- expect(result.context).toBe('context-label-values');
- expect(result.suggestions).toEqual([
- {
- items: [
- { label: 'label1_val1', filterText: '"label1_val1"' },
- { label: 'label1_val2', filterText: '"label1_val2"' },
- ],
- label: 'Label values for "label1"',
- },
- ]);
- });
- it('returns label values suggestions from Loki when re-editing', async () => {
- const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const input = createTypeaheadInput('{label1="label1_v"}', 'label1_v', 'label1', 17, [
- 'attr-value',
- 'context-labels',
- ]);
- let result = await provider.provideCompletionItems(input);
- expect(result.context).toBe('context-label-values');
- expect(result.suggestions).toEqual([
- {
- items: [
- { label: 'label1_val1', filterText: '"label1_val1"' },
- { label: 'label1_val2', filterText: '"label1_val2"' },
- ],
- label: 'Label values for "label1"',
- },
- ]);
- });
- });
- describe('label values', () => {
- it('should fetch label values if not cached', async () => {
- const datasource = makeMockLokiDatasource({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const requestSpy = jest.spyOn(provider, 'request');
- const labelValues = await provider.fetchLabelValues('testkey');
- expect(requestSpy).toHaveBeenCalled();
- expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
- });
- it('should return cached values', async () => {
- const datasource = makeMockLokiDatasource({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const requestSpy = jest.spyOn(provider, 'request');
- const labelValues = await provider.fetchLabelValues('testkey');
- expect(requestSpy).toHaveBeenCalledTimes(1);
- expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
- const nextLabelValues = await provider.fetchLabelValues('testkey');
- expect(requestSpy).toHaveBeenCalledTimes(1);
- expect(nextLabelValues).toEqual(['label1_val1', 'label1_val2']);
- });
- it('should encode special characters', async () => {
- const datasource = makeMockLokiDatasource({ '`\\"testkey': ['label1_val1', 'label1_val2'], label2: [] });
- const provider = await getLanguageProvider(datasource);
- const requestSpy = jest.spyOn(provider, 'request');
- await provider.fetchLabelValues('`\\"testkey');
- expect(requestSpy).toHaveBeenCalledWith('label/%60%5C%22testkey/values', expect.any(Object));
- });
- });
- });
- describe('Request URL', () => {
- it('should contain range params', async () => {
- const datasourceWithLabels = makeMockLokiDatasource({ other: [] });
- const rangeParams = datasourceWithLabels.getTimeRangeParams();
- const datasourceSpy = jest.spyOn(datasourceWithLabels as any, 'metadataRequest');
- const instance = new LanguageProvider(datasourceWithLabels);
- instance.fetchLabels();
- const expectedUrl = 'labels';
- expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeParams);
- });
- });
- describe('Query imports', () => {
- const datasource = makeMockLokiDatasource({});
- it('returns empty queries', async () => {
- const instance = new LanguageProvider(datasource);
- const result = await instance.importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
- expect(result).toEqual({ refId: 'bar', expr: '', queryType: LokiQueryType.Range });
- });
- describe('exporting to abstract query', () => {
- it('exports labels', async () => {
- const instance = new LanguageProvider(datasource);
- const abstractQuery = instance.exportToAbstractQuery({
- refId: 'bar',
- expr: '{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
- instant: true,
- range: false,
- });
- expect(abstractQuery).toMatchObject({
- refId: 'bar',
- labelMatchers: [
- { name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
- { name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
- { name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
- { name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
- ],
- });
- });
- });
- });
- async function getLanguageProvider(datasource: LokiDatasource) {
- const instance = new LanguageProvider(datasource);
- await instance.start();
- return instance;
- }
- /**
- * @param value Value of the full input
- * @param text Last piece of text (not sure but in case of {label=} this would be just '=')
- * @param labelKey Label by which to search for values. Cutting corners a bit here as this should be inferred from value
- */
- function createTypeaheadInput(
- value: string,
- text: string,
- labelKey?: string,
- anchorOffset?: number,
- wrapperClasses?: string[],
- instance?: LanguageProvider
- ): TypeaheadInput {
- const deserialized = Plain.deserialize(value);
- const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1));
- const valueWithSelection = deserialized.setSelection(range);
- return {
- text,
- prefix: instance ? instance.cleanText(text) : '',
- wrapperClasses: wrapperClasses || ['context-labels'],
- value: valueWithSelection,
- labelKey,
- };
- }
|