[Discover] Fix CSV for ES|QL embeddable (#216325)

- Closes https://github.com/elastic/kibana/issues/215893

## Summary

This PR extract the logic from Share > Export > Generate CSV into new
utils and uses it to fix CSV export for Dashboard panels.

<img width="903" alt="Screenshot 2025-04-01 at 12 05 58"
src="https://github.com/user-attachments/assets/20e611d7-b1da-4b50-a8fc-e18ac3db3a55"
/>


## Testing

Please test for both data view mode and ES|QL mode. The steps are the
following:
1. Save a discover session
2. Add it to a new dashboard
3. Save the Dashboard and switch to View mode
4. Via panel actions, press "Generate CSV report"
5. Compare the CSV results with what is shown in the grid
6. Add a custom time range to the panel via panel actions "Settings"
7. Generate a CSV report and compare results again

Also check that reporting still works when generated from Discover page.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2025-04-02 15:14:55 +02:00 committed by GitHub
parent cf1cd55a49
commit dc78614d29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 565 additions and 38 deletions

View file

@ -14,6 +14,7 @@ import type {
BasePayload,
BasePayloadV2,
CsvPagingStrategy,
LocatorParams,
} from '@kbn/reporting-common/types';
export * from './constants';
@ -23,7 +24,12 @@ interface BaseParamsCSV {
columns?: string[];
}
interface BaseParamsCsvV2 {
locatorParams: LocatorParams[];
}
export type JobParamsCSV = BaseParamsCSV & BaseParams;
export type JobParamsCsvV2 = BaseParamsCsvV2 & BaseParams;
export type TaskPayloadCSV = BaseParamsCSV & BasePayload;
/**
@ -33,6 +39,7 @@ export type TaskPayloadCSV = BaseParamsCSV & BasePayload;
* @public
*/
export type JobAppParamsCSV = Omit<JobParamsCSV, 'browserTimezone' | 'version'>;
export type JobAppParamsCsvV2 = Omit<JobParamsCsvV2, 'browserTimezone' | 'version'>;
interface CsvFromSavedObjectBase {
objectType: 'search';

View file

@ -16,7 +16,7 @@ import {
I18nStart,
NotificationsSetup,
} from '@kbn/core/public';
import { DataPublicPluginStart, SerializedSearchSourceFields } from '@kbn/data-plugin/public';
import { DataPublicPluginStart, type SerializedSearchSourceFields } from '@kbn/data-plugin/public';
import {
loadSharingDataHelpers,
SEARCH_EMBEDDABLE_TYPE,
@ -25,6 +25,7 @@ import {
HasTimeRange,
} from '@kbn/discover-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { DISCOVER_APP_LOCATOR, type DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
import {
apiCanAccessViewMode,
apiHasType,
@ -34,16 +35,24 @@ import {
EmbeddableApiContext,
getInheritedViewMode,
HasType,
type PublishesSavedObjectId,
PublishesTitle,
type PublishesUnifiedSearch,
} from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { CSV_REPORTING_ACTION, JobAppParamsCSV } from '@kbn/reporting-export-types-csv-common';
import { CSV_REPORTING_ACTION } from '@kbn/reporting-export-types-csv-common';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import type { UiActionsActionDefinition as ActionDefinition } from '@kbn/ui-actions-plugin/public';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import type { ClientConfigType } from '@kbn/reporting-public/types';
import { checkLicense } from '@kbn/reporting-public/license_check';
import {
getSearchCsvJobParams,
CsvSearchModeParams,
} from '@kbn/reporting-public/share/shared/get_search_csv_job_params';
import type { ReportingAPIClient } from '@kbn/reporting-public/reporting_api_client';
import { LocatorParams } from '@kbn/reporting-common/types';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { getI18nStrings } from './strings';
export interface PanelActionDependencies {
@ -75,8 +84,7 @@ interface Params {
}
interface ExecutionParams {
searchSource: SerializedSearchSourceFields;
columns: string[] | undefined;
searchModeParams: CsvSearchModeParams;
title: string;
analytics: AnalyticsServiceStart;
i18nStart: I18nStart;
@ -127,7 +135,16 @@ export class ReportingCsvPanelAction implements ActionDefinition<EmbeddableApiCo
public async getSharingData(savedSearch: SavedSearch) {
const [{ uiSettings }, { data }] = await firstValueFrom(this.startServices$);
const { getSharingData } = await loadSharingDataHelpers();
return await getSharingData(savedSearch.searchSource, savedSearch, { uiSettings, data });
return await getSharingData(
savedSearch.searchSource,
savedSearch,
{ uiSettings, data },
this.isEsqlMode(savedSearch)
);
}
private isEsqlMode(savedSearch: SavedSearch) {
return isOfAggregateQueryType(savedSearch.searchSource.getField('query'));
}
public isCompatible = async (context: EmbeddableApiContext) => {
@ -152,16 +169,15 @@ export class ReportingCsvPanelAction implements ActionDefinition<EmbeddableApiCo
private executeGenerate = async (params: ExecutionParams) => {
const [startServices] = await firstValueFrom(this.startServices$);
const { searchSource, columns, title } = params;
const csvJobParams = this.apiClient.getDecoratedJobParams<JobAppParamsCSV>({
searchSource,
columns,
const { searchModeParams, title } = params;
const { reportType, decoratedJobParams } = getSearchCsvJobParams({
apiClient: this.apiClient,
searchModeParams,
title,
objectType: 'search',
});
await this.apiClient
.createReportingJob('csv_searchsource', csvJobParams)
.createReportingJob(reportType, decoratedJobParams)
.then((job) => {
if (job) {
this.notifications.toasts.addSuccess({
@ -182,6 +198,21 @@ export class ReportingCsvPanelAction implements ActionDefinition<EmbeddableApiCo
});
};
private getDiscoverLocatorParamsForEsqlCSV = (
api: PublishesSavedSearch & Partial<PublishesSavedObjectId & PublishesUnifiedSearch>,
searchSourceFields: SerializedSearchSourceFields,
columns: string[]
): DiscoverAppLocatorParams => {
const savedObjectId = api.savedObjectId$?.getValue();
return {
...(savedObjectId ? { savedSearchId: savedObjectId } : {}),
query: searchSourceFields.query,
filters: searchSourceFields.parent?.filter, // time range filter
columns,
};
};
public execute = async (context: EmbeddableApiContext) => {
const { embeddable } = context;
@ -196,14 +227,34 @@ export class ReportingCsvPanelAction implements ActionDefinition<EmbeddableApiCo
}
const [{ i18n: i18nStart, analytics }] = await firstValueFrom(this.startServices$);
const title = embeddable.title$.getValue() ?? '';
const executionParamsCommon = { title, i18nStart, analytics };
const { columns, getSearchSource } = await this.getSharingData(savedSearch);
const searchSource = getSearchSource({
addGlobalTimeFilter: !embeddable.hasTimeRange(),
absoluteTime: true,
});
const title = embeddable.title$.getValue() ?? '';
const executionParams = { searchSource, columns, title, savedSearch, i18nStart, analytics };
return this.executeGenerate(executionParams);
if (this.isEsqlMode(savedSearch)) {
return this.executeGenerate({
...executionParamsCommon,
searchModeParams: {
isEsqlMode: true,
locatorParams: [
{
id: DISCOVER_APP_LOCATOR,
params: this.getDiscoverLocatorParamsForEsqlCSV(embeddable, searchSource, columns),
} as LocatorParams,
],
},
});
}
return this.executeGenerate({
...executionParamsCommon,
searchModeParams: { isEsqlMode: false, searchSource, columns },
});
};
}

View file

@ -27,5 +27,7 @@
"@kbn/react-kibana-mount",
"@kbn/reporting-public",
"@kbn/presentation-publishing",
"@kbn/reporting-common",
"@kbn/es-query",
]
}

View file

@ -11,12 +11,11 @@ import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/react-kibana-mount';
import React from 'react';
import { firstValueFrom } from 'rxjs';
import { CSV_JOB_TYPE, CSV_JOB_TYPE_V2 } from '@kbn/reporting-export-types-csv-common';
import type { SearchSourceFields } from '@kbn/data-plugin/common';
import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react';
import { ShareContext, ShareMenuItemV2 } from '@kbn/share-plugin/public';
import { LocatorParams } from '@kbn/reporting-common/types';
import { getSearchCsvJobParams, CsvSearchModeParams } from '../shared/get_search_csv_job_params';
import type { ExportModalShareOpts } from '.';
import { checkLicense } from '../..';
@ -31,35 +30,26 @@ export const reportingCsvShareProvider = ({
return [];
}
// only csv v2 supports esql (isTextBased) reports
// TODO: whole csv reporting should move to v2 https://github.com/elastic/kibana/issues/151190
const reportType = sharingData.isTextBased ? CSV_JOB_TYPE_V2 : CSV_JOB_TYPE;
const getSearchSource = sharingData.getSearchSource as ({
addGlobalTimeFilter,
absoluteTime,
}: {
addGlobalTimeFilter?: boolean;
absoluteTime?: boolean;
}) => SearchSourceFields;
}) => SerializedSearchSourceFields;
const jobParams = {
title: sharingData.title as string,
objectType,
};
const getJobParams = (forShareUrl?: boolean) => {
if (reportType === CSV_JOB_TYPE_V2) {
const getSearchModeParams = (forShareUrl?: boolean): CsvSearchModeParams => {
if (sharingData.isTextBased) {
// csv v2 uses locator params
return {
...jobParams,
locatorParams: sharingData.locatorParams as [Record<string, unknown>],
isEsqlMode: true,
locatorParams: sharingData.locatorParams as LocatorParams[],
};
}
// csv v1 uses search source and columns
return {
...jobParams,
isEsqlMode: false,
columns: sharingData.columns as string[] | undefined,
searchSource: getSearchSource({
addGlobalTimeFilter: true,
@ -78,7 +68,12 @@ export const reportingCsvShareProvider = ({
const capabilityHasCsvReporting = application.capabilities.discover_v2?.generateCsv === true;
const generateReportingJobCSV = ({ intl }: { intl: InjectedIntl }) => {
const decoratedJobParams = apiClient.getDecoratedJobParams(getJobParams());
const { reportType, decoratedJobParams } = getSearchCsvJobParams({
apiClient,
searchModeParams: getSearchModeParams(false),
title: sharingData.title as string,
});
return apiClient
.createReportingShareJob(reportType, decoratedJobParams)
.then(() => firstValueFrom(startServices$))
@ -135,10 +130,13 @@ export const reportingCsvShareProvider = ({
const reportingUrl = new URL(window.location.origin);
const relativePath = apiClient.getReportingPublicJobPath(
reportType,
apiClient.getDecoratedJobParams(getJobParams(true))
);
const { reportType, decoratedJobParams } = getSearchCsvJobParams({
apiClient,
searchModeParams: getSearchModeParams(true),
title: sharingData.title as string,
});
const relativePath = apiClient.getReportingPublicJobPath(reportType, decoratedJobParams);
const absoluteUrl = new URL(relativePath, window.location.href).toString();

View file

@ -0,0 +1,75 @@
/*
* 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 {
CSV_JOB_TYPE_V2,
CSV_JOB_TYPE,
JobAppParamsCSV,
JobAppParamsCsvV2,
} from '@kbn/reporting-export-types-csv-common';
import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { LocatorParams, BaseParams } from '@kbn/reporting-common/types';
import type { ReportingAPIClient } from '../../reporting_api_client';
export type CsvSearchModeParams =
| {
isEsqlMode: false;
searchSource: SerializedSearchSourceFields;
columns: string[] | undefined;
}
| {
isEsqlMode: true;
locatorParams: LocatorParams[];
};
interface GetSearchCsvJobParams {
apiClient: ReportingAPIClient;
searchModeParams: CsvSearchModeParams;
title: string;
}
export const getSearchCsvJobParams = ({
apiClient,
searchModeParams,
title,
}: GetSearchCsvJobParams): {
reportType: typeof CSV_JOB_TYPE_V2 | typeof CSV_JOB_TYPE;
decoratedJobParams: BaseParams;
} => {
// only csv v2 supports esql reports
// TODO: whole csv reporting should move to v2 https://github.com/elastic/kibana/issues/151190
const reportType = searchModeParams.isEsqlMode ? CSV_JOB_TYPE_V2 : CSV_JOB_TYPE;
const commonJobParams = {
title,
objectType: 'search',
};
if (searchModeParams.isEsqlMode) {
// csv v2 uses locator params
return {
reportType,
decoratedJobParams: apiClient.getDecoratedJobParams<JobAppParamsCsvV2>({
...commonJobParams,
locatorParams: searchModeParams.locatorParams,
}),
};
}
// csv v1 uses search source and columns
return {
reportType,
decoratedJobParams: apiClient.getDecoratedJobParams<JobAppParamsCSV>({
...commonJobParams,
columns: searchModeParams.columns,
searchSource: searchModeParams.searchSource,
}),
};
};

View file

@ -0,0 +1,215 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`discover Discover Embeddable - Generate CSV report per panel Generate Embeddable CSV for savedDiscoverSessionWithDataView generates a report with custom time range 1`] = `
"\\"@timestamp\\",extension
\\"Sep 21, 2015 @ 00:59:00.367\\",jpg
\\"Sep 20, 2015 @ 19:49:05.899\\",jpg
\\"Sep 20, 2015 @ 19:29:43.926\\",jpg
\\"Sep 20, 2015 @ 17:38:42.285\\",jpg
\\"Sep 20, 2015 @ 17:08:30.609\\",jpg
\\"Sep 20, 2015 @ 16:53:17.163\\",jpg
\\"Sep 20, 2015 @ 16:45:00.480\\",jpg
\\"Sep 20, 2015 @ 16:08:49.527\\",css
\\"Sep 20, 2015 @ 15:55:31.002\\",css
\\"Sep 20, 2015 @ 15:32:20.876\\",css
\\"Sep 20, 2015 @ 15:20:47.204\\",png
\\"Sep 20, 2015 @ 15:14:43.471\\",css
\\"Sep 20, 2015 @ 15:14:26.330\\",png
\\"Sep 20, 2015 @ 15:03:45.263\\",jpg
\\"Sep 20, 2015 @ 14:55:58.779\\",jpg
\\"Sep 20, 2015 @ 14:47:52.748\\",css
\\"Sep 20, 2015 @ 14:44:34.702\\",jpg
\\"Sep 20, 2015 @ 14:39:50.742\\",jpg
\\"Sep 20, 2015 @ 14:39:11.084\\",jpg
\\"Sep 20, 2015 @ 14:25:09.526\\",jpg
\\"Sep 20, 2015 @ 14:21:12.441\\",php
\\"Sep 20, 2015 @ 14:18:32.300\\",gif
\\"Sep 20, 2015 @ 14:14:23.696\\",jpg
\\"Sep 20, 2015 @ 14:14:13.170\\",jpg
\\"Sep 20, 2015 @ 13:32:59.544\\",css
\\"Sep 20, 2015 @ 13:14:11.094\\",jpg
\\"Sep 20, 2015 @ 13:09:17.207\\",jpg
\\"Sep 20, 2015 @ 12:50:29.152\\",jpg
\\"Sep 20, 2015 @ 12:44:47.907\\",jpg
\\"Sep 20, 2015 @ 12:35:46.175\\",jpg
\\"Sep 20, 2015 @ 12:35:16.811\\",css
\\"Sep 20, 2015 @ 12:34:41.080\\",jpg
\\"Sep 20, 2015 @ 12:27:57.163\\",jpg
\\"Sep 20, 2015 @ 12:25:20.687\\",jpg
\\"Sep 20, 2015 @ 12:24:50.429\\",jpg
\\"Sep 20, 2015 @ 11:47:17.612\\",jpg
\\"Sep 20, 2015 @ 11:41:19.508\\",jpg
\\"Sep 20, 2015 @ 11:37:37.381\\",jpg
\\"Sep 20, 2015 @ 11:30:51.674\\",css
\\"Sep 20, 2015 @ 11:24:51.803\\",jpg
\\"Sep 20, 2015 @ 11:14:23.740\\",jpg
\\"Sep 20, 2015 @ 11:03:40.696\\",css
\\"Sep 20, 2015 @ 10:58:13.547\\",jpg
\\"Sep 20, 2015 @ 10:53:07.793\\",jpg
\\"Sep 20, 2015 @ 10:42:16.645\\",png
\\"Sep 20, 2015 @ 10:41:14.358\\",css
\\"Sep 20, 2015 @ 10:31:30.074\\",css
\\"Sep 20, 2015 @ 10:31:00.654\\",jpg
\\"Sep 20, 2015 @ 10:30:25.744\\",css
\\"Sep 20, 2015 @ 10:23:27.213\\",png
\\"Sep 20, 2015 @ 10:16:44.923\\",jpg
\\"Sep 20, 2015 @ 09:48:34.742\\",css
\\"Sep 20, 2015 @ 09:48:21.935\\",jpg
\\"Sep 20, 2015 @ 09:43:53.380\\",jpg
\\"Sep 20, 2015 @ 09:38:59.682\\",jpg
\\"Sep 20, 2015 @ 09:22:15.681\\",jpg
\\"Sep 20, 2015 @ 09:21:34.096\\",jpg
\\"Sep 20, 2015 @ 09:11:27.068\\",gif
\\"Sep 20, 2015 @ 09:08:30.796\\",css
\\"Sep 20, 2015 @ 08:49:57.121\\",jpg
\\"Sep 20, 2015 @ 08:40:41.779\\",png
\\"Sep 20, 2015 @ 08:34:53.186\\",css
\\"Sep 20, 2015 @ 08:15:38.922\\",png
\\"Sep 20, 2015 @ 07:50:33.356\\",jpg
\\"Sep 20, 2015 @ 07:23:02.614\\",css
\\"Sep 20, 2015 @ 07:18:15.800\\",css
\\"Sep 20, 2015 @ 07:07:01.048\\",jpg
\\"Sep 20, 2015 @ 06:41:00.585\\",css
\\"Sep 20, 2015 @ 06:39:27.633\\",jpg
\\"Sep 20, 2015 @ 06:26:36.476\\",png
\\"Sep 20, 2015 @ 06:26:10.475\\",css
\\"Sep 20, 2015 @ 06:24:30.150\\",jpg
\\"Sep 20, 2015 @ 06:03:53.342\\",jpg
\\"Sep 20, 2015 @ 05:48:01.031\\",gif
\\"Sep 20, 2015 @ 04:32:06.465\\",jpg
\\"Sep 20, 2015 @ 04:22:45.702\\",jpg
\\"Sep 20, 2015 @ 04:18:54.867\\",css
\\"Sep 20, 2015 @ 04:11:49.921\\",jpg
\\"Sep 20, 2015 @ 03:48:32.880\\",jpg
\\"Sep 20, 2015 @ 03:44:10.788\\",jpg
"
`;
exports[`discover Discover Embeddable - Generate CSV report per panel Generate Embeddable CSV for savedDiscoverSessionWithDataView generates a report with global time range 1`] = `
"\\"@timestamp\\",extension
\\"Sep 21, 2015 @ 11:26:44.669\\",jpg
\\"Sep 21, 2015 @ 11:17:34.550\\",jpg
\\"Sep 21, 2015 @ 10:49:38.164\\",jpg
\\"Sep 21, 2015 @ 10:48:46.371\\",jpg
\\"Sep 21, 2015 @ 10:33:15.502\\",jpg
\\"Sep 21, 2015 @ 10:15:36.162\\",jpg
\\"Sep 21, 2015 @ 10:10:20.302\\",jpg
\\"Sep 21, 2015 @ 10:07:18.887\\",php
\\"Sep 21, 2015 @ 09:59:45.555\\",jpg
\\"Sep 21, 2015 @ 09:36:07.770\\",jpg
\\"Sep 21, 2015 @ 09:20:36.111\\",css
\\"Sep 21, 2015 @ 09:14:28.585\\",jpg
\\"Sep 21, 2015 @ 08:52:59.787\\",jpg
\\"Sep 21, 2015 @ 08:42:28.854\\",jpg
\\"Sep 21, 2015 @ 08:07:44.865\\",jpg
\\"Sep 21, 2015 @ 08:05:13.743\\",css
\\"Sep 21, 2015 @ 07:19:42.176\\",jpg
\\"Sep 21, 2015 @ 06:49:26.535\\",jpg
\\"Sep 21, 2015 @ 06:31:49.112\\",jpg
\\"Sep 21, 2015 @ 05:34:13.304\\",gif
\\"Sep 21, 2015 @ 05:06:15.814\\",jpg
\\"Sep 21, 2015 @ 04:08:39.759\\",png
\\"Sep 21, 2015 @ 03:41:43.705\\",jpg
\\"Sep 21, 2015 @ 02:54:47.500\\",jpg
\\"Sep 21, 2015 @ 00:59:00.367\\",jpg
\\"Sep 20, 2015 @ 19:49:05.899\\",jpg
\\"Sep 20, 2015 @ 19:29:43.926\\",jpg
\\"Sep 20, 2015 @ 17:38:42.285\\",jpg
\\"Sep 20, 2015 @ 17:08:30.609\\",jpg
\\"Sep 20, 2015 @ 16:53:17.163\\",jpg
\\"Sep 20, 2015 @ 16:45:00.480\\",jpg
\\"Sep 20, 2015 @ 16:08:49.527\\",css
\\"Sep 20, 2015 @ 15:55:31.002\\",css
\\"Sep 20, 2015 @ 15:32:20.876\\",css
\\"Sep 20, 2015 @ 15:20:47.204\\",png
\\"Sep 20, 2015 @ 15:14:43.471\\",css
\\"Sep 20, 2015 @ 15:14:26.330\\",png
\\"Sep 20, 2015 @ 15:03:45.263\\",jpg
\\"Sep 20, 2015 @ 14:55:58.779\\",jpg
\\"Sep 20, 2015 @ 14:47:52.748\\",css
\\"Sep 20, 2015 @ 14:44:34.702\\",jpg
\\"Sep 20, 2015 @ 14:39:50.742\\",jpg
\\"Sep 20, 2015 @ 14:39:11.084\\",jpg
\\"Sep 20, 2015 @ 14:25:09.526\\",jpg
\\"Sep 20, 2015 @ 14:21:12.441\\",php
\\"Sep 20, 2015 @ 14:18:32.300\\",gif
\\"Sep 20, 2015 @ 14:14:23.696\\",jpg
\\"Sep 20, 2015 @ 14:14:13.170\\",jpg
\\"Sep 20, 2015 @ 13:32:59.544\\",css
\\"Sep 20, 2015 @ 13:14:11.094\\",jpg
\\"Sep 20, 2015 @ 13:09:17.207\\",jpg
\\"Sep 20, 2015 @ 12:50:29.152\\",jpg
\\"Sep 20, 2015 @ 12:44:47.907\\",jpg
\\"Sep 20, 2015 @ 12:35:46.175\\",jpg
\\"Sep 20, 2015 @ 12:35:16.811\\",css
\\"Sep 20, 2015 @ 12:34:41.080\\",jpg
\\"Sep 20, 2015 @ 12:27:57.163\\",jpg
\\"Sep 20, 2015 @ 12:25:20.687\\",jpg
\\"Sep 20, 2015 @ 12:24:50.429\\",jpg
\\"Sep 20, 2015 @ 11:47:17.612\\",jpg
\\"Sep 20, 2015 @ 11:41:19.508\\",jpg
\\"Sep 20, 2015 @ 11:37:37.381\\",jpg
\\"Sep 20, 2015 @ 11:30:51.674\\",css
\\"Sep 20, 2015 @ 11:24:51.803\\",jpg
\\"Sep 20, 2015 @ 11:14:23.740\\",jpg
\\"Sep 20, 2015 @ 11:03:40.696\\",css
\\"Sep 20, 2015 @ 10:58:13.547\\",jpg
\\"Sep 20, 2015 @ 10:53:07.793\\",jpg
\\"Sep 20, 2015 @ 10:42:16.645\\",png
\\"Sep 20, 2015 @ 10:41:14.358\\",css
\\"Sep 20, 2015 @ 10:31:30.074\\",css
\\"Sep 20, 2015 @ 10:31:00.654\\",jpg
\\"Sep 20, 2015 @ 10:30:25.744\\",css
\\"Sep 20, 2015 @ 10:23:27.213\\",png
\\"Sep 20, 2015 @ 10:16:44.923\\",jpg
\\"Sep 20, 2015 @ 09:48:34.742\\",css
\\"Sep 20, 2015 @ 09:48:21.935\\",jpg
\\"Sep 20, 2015 @ 09:43:53.380\\",jpg
\\"Sep 20, 2015 @ 09:38:59.682\\",jpg
\\"Sep 20, 2015 @ 09:22:15.681\\",jpg
\\"Sep 20, 2015 @ 09:21:34.096\\",jpg
\\"Sep 20, 2015 @ 09:11:27.068\\",gif
\\"Sep 20, 2015 @ 09:08:30.796\\",css
\\"Sep 20, 2015 @ 08:49:57.121\\",jpg
\\"Sep 20, 2015 @ 08:40:41.779\\",png
\\"Sep 20, 2015 @ 08:34:53.186\\",css
\\"Sep 20, 2015 @ 08:15:38.922\\",png
\\"Sep 20, 2015 @ 07:50:33.356\\",jpg
\\"Sep 20, 2015 @ 07:23:02.614\\",css
\\"Sep 20, 2015 @ 07:18:15.800\\",css
\\"Sep 20, 2015 @ 07:07:01.048\\",jpg
\\"Sep 20, 2015 @ 06:41:00.585\\",css
\\"Sep 20, 2015 @ 06:39:27.633\\",jpg
\\"Sep 20, 2015 @ 06:26:36.476\\",png
\\"Sep 20, 2015 @ 06:26:10.475\\",css
\\"Sep 20, 2015 @ 06:24:30.150\\",jpg
\\"Sep 20, 2015 @ 06:03:53.342\\",jpg
\\"Sep 20, 2015 @ 05:48:01.031\\",gif
\\"Sep 20, 2015 @ 04:32:06.465\\",jpg
\\"Sep 20, 2015 @ 04:22:45.702\\",jpg
\\"Sep 20, 2015 @ 04:18:54.867\\",css
\\"Sep 20, 2015 @ 04:11:49.921\\",jpg
\\"Sep 20, 2015 @ 03:48:32.880\\",jpg
\\"Sep 20, 2015 @ 03:44:10.788\\",jpg
"
`;
exports[`discover Discover Embeddable - Generate CSV report per panel Generate Embeddable CSV for savedDiscoverSessionWithESQL generates a report with custom time range 1`] = `
"averageB,extension
\\"10497.149253731342\\",png
\\"5738.34725848564\\",css
\\"5536.247799152266\\",jpg
\\"4868.18064516129\\",php
\\"466.22712933753945\\",gif
"
`;
exports[`discover Discover Embeddable - Generate CSV report per panel Generate Embeddable CSV for savedDiscoverSessionWithESQL generates a report with global time range 1`] = `
"averageB,extension
\\"10429.042836041359\\",png
\\"5765.214962121212\\",css
\\"5563.784938941655\\",jpg
\\"4884.612149532711\\",php
\\"471.8752783964365\\",gif
"
`;

View file

@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./preserve_url'));
loadTestFile(require.resolve('./async_scripted_fields'));
loadTestFile(require.resolve('./reporting'));
loadTestFile(require.resolve('./reporting_embeddable'));
loadTestFile(require.resolve('./error_handling'));
loadTestFile(require.resolve('./saved_queries'));
loadTestFile(require.resolve('./saved_searches'));

View file

@ -0,0 +1,178 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const { reporting, common, discover, timePicker, header, dashboard, unifiedFieldList } =
getPageObjects([
'reporting',
'common',
'discover',
'timePicker',
'header',
'dashboard',
'unifiedFieldList',
]);
const monacoEditor = getService('monacoEditor');
const queryBar = getService('queryBar');
const testSubjects = getService('testSubjects');
const toasts = getService('toasts');
const dataGrid = getService('dataGrid');
const dashboardAddPanel = getService('dashboardAddPanel');
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const dashboardBadgeActions = getService('dashboardBadgeActions');
const GENERATE_CSV_DATA_TEST_SUBJ = 'embeddablePanelAction-generateCsvReport';
const SAVED_DISCOVER_SESSION_WITH_DATA_VIEW = 'savedDiscoverSessionWithDataView';
const SAVED_DISCOVER_SESSION_WITH_ESQL = 'savedDiscoverSessionWithESQL';
const getDashboardPanelReport = async (title: string, { timeout } = { timeout: 60 * 1000 }) => {
await toasts.dismissAll();
await dashboardPanelActions.expectExistsPanelAction(GENERATE_CSV_DATA_TEST_SUBJ, title);
await dashboardPanelActions.clickPanelActionByTitle(GENERATE_CSV_DATA_TEST_SUBJ, title);
await testSubjects.existOrFail('csvReportStarted'); /* validate toast panel */
const url = await reporting.getReportURL(timeout);
const res = await reporting.getResponse(url ?? '');
expect(res.status).to.equal(200);
expect(res.get('content-type')).to.equal('text/csv; charset=utf-8');
return res;
};
const addEmbeddableToDashboard = async (title: string) => {
await dashboardAddPanel.addSavedSearch(title);
await header.waitUntilLoadingHasFinished();
await dashboard.waitForRenderComplete();
const rows = await dataGrid.getDocTableRows();
expect(rows.length).to.be.above(0);
await dashboard.saveDashboard('test-csv', {
saveAsNew: true,
waitDialogIsClosed: false,
exitFromEditMode: true,
});
await header.waitUntilLoadingHasFinished();
await dashboard.waitForRenderComplete();
};
const setDashboardGlobalTimeRange = async () => {
const fromTime = 'Sep 19, 2015 @ 16:31:44.000';
const toTime = 'Sep 21, 2015 @ 11:31:44.000';
await timePicker.setAbsoluteRange(fromTime, toTime);
};
const setDashboardPanelCustomTimeRange = async () => {
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.enableCustomTimeRange();
const toTime = 'Sep 21, 2015 @ 01:31:44.000';
const endTimeTestSubj =
'customizePanelTimeRangeDatePicker > superDatePickerendDatePopoverButton';
await retry.waitFor(`endDate is set to ${toTime}`, async () => {
await testSubjects.click(endTimeTestSubj);
await testSubjects.click('superDatePickerAbsoluteDateInput');
await timePicker.inputValue('superDatePickerAbsoluteDateInput', toTime);
await testSubjects.click(endTimeTestSubj);
const actualToTime = await testSubjects.getVisibleText(endTimeTestSubj);
return toTime === actualToTime;
});
await dashboardCustomizePanel.clickSaveButton();
await dashboard.waitForRenderComplete();
await dashboardBadgeActions.expectExistsTimeRangeBadgeAction();
};
describe('Discover Embeddable - Generate CSV report per panel', () => {
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
await kibanaServer.importExport.load(
'src/platform/test/functional/fixtures/kbn_archiver/discover'
);
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await common.navigateToApp('discover');
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await discover.selectIndexPattern('logstash-*');
// create and save a discover session with filters in data view mode
await unifiedFieldList.waitUntilSidebarHasLoaded();
await unifiedFieldList.clickFieldListItem('geo.src');
await unifiedFieldList.clickFieldListPlusFilter('geo.src', 'US');
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await unifiedFieldList.clickFieldListItemAdd('extension');
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await queryBar.setQuery('machine.os : ios');
await queryBar.submitQuery();
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getDocCount()).to.be(222);
await discover.saveSearch(SAVED_DISCOVER_SESSION_WITH_DATA_VIEW);
await header.waitUntilLoadingHasFinished();
// create and save a discover session with filters in ES|QL mode
await discover.clickNewSearchButton();
await discover.selectTextBaseLang();
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await monacoEditor.setCodeEditorValue(
'from logstash-* | sort @timestamp desc | stats averageB = avg(bytes) by extension | sort averageB desc'
);
await testSubjects.click('querySubmitButton');
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getDocCount()).to.be(5);
await discover.saveSearch(SAVED_DISCOVER_SESSION_WITH_ESQL);
await header.waitUntilLoadingHasFinished();
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional');
await kibanaServer.savedObjects.cleanStandardList();
});
[SAVED_DISCOVER_SESSION_WITH_DATA_VIEW, SAVED_DISCOVER_SESSION_WITH_ESQL].map(
(title: string) => {
describe(`Generate Embeddable CSV for ${title}`, () => {
beforeEach(async () => {
await kibanaServer.savedObjects.clean({ types: ['dashboard'] });
});
it('generates a report with global time range', async () => {
await dashboard.navigateToApp();
await dashboard.clickNewDashboard();
await setDashboardGlobalTimeRange();
await addEmbeddableToDashboard(title);
const { text: csvFile } = await getDashboardPanelReport(title);
expectSnapshot(csvFile).toMatch();
});
it('generates a report with custom time range', async () => {
await dashboard.navigateToApp();
await dashboard.clickNewDashboard();
await setDashboardGlobalTimeRange();
await addEmbeddableToDashboard(title);
await setDashboardPanelCustomTimeRange();
const { text: csvFile } = await getDashboardPanelReport(title);
expectSnapshot(csvFile).toMatch();
});
});
}
);
});
}