mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
974347d038
commit
0363703f9a
6 changed files with 125 additions and 24 deletions
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -30,5 +30,6 @@
|
|||
"@kbn/es-types",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/search-types",
|
||||
"@kbn/esql-utils",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue