Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;