diff --git a/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts b/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts index f4ee80ecbe14..acb890e96ddb 100644 --- a/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts +++ b/src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { timefilterServiceMock } from '../../../data/public/query/timefilter/timefilter_service.mock'; import { PPLQueryParser } from './ppl_parser'; +import { TimeCache } from './time_cache'; test('it should throw error if with invalid url object', () => { const searchApiMock = { @@ -11,7 +13,8 @@ test('it should throw error if with invalid url object', () => { toPromise: jest.fn(() => Promise.resolve({})), })), }; - const parser = new PPLQueryParser(searchApiMock); + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + const parser = new PPLQueryParser(timeCache, searchApiMock); expect(() => parser.parseUrl({}, {})).toThrowError(); expect(() => parser.parseUrl({}, { body: {} })).toThrowError(); expect(() => parser.parseUrl({}, { body: { query: {} } })).toThrowError(); @@ -23,13 +26,60 @@ test('it should parse url object', () => { toPromise: jest.fn(() => Promise.resolve({})), })), }; - const parser = new PPLQueryParser(searchApiMock); + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + const parser = new PPLQueryParser(timeCache, searchApiMock); const result = parser.parseUrl({}, { body: { query: 'source=test_index' } }); expect(result.dataObject).toEqual({}); expect(result.url).toEqual({ body: { query: 'source=test_index' } }); }); -it('should populate data to request', async () => { +test('it should parse url object with %timefield% with injecting time filter to ppl query', () => { + const from = new Date('2024-10-07T05:03:22.548Z'); + const to = new Date('2025-01-08T05:03:30.981Z'); + jest + .spyOn(TimeCache.prototype, 'getTimeBounds') + .mockReturnValue({ max: from.valueOf(), min: to.valueOf() }); + + const searchApiMock = { + search: jest.fn(() => ({ + toPromise: jest.fn(() => Promise.resolve({})), + })), + }; + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + timeCache.setTimeRange({ + from: from.toISOString(), + to: to.toISOString(), + mode: 'absolute', + }); + + const parser = new PPLQueryParser(timeCache, searchApiMock); + const result1 = parser.parseUrl( + {}, + { body: { query: 'source=test_index' }, '%timefield%': 'timestamp' } + ); + expect(result1.url).toEqual({ + body: { + query: + "source=test_index | where `timestamp` >= '2025-01-08 13:03:30.981' and `timestamp` <= '2024-10-07 13:03:22.548'", + }, + }); + + const result2 = parser.parseUrl( + {}, + { + body: { query: 'source=test_index | stats count() as doc_count' }, + '%timefield%': 'timestamp', + } + ); + expect(result2.url).toEqual({ + body: { + query: + "source=test_index | where `timestamp` >= '2025-01-08 13:03:30.981' and `timestamp` <= '2024-10-07 13:03:22.548' | stats count() as doc_count", + }, + }); +}); + +test('it should populate data to request', async () => { const searchApiMock = { search: jest.fn(() => ({ toPromise: jest.fn(() => @@ -37,7 +87,8 @@ it('should populate data to request', async () => { ), })), }; - const parser = new PPLQueryParser(searchApiMock); + const timeCache = new TimeCache(timefilterServiceMock.createStartContract().timefilter, 100); + const parser = new PPLQueryParser(timeCache, searchApiMock); const request = { url: { body: { query: 'source=test_index' } }, dataObject: { diff --git a/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts b/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts index 8babcfc6e387..323cde0f83b5 100644 --- a/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/ppl_parser.ts @@ -4,8 +4,13 @@ */ import { i18n } from '@osd/i18n'; +import moment from 'moment'; + import { Data, UrlObject, PPLQueryRequest } from './types'; import { SearchAPI } from './search_api'; +import { TimeCache } from './time_cache'; + +const TIMEFIELD = '%timefield%'; const getRequestName = (request: PPLQueryRequest, index: number) => request.dataObject.name || @@ -15,13 +20,29 @@ const getRequestName = (request: PPLQueryRequest, index: number) => }); export class PPLQueryParser { - searchAPI: SearchAPI; - - constructor(searchAPI: SearchAPI) { + constructor(private readonly timeCache: TimeCache, private readonly searchAPI: SearchAPI) { this.searchAPI = searchAPI; } + injectTimeFilter(query: string, timefield: string) { + if (this.timeCache._timeRange) { + const [source, ...others] = query.split('|'); + const bounds = this.timeCache.getTimeBounds(); + const from = moment(bounds.min).format('YYYY-MM-DD HH:mm:ss.SSS'); + const to = moment(bounds.max).format('YYYY-MM-DD HH:mm:ss.SSS'); + const timeFilter = `where \`${timefield}\` >= '${from}' and \`${timefield}\` <= '${to}'`; + if (others.length > 0) { + return `${source.trim()} | ${timeFilter} | ${others.map((s) => s.trim()).join(' | ')}`; + } + return `${source.trim()} | ${timeFilter}`; + } + return query; + } + parseUrl(dataObject: Data, url: UrlObject) { + const timefield = url[TIMEFIELD]; + delete url[TIMEFIELD]; + // data.url.body.query must be defined if (!url.body || !url.body.query || typeof url.body.query !== 'string') { throw new Error( @@ -34,6 +55,11 @@ export class PPLQueryParser { ); } + if (timefield) { + const query = this.injectTimeFilter(url.body.query, timefield); + url.body.query = query; + } + return { dataObject, url }; } diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 349e771c65b1..becfa8f32b6e 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -656,7 +656,7 @@ The URL is an identifier only. OpenSearch Dashboards and your browser will never opensearch: new OpenSearchQueryParser(this.timeCache, this.searchAPI, this.filters, onWarn), emsfile: new EmsFileParser(serviceSettings), url: new UrlParser(onWarn), - ppl: new PPLQueryParser(this.searchAPI), + ppl: new PPLQueryParser(this.timeCache, this.searchAPI), }; } const pending: PendingType = {};