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

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL][Discover] Fixes CSV export with named params
(#206914)](https://github.com/elastic/kibana/pull/206914)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Stratoula
Kalafateli","email":"efstratia.kalafateli@elastic.co"},"sourceCommit":{"committedDate":"2025-01-17T06:47:46Z","message":"[ES|QL][Discover]
Fixes CSV export with named params (#206914)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/206719\r\n\r\nAllows the csv
report to get generated when there are time named
params\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"385d2c115431952a4d8f5bc3ea851e00dc783c89","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v9.0.0","Team:DataDiscovery","Feature:ES|QL","backport:version","v8.18.0"],"title":"[ES|QL][Discover]
Fixes CSV export with named
params","number":206914,"url":"https://github.com/elastic/kibana/pull/206914","mergeCommit":{"message":"[ES|QL][Discover]
Fixes CSV export with named params (#206914)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/206719\r\n\r\nAllows the csv
report to get generated when there are time named
params\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"385d2c115431952a4d8f5bc3ea851e00dc783c89"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206914","number":206914,"mergeCommit":{"message":"[ES|QL][Discover]
Fixes CSV export with named params (#206914)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/206719\r\n\r\nAllows the csv
report to get generated when there are time named
params\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"385d2c115431952a4d8f5bc3ea851e00dc783c89"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2025-01-17 10:40:57 +02:00 committed by GitHub
parent 974347d038
commit 0363703f9a
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 };
@ -107,7 +104,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,
{
@ -139,7 +136,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -167,7 +164,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -197,7 +194,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -287,7 +284,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsvPromise = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfigWithAutoScrollDuration,
taskInstanceFields,
{
@ -363,7 +360,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsvPromise = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfigWithAutoScrollDuration,
taskInstanceFields,
{
@ -414,7 +411,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob({ columns: ['message', 'date', 'something else'] }),
createMockJob({ query: { esql: '' }, columns: ['message', 'date', 'something else'] }),
mockConfig,
mockTaskInstanceFields,
{
@ -485,7 +482,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,
},
},
{
@ -509,7 +579,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -539,7 +609,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -578,7 +648,7 @@ describe('CsvESQLGenerator', () => {
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -607,7 +677,7 @@ describe('CsvESQLGenerator', () => {
throw new Error('An unknown error');
});
const generateCsv = new CsvESQLGenerator(
createMockJob(),
createMockJob({ query: { esql: '' } }),
mockConfig,
mockTaskInstanceFields,
{
@ -644,7 +714,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

@ -30,5 +30,6 @@
"@kbn/es-types",
"@kbn/data-views-plugin",
"@kbn/search-types",
"@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';