diff --git a/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts b/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts index 0ec9f0c93a381..79a00d0a70ee0 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts @@ -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 { @@ -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[0]) ); diff --git a/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts index 577cac6f7aa5d..154fb23e6d482 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.test.ts @@ -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 = [ { diff --git a/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts index 25f24bfdf1248..2d732e3b3cd54 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/lib/override_time_range.ts @@ -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`); @@ -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; @@ -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); @@ -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; diff --git a/src/platform/packages/private/kbn-reporting/export_types/csv/csv_v2.ts b/src/platform/packages/private/kbn-reporting/export_types/csv/csv_v2.ts index ab765db7091ed..f6f8b12fd97bf 100644 --- a/src/platform/packages/private/kbn-reporting/export_types/csv/csv_v2.ts +++ b/src/platform/packages/private/kbn-reporting/export_types/csv/csv_v2.ts @@ -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); @@ -141,6 +142,7 @@ export class CsvV2ExportType extends ExportType< columns, query, filters, + timeFieldName, ...job, }, csvConfig, diff --git a/src/platform/plugins/shared/discover/server/index.ts b/src/platform/plugins/shared/discover/server/index.ts index 711d008f1e150..93182d9d298b9 100644 --- a/src/platform/plugins/shared/discover/server/index.ts +++ b/src/platform/plugins/shared/discover/server/index.ts @@ -15,6 +15,7 @@ import type { TitleFromLocatorFn, QueryFromLocatorFn, FiltersFromLocatorFn, + TimeFieldNameFromLocatorFn, } from './locator'; export interface DiscoverServerPluginStartDeps { @@ -27,6 +28,7 @@ export interface LocatorServiceScopedClient { titleFromLocator: TitleFromLocatorFn; queryFromLocator: QueryFromLocatorFn; filtersFromLocator: FiltersFromLocatorFn; + timeFieldNameFromLocator: TimeFieldNameFromLocatorFn; } export interface DiscoverServerPluginLocatorService { diff --git a/src/platform/plugins/shared/discover/server/locator/index.ts b/src/platform/plugins/shared/discover/server/locator/index.ts index 6dfd09302716f..5a5e423b6d2da 100644 --- a/src/platform/plugins/shared/discover/server/locator/index.ts +++ b/src/platform/plugins/shared/discover/server/locator/index.ts @@ -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 diff --git a/src/platform/plugins/shared/discover/server/locator/mocks.ts b/src/platform/plugins/shared/discover/server/locator/mocks.ts index 70febe2539c51..0aaba7428794b 100644 --- a/src/platform/plugins/shared/discover/server/locator/mocks.ts +++ b/src/platform/plugins/shared/discover/server/locator/mocks.ts @@ -37,6 +37,10 @@ export const createLocatorServiceMock = (): DiscoverServerPluginLocatorService = .fn, [DiscoverAppLocatorParams]>() .mockResolvedValue([]); + const timeFieldNameFromLocatorMock = jest + .fn, [DiscoverAppLocatorParams]>() + .mockResolvedValue('@timestamp'); + return { asScopedClient: jest .fn, [req: KibanaRequest]>() @@ -47,6 +51,7 @@ export const createLocatorServiceMock = (): DiscoverServerPluginLocatorService = titleFromLocator: titleFromLocatorMock, queryFromLocator: queryFromLocatorMock, filtersFromLocator: filtersFromLocatorMock, + timeFieldNameFromLocator: timeFieldNameFromLocatorMock, } as LocatorServiceScopedClient); }), }; diff --git a/src/platform/plugins/shared/discover/server/locator/service.ts b/src/platform/plugins/shared/discover/server/locator/service.ts index e58e6536213b2..b4af315197126 100644 --- a/src/platform/plugins/shared/discover/server/locator/service.ts +++ b/src/platform/plugins/shared/discover/server/locator/service.ts @@ -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, @@ -32,6 +33,7 @@ export const getScopedClient = ( titleFromLocator: titleFromLocatorFactory(services), queryFromLocator: queryFromLocatorFactory(services), filtersFromLocator: filtersFromLocatorFactory(services), + timeFieldNameFromLocator: timeFieldNameFromLocatorFactory(services), }; }, }; diff --git a/src/platform/plugins/shared/discover/server/locator/time_field_name_from_locator.test.ts b/src/platform/plugins/shared/discover/server/locator/time_field_name_from_locator.test.ts new file mode 100644 index 0000000000000..181fa9d29e00e --- /dev/null +++ b/src/platform/plugins/shared/discover/server/locator/time_field_name_from_locator.test.ts @@ -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(); +}); diff --git a/src/platform/plugins/shared/discover/server/locator/time_field_name_from_locator.ts b/src/platform/plugins/shared/discover/server/locator/time_field_name_from_locator.ts new file mode 100644 index 0000000000000..336999eeebe40 --- /dev/null +++ b/src/platform/plugins/shared/discover/server/locator/time_field_name_from_locator.ts @@ -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 => { + return params.dataViewSpec?.timeFieldName; + }; + + return timeFieldNameFromLocator; +}; + +export type TimeFieldNameFromLocatorFn = ReturnType;