Skip to content

Commit

Permalink
feat(vis_type_vega): support reading time field
Browse files Browse the repository at this point in the history
Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl committed Jan 7, 2025
1 parent 15a19fa commit a4be69c
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 8 deletions.
59 changes: 55 additions & 4 deletions src/plugins/vis_type_vega/public/data_model/ppl_parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
* 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 = {
search: jest.fn(() => ({
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();
Expand All @@ -23,21 +26,69 @@ 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(() =>
Promise.resolve([{ name: 'request name', rawResponse: { jsonData: [{ id: 'id1' }] } }])
),
})),
};
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: {
Expand Down
32 changes: 29 additions & 3 deletions src/plugins/vis_type_vega/public/data_model/ppl_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand All @@ -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(' | ')}`;

Check warning on line 35 in src/plugins/vis_type_vega/public/data_model/ppl_parser.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/vis_type_vega/public/data_model/ppl_parser.ts#L35

Added line #L35 was not covered by tests
}
return `${source.trim()} | ${timeFilter}`;
}
return query;

Check warning on line 39 in src/plugins/vis_type_vega/public/data_model/ppl_parser.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/vis_type_vega/public/data_model/ppl_parser.ts#L39

Added line #L39 was not covered by tests
}

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(
Expand All @@ -34,6 +55,11 @@ export class PPLQueryParser {
);
}

if (timefield) {
const query = this.injectTimeFilter(url.body.query, timefield);
url.body.query = query;
}

return { dataObject, url };
}

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/vis_type_vega/public/data_model/vega_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down

0 comments on commit a4be69c

Please sign in to comment.