[ES|QL][Discover] Fixes CSV export with named params (#206914)

## Summary

Closes https://github.com/elastic/kibana/issues/206719

Allows the csv report to get generated when there are time named params

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2025-01-17 08:47:46 +02:00 committed by GitHub
parent 75e1866915
commit 385d2c1154
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 125 additions and 24 deletions

View file

@ -32,11 +32,8 @@ import {
} from '../constants';
import { CsvESQLGenerator, JobParamsCsvESQL } from './generate_csv_esql';
const createMockJob = (
params: Partial<JobParamsCsvESQL> = { query: { esql: '' } }
): JobParamsCsvESQL => ({
const createMockJob = (params: JobParamsCsvESQL): JobParamsCsvESQL => ({
...params,
query: { esql: '' },
});
const mockTaskInstanceFields = { startedAt: null, retryAt: null };
@ -106,7 +103,7 @@ describe('CsvESQLGenerator', () => {
it('formats an empty search result to CSV content', async () => {
const generateCsv = new CsvESQLGenerator(
createMockJob({ columns: ['date', 'ip', 'message'] }),
createMockJob({ query: { esql: '' }, columns: ['date', 'ip', 'message'] }),
mockConfig,
mockTaskInstanceFields,
{
@ -138,7 +135,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -166,7 +163,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -196,7 +193,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -286,7 +283,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsvPromise = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfigWithAutoScrollDuration,
taskInstanceFields,
{
@ -362,7 +359,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsvPromise = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfigWithAutoScrollDuration,
taskInstanceFields,
{
@ -413,7 +410,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob({ columns: ['message', 'date', 'something else'] }),
createMockJob({ query: { esql: '' }, columns: ['message', 'date', 'something else'] }),
mockConfig,
mockTaskInstanceFields,
{
@ -484,7 +481,80 @@ describe('CsvESQLGenerator', () => {
},
},
locale: 'en',
query: '',
query: query.esql,
},
},
{
strategy: 'esql',
transport: {
requestTimeout: '30s',
},
abortSignal: expect.any(AbortSignal),
}
);
});
it('passes params to the query', async () => {
const query = {
esql: 'FROM custom-metrics-without-timestamp | WHERE event.ingested >= ?_tstart AND event.ingested <= ?_tend',
};
const filters = [
{
meta: {},
query: {
range: {
'event.ingested': { format: 'strict_date_optional_time', gte: 'now-15m', lte: 'now' },
},
},
},
];
const generateCsv = new CsvESQLGenerator(
createMockJob({ query, filters }),
mockConfig,
mockTaskInstanceFields,
{
es: mockEsClient,
data: mockDataClient,
uiSettings: uiSettingsClient,
},
new CancellationToken(),
mockLogger,
stream
);
await generateCsv.generateData();
expect(mockDataClient.search).toHaveBeenCalledWith(
{
params: {
filter: {
bool: {
filter: [
{
range: {
'event.ingested': {
format: 'strict_date_optional_time',
gte: 'now-15m',
lte: 'now',
},
},
},
],
must: [],
must_not: [],
should: [],
},
},
params: expect.arrayContaining([
expect.objectContaining({
_tstart: expect.any(String),
}),
expect.objectContaining({
_tend: expect.any(String),
}),
]),
locale: 'en',
query: query.esql,
},
},
{
@ -508,7 +578,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -538,7 +608,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -576,7 +646,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -605,7 +675,7 @@ describe('CsvESQLGenerator', () => {
throw new Error('An unknown error');
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -642,7 +712,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{

View file

@ -14,7 +14,8 @@ import type { IScopedClusterClient, IUiSettingsClient, Logger } from '@kbn/core/
import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types';
import { ESQL_SEARCH_STRATEGY, cellHasFormulas, getEsQueryConfig } from '@kbn/data-plugin/common';
import type { IScopedSearchClient } from '@kbn/data-plugin/server';
import { type Filter, buildEsQuery } from '@kbn/es-query';
import { type Filter, buildEsQuery, extractTimeRange } from '@kbn/es-query';
import { getTimeFieldFromESQLQuery, getStartEndParams } from '@kbn/esql-utils';
import type { ESQLSearchParams, ESQLSearchResponse } from '@kbn/es-types';
import { i18n } from '@kbn/i18n';
import {
@ -76,6 +77,17 @@ export class CsvESQLGenerator {
const { maxSizeBytes, bom, escapeFormulaValues } = settings;
const builder = new MaxSizeStringBuilder(this.stream, byteSizeValueToNumber(maxSizeBytes), bom);
// it will return undefined if there are no _tstart, _tend named params in the query
const timeFieldName = getTimeFieldFromESQLQuery(this.job.query.esql);
const params = [];
if (timeFieldName && this.job.filters) {
const { timeRange } = extractTimeRange(this.job.filters, timeFieldName);
if (timeRange) {
const namedParams = getStartEndParams(this.job.query.esql, timeRange);
params.push(...namedParams);
}
}
const filter =
this.job.filters &&
buildEsQuery(
@ -91,6 +103,7 @@ export class CsvESQLGenerator {
filter,
// locale can be used for number/date formatting
locale: i18n.getLocale(),
...(params.length ? { params } : {}),
// TODO: time_zone support was temporarily removed from ES|QL,
// we will need to add it back in once it is supported again.
// https://github.com/elastic/elasticsearch/pull/102767

View file

@ -31,5 +31,6 @@
"@kbn/data-views-plugin",
"@kbn/search-types",
"@kbn/task-manager-plugin",
"@kbn/esql-utils",
]
}

View file

@ -24,4 +24,15 @@ describe('convertRangeFilterToTimeRange', () => {
expect(convertedRangeFilter).toEqual(filterAfterConvertedRangeFilter);
});
it('should return converted range for relative dates', () => {
const filter: any = { query: { range: { '@timestamp': { gte: 'now-1d', lte: 'now' } } } };
const filterAfterConvertedRangeFilter = {
from: 'now-1d',
to: 'now',
};
const convertedRangeFilter = convertRangeFilterToTimeRange(filter);
expect(convertedRangeFilter).toEqual(filterAfterConvertedRangeFilter);
});
});

View file

@ -12,20 +12,26 @@ import { keys } from 'lodash';
import type { RangeFilter } from '../build_filters';
import type { TimeRange } from './types';
const isRelativeTime = (value: string | number | undefined): boolean => {
return typeof value === 'string' && value.includes('now');
};
export function convertRangeFilterToTimeRange(filter: RangeFilter) {
const key = keys(filter.query.range)[0];
const values = filter.query.range[key];
const from = values.gt || values.gte;
const to = values.lt || values.lte;
return {
from: moment(values.gt || values.gte),
to: moment(values.lt || values.lte),
from: from && isRelativeTime(from) ? String(from) : moment(from),
to: to && isRelativeTime(to) ? String(to) : moment(to),
};
}
export function convertRangeFilterToTimeRangeString(filter: RangeFilter): TimeRange {
const { from, to } = convertRangeFilterToTimeRange(filter);
return {
from: from?.toISOString(),
to: to?.toISOString(),
from: moment.isMoment(from) ? from?.toISOString() : from,
to: moment.isMoment(to) ? to?.toISOString() : to,
};
}

View file

@ -22,8 +22,8 @@ export interface TimefilterConfig {
export type InputTimeRange =
| TimeRange
| {
from: Moment;
to: Moment;
from: Moment | string;
to: Moment | string;
};
export type { TimeRangeBounds } from '../../../common';