Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ import { CONTENT_TYPE_CSV } from '../constants';
import { type CsvExportSettings, getExportSettings } from './lib/get_export_settings';
import { i18nTexts } from './lib/i18n_texts';
import { MaxSizeStringBuilder } from './lib/max_size_string_builder';
import { overrideTimeRange } from './lib/override_time_range';

export interface JobParamsCsvESQL {
query: { esql: string };
columns?: string[];
filters?: Filter[];
browserTimezone?: string;
forceNow?: string;
timeFieldName?: string;
}

interface Clients {
Expand Down Expand Up @@ -85,12 +88,34 @@ export class CsvESQLGenerator {
}
}

let currentFilters = this.job.filters;
if (this.job.forceNow) {
this.logger.debug(`Overriding time range filter using forceNow: ${this.job.forceNow}`, {
tags: [this.jobId],
});
this.logger.debug(() => `Current filters: ${JSON.stringify(currentFilters)}`, {
tags: [this.jobId],
});
const updatedFilters = overrideTimeRange({
currentFilters,
forceNow: this.job.forceNow,
logger: this.logger,
timeFieldName: this.job.timeFieldName,
});
this.logger.debug(() => `Updated filters: ${JSON.stringify(updatedFilters)}`, {
tags: [this.jobId],
});
if (updatedFilters) {
currentFilters = updatedFilters;
}
}

const filter =
this.job.filters &&
currentFilters &&
buildEsQuery(
undefined,
[],
this.job.filters,
currentFilters,
getEsQueryConfig(this.clients.uiSettings as Parameters<typeof getEsQueryConfig>[0])
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,42 @@ describe('overrideTimeRange', () => {
expect(updated).toBeUndefined();
});

it('should use timeFieldName if no meta field found', () => {
const filter = [
{
query: {
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2025-01-01T19:38:24.286Z',
lte: '2025-01-01T20:03:24.286Z',
},
},
},
},
];

const updated = overrideTimeRange({
// @ts-expect-error missing meta field
currentFilters: filter,
forceNow: '2025-06-18T19:55:00.000Z',
timeFieldName: '@timestamp',
});
expect(updated).toEqual([
{
query: {
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2025-06-18T19:30:00.000Z',
lte: '2025-06-18T19:55:00.000Z',
},
},
},
},
]);
});

it('should return undefined if invalid time', () => {
const filter = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ import { set } from '@kbn/safer-lodash-set';
import type { Logger } from '@kbn/core/server';
import { cloneDeep, get, has, isArray } from 'lodash';

interface TimeFields {
metaField?: string;
timeFormat?: string;
timeGte?: string;
timeLte?: string;
}
const getTimeFieldAccessorString = (metaField: string): string => `query.range['${metaField}']`;
const getTimeFields = (filter: Filter) => {
const metaField = get(filter, 'meta.field');
const getTimeFields = (filter: Filter, timeFieldName?: string): TimeFields => {
const metaField: string | undefined = get(filter, 'meta.field') || timeFieldName;
if (metaField) {
const timeFieldAccessorString = getTimeFieldAccessorString(metaField);
const timeFormat = get(filter, `${timeFieldAccessorString}.format`);
Expand All @@ -36,11 +42,13 @@ interface OverrideTimeRangeOpts {
currentFilters: Filter[] | Filter | undefined;
forceNow: string;
logger: Logger;
timeFieldName?: string;
}
export const overrideTimeRange = ({
currentFilters,
forceNow,
logger,
timeFieldName,
}: OverrideTimeRangeOpts): Filter[] | undefined => {
if (!currentFilters) {
return;
Expand Down Expand Up @@ -77,7 +85,7 @@ export const overrideTimeRange = ({
timeFormat: maybeTimeFieldFormat,
timeGte: maybeTimeFieldGte,
timeLte: maybeTimeFieldLte,
} = getTimeFields(filter);
} = getTimeFields(filter, timeFieldName);

if (maybeTimeFieldFormat && maybeTimeFieldGte && maybeTimeFieldLte) {
return isValidDateTime(maybeTimeFieldGte) && isValidDateTime(maybeTimeFieldLte);
Expand All @@ -88,8 +96,8 @@ export const overrideTimeRange = ({
if (timeFilterIndex >= 0) {
try {
const timeFilter = cloneDeep(filters[timeFilterIndex]);
const { metaField, timeGte, timeLte } = getTimeFields(timeFilter);
if (metaField) {
const { metaField, timeGte, timeLte } = getTimeFields(timeFilter, timeFieldName);
if (metaField && timeGte && timeLte) {
const timeGteMs = Date.parse(timeGte);
const timeLteMs = Date.parse(timeLte);
const timeDiffMs = timeLteMs - timeGteMs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class CsvV2ExportType extends ExportType<
// this should be addressed here https://github.com/elastic/kibana/issues/151190
// const columns = await locatorClient.columnsFromLocator(params);
const columns = params.columns as string[] | undefined;
const timeFieldName = await locatorClient.timeFieldNameFromLocator(params);
const filters = await locatorClient.filtersFromLocator(params);
const es = this.startDeps.esClient.asScoped(request);

Expand All @@ -141,6 +142,7 @@ export class CsvV2ExportType extends ExportType<
columns,
query,
filters,
timeFieldName,
...job,
},
csvConfig,
Expand Down
2 changes: 2 additions & 0 deletions src/platform/plugins/shared/discover/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
TitleFromLocatorFn,
QueryFromLocatorFn,
FiltersFromLocatorFn,
TimeFieldNameFromLocatorFn,
} from './locator';

export interface DiscoverServerPluginStartDeps {
Expand All @@ -27,6 +28,7 @@ export interface LocatorServiceScopedClient {
titleFromLocator: TitleFromLocatorFn;
queryFromLocator: QueryFromLocatorFn;
filtersFromLocator: FiltersFromLocatorFn;
timeFieldNameFromLocator: TimeFieldNameFromLocatorFn;
}

export interface DiscoverServerPluginLocatorService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type { SearchSourceFromLocatorFn } from './searchsource_from_locator';
export type { TitleFromLocatorFn } from './title_from_locator';
export type { QueryFromLocatorFn } from './query_from_locator';
export type { FiltersFromLocatorFn } from './filters_from_locator';
export type { TimeFieldNameFromLocatorFn } from './time_field_name_from_locator';

/**
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const createLocatorServiceMock = (): DiscoverServerPluginLocatorService =
.fn<Promise<Filter[]>, [DiscoverAppLocatorParams]>()
.mockResolvedValue([]);

const timeFieldNameFromLocatorMock = jest
.fn<Promise<string | undefined>, [DiscoverAppLocatorParams]>()
.mockResolvedValue('@timestamp');

return {
asScopedClient: jest
.fn<Promise<LocatorServiceScopedClient>, [req: KibanaRequest]>()
Expand All @@ -47,6 +51,7 @@ export const createLocatorServiceMock = (): DiscoverServerPluginLocatorService =
titleFromLocator: titleFromLocatorMock,
queryFromLocator: queryFromLocatorMock,
filtersFromLocator: filtersFromLocatorMock,
timeFieldNameFromLocator: timeFieldNameFromLocatorMock,
} as LocatorServiceScopedClient);
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { searchSourceFromLocatorFactory } from './searchsource_from_locator';
import { titleFromLocatorFactory } from './title_from_locator';
import { queryFromLocatorFactory } from './query_from_locator';
import { filtersFromLocatorFactory } from './filters_from_locator';
import { timeFieldNameFromLocatorFactory } from './time_field_name_from_locator';

export const getScopedClient = (
core: CoreStart,
Expand All @@ -32,6 +33,7 @@ export const getScopedClient = (
titleFromLocator: titleFromLocatorFactory(services),
queryFromLocator: queryFromLocatorFactory(services),
filtersFromLocator: filtersFromLocatorFactory(services),
timeFieldNameFromLocator: timeFieldNameFromLocatorFactory(services),
};
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { IUiSettingsClient, SavedObjectsClientContract } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import type { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { dataPluginMock } from '@kbn/data-plugin/server/mocks';
import type { LocatorServicesDeps as Services } from '.';
import { timeFieldNameFromLocatorFactory } from './time_field_name_from_locator';

const coreStart = coreMock.createStart();
let uiSettingsClient: IUiSettingsClient;
let soClient: SavedObjectsClientContract;
let searchSourceStart: ISearchStartSearchSource;
let mockServices: Services;

beforeAll(async () => {
const dataStartMock = dataPluginMock.createStartContract();
const request = httpServerMock.createKibanaRequest();
soClient = coreStart.savedObjects.getScopedClient(request);
uiSettingsClient = coreMock.createStart().uiSettings.asScopedToClient(soClient);
searchSourceStart = await dataStartMock.search.searchSource.asScoped(request);

mockServices = {
searchSourceStart,
savedObjects: soClient,
uiSettings: uiSettingsClient,
};
});

test(`returns timeFieldName from DiscoverAppLocatorParams`, async () => {
const params = { dataViewSpec: { timeFieldName: '@timestamp' } };
const timeFieldNameFromLocatorFn = timeFieldNameFromLocatorFactory(mockServices);
const timeField = await timeFieldNameFromLocatorFn(params);
expect(timeField).toBe('@timestamp');
});

test(`returns undefined if there is no timeFieldName in DiscoverAppLocatorParams`, async () => {
const params = { dataViewSpec: {} };
const timeFieldNameFromLocatorFn = timeFieldNameFromLocatorFactory(mockServices);
const timeField = await timeFieldNameFromLocatorFn(params);
expect(timeField).toBeUndefined();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { LocatorServicesDeps } from '.';
import type { DiscoverAppLocatorParams } from '../../common';

/**
* @internal
*/
export const timeFieldNameFromLocatorFactory = (services: LocatorServicesDeps) => {
/**
* @public
*/
const timeFieldNameFromLocator = async (
params: DiscoverAppLocatorParams
): Promise<string | undefined> => {
return params.dataViewSpec?.timeFieldName;
};

return timeFieldNameFromLocator;
};

export type TimeFieldNameFromLocatorFn = ReturnType<typeof timeFieldNameFromLocatorFactory>;