diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index ebb1946b524cd..6a51c085ebbc9 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ -import { Capabilities } from 'kibana/public'; -import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; -import { IUiSettingsClient } from 'kibana/public'; +import { Capabilities, IUiSettingsClient } from 'kibana/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { IndexPattern } from 'src/plugins/data/public'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; describe('getSharingData', () => { let mockConfig: IUiSettingsClient; @@ -36,6 +35,32 @@ describe('getSharingData', () => { const result = await getSharingData(searchSourceMock, { columns: [] }, mockConfig); expect(result).toMatchInlineSnapshot(` Object { + "columns": Array [], + "searchSource": Object { + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_score": "desc", + }, + ], + }, + } + `); + }); + + test('returns valid data for sharing when columns are selected', async () => { + const searchSourceMock = createSearchSourceMock({ index: indexPatternMock }); + const result = await getSharingData( + searchSourceMock, + { columns: ['column_a', 'column_b'] }, + mockConfig + ); + expect(result).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "column_a", + "column_b", + ], "searchSource": Object { "index": "the-index-pattern-id", "sort": Array [ @@ -69,16 +94,16 @@ describe('getSharingData', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "columns": Array [ + "cool-timefield", + "cool-field-1", + "cool-field-2", + "cool-field-3", + "cool-field-4", + "cool-field-5", + "cool-field-6", + ], "searchSource": Object { - "fields": Array [ - "cool-timefield", - "cool-field-1", - "cool-field-2", - "cool-field-3", - "cool-field-4", - "cool-field-5", - "cool-field-6", - ], "index": "the-index-pattern-id", "sort": Array [ Object { @@ -120,15 +145,15 @@ describe('getSharingData', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "columns": Array [ + "cool-field-1", + "cool-field-2", + "cool-field-3", + "cool-field-4", + "cool-field-5", + "cool-field-6", + ], "searchSource": Object { - "fields": Array [ - "cool-field-1", - "cool-field-2", - "cool-field-3", - "cool-field-4", - "cool-field-5", - "cool-field-6", - ], "index": "the-index-pattern-id", "sort": Array [ Object { diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index f0e07ccc38deb..47be4b8037152 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -7,11 +7,11 @@ */ import type { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { getSortForSearchSource } from '../angular/doc_table'; import { ISearchSource } from '../../../../data/common'; -import { AppState } from '../angular/discover_state'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; import type { SavedSearch, SortOrder } from '../../saved_searches/types'; +import { AppState } from '../angular/discover_state'; +import { getSortForSearchSource } from '../angular/doc_table'; /** * Preparing data to share the current state as link or CSV/Report @@ -23,10 +23,6 @@ export async function getSharingData( ) { const searchSource = currentSearchSource.createCopy(); const index = searchSource.getField('index')!; - const fields = { - fields: searchSource.getField('fields'), - fieldsFromSource: searchSource.getField('fieldsFromSource'), - }; searchSource.setField( 'sort', @@ -37,7 +33,7 @@ export async function getSharingData( searchSource.removeField('aggs'); searchSource.removeField('size'); - // fields get re-set to match the saved search columns + // Columns that the user has selected in the saved search let columns = state.columns || []; if (columns && columns.length > 0) { @@ -50,14 +46,11 @@ export async function getSharingData( if (timeFieldName && !columns.includes(timeFieldName)) { columns = [timeFieldName, ...columns]; } - - // if columns were selected in the saved search, use them for the searchSource's fields - const fieldsKey = fields.fieldsFromSource ? 'fieldsFromSource' : 'fields'; - searchSource.setField(fieldsKey, columns); } return { searchSource: searchSource.getSerializedFields(true), + columns, }; } diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index 4e1b9ccd2642f..06d626a4c4044 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -16,6 +16,7 @@ describe('GetCsvReportPanelAction', () => { let core: any; let context: any; let mockLicense$: any; + let mockSearchSource: any; beforeAll(() => { if (typeof window.URL.revokeObjectURL === 'undefined') { @@ -49,22 +50,19 @@ describe('GetCsvReportPanelAction', () => { }, } as any; + mockSearchSource = { + createCopy: () => mockSearchSource, + removeField: jest.fn(), + setField: jest.fn(), + getField: jest.fn(), + getSerializedFields: jest.fn().mockImplementation(() => ({})), + }; + context = { embeddable: { type: 'search', getSavedSearch: () => { - const searchSource = { - createCopy: () => searchSource, - removeField: jest.fn(), - setField: jest.fn(), - getField: jest.fn().mockImplementation((key: string) => { - if (key === 'index') { - return 'my-test-index-*'; - } - }), - getSerializedFields: jest.fn().mockImplementation(() => ({})), - }; - return { searchSource }; + return { searchSource: mockSearchSource }; }, getTitle: () => `The Dude`, getInspectorAdapters: () => null, @@ -79,6 +77,49 @@ describe('GetCsvReportPanelAction', () => { } as any; }); + it('translates empty embeddable context into job params', async () => { + const panel = new GetCsvReportPanelAction(core, mockLicense$()); + + await panel.execute(context); + + expect(core.http.post).toHaveBeenCalledWith( + '/api/reporting/v1/generate/immediate/csv_searchsource', + { + body: '{"searchSource":{},"columns":[],"browserTimezone":"America/New_York"}', + } + ); + }); + + it('translates embeddable context into job params', async () => { + // setup + mockSearchSource = { + createCopy: () => mockSearchSource, + removeField: jest.fn(), + setField: jest.fn(), + getField: jest.fn(), + getSerializedFields: jest.fn().mockImplementation(() => ({ testData: 'testDataValue' })), + }; + context.embeddable.getSavedSearch = () => { + return { + searchSource: mockSearchSource, + columns: ['column_a', 'column_b'], + }; + }; + + const panel = new GetCsvReportPanelAction(core, mockLicense$()); + + // test + await panel.execute(context); + + expect(core.http.post).toHaveBeenCalledWith( + '/api/reporting/v1/generate/immediate/csv_searchsource', + { + body: + '{"searchSource":{"testData":"testDataValue"},"columns":["column_a","column_b"],"browserTimezone":"America/New_York"}', + } + ); + }); + it('allows downloading for valid licenses', async () => { const panel = new GetCsvReportPanelAction(core, mockLicense$()); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index d440edc3f3fe9..95d193880975c 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -7,21 +7,19 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; -import { CoreSetup } from 'src/core/public'; +import type { CoreSetup } from 'src/core/public'; +import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public'; import { loadSharingDataHelpers, - ISearchEmbeddable, - SavedSearch, SEARCH_EMBEDDABLE_TYPE, } from '../../../../../src/plugins/discover/public'; -import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; -import { - IncompatibleActionError, - UiActionsActionDefinition as ActionDefinition, -} from '../../../../../src/plugins/ui_actions/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; +import type { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; +import { ViewMode } from '../../../../../src/plugins/embeddable/public'; +import type { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; +import { IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; +import type { LicensingPluginSetup } from '../../../licensing/public'; import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; -import { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; +import type { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; import { checkLicense } from '../lib/license_check'; function isSavedSearchEmbeddable( @@ -64,14 +62,11 @@ export class GetCsvReportPanelAction implements ActionDefinition public async getSearchSource(savedSearch: SavedSearch, embeddable: ISearchEmbeddable) { const { getSharingData } = await loadSharingDataHelpers(); - const searchSource = savedSearch.searchSource.createCopy(); - const { searchSource: serializedSearchSource } = await getSharingData( - searchSource, + return await getSharingData( + savedSearch.searchSource, savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977 this.core.uiSettings ); - - return serializedSearchSource; } public isCompatible = async (context: ActionContext) => { @@ -96,12 +91,13 @@ export class GetCsvReportPanelAction implements ActionDefinition } const savedSearch = embeddable.getSavedSearch(); - const searchSource = await this.getSearchSource(savedSearch, embeddable); + const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable); const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; const immediateJobParams: JobParamsDownloadCSV = { searchSource, + columns, browserTimezone, title: savedSearch.title, }; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 97433f7a4f0c1..8995ef4739b09 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -8,14 +8,15 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; -import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { ShareContext } from '../../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; +import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; +import type { SearchSourceFields } from 'src/plugins/data/common'; +import type { ShareContext } from '../../../../../src/plugins/share/public'; +import type { LicensingPluginSetup } from '../../../licensing/public'; import { CSV_JOB_TYPE } from '../../common/constants'; -import { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; +import type { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; import { ReportingPanelContent } from '../components/reporting_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ReportingAPIClient } from '../lib/reporting_api_client'; interface ReportingProvider { apiClient: ReportingAPIClient; @@ -65,7 +66,8 @@ export const csvReportingProvider = ({ browserTimezone, title: sharingData.title as string, objectType, - searchSource: sharingData.searchSource, + searchSource: sharingData.searchSource as SearchSourceFields, + columns: sharingData.columns as string[] | undefined, }; const getJobParams = () => jobParams; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 87011cc918587..00ba167c50ae6 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -8,15 +8,15 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; -import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { ShareContext } from '../../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; -import { LayoutParams } from '../../common/types'; -import { JobParamsPNG } from '../../server/export_types/png/types'; -import { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; +import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; +import type { ShareContext } from '../../../../../src/plugins/share/public'; +import type { LicensingPluginSetup } from '../../../licensing/public'; +import type { LayoutParams } from '../../common/types'; +import type { JobParamsPNG } from '../../server/export_types/png/types'; +import type { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ReportingAPIClient } from '../lib/reporting_api_client'; interface ReportingPDFPNGProvider { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap index 62c9ecff830ff..789b68a25ac42 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap @@ -1,18 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`fields cells can be multi-value 1`] = ` +exports[`fields from job.columns (7.13+ generated) cells can be multi-value 1`] = ` +"product,category +coconut,\\"cool, rad\\" +" +`; + +exports[`fields from job.columns (7.13+ generated) columns can be top-level fields such as _id and _index 1`] = ` +"\\"_id\\",\\"_index\\",product,category +\\"my-cool-id\\",\\"my-cool-index\\",coconut,\\"cool, rad\\" +" +`; + +exports[`fields from job.columns (7.13+ generated) empty columns defaults to using searchSource.getFields() 1`] = ` +"product +coconut +" +`; + +exports[`fields from job.searchSource.getFields() (7.12 generated) cells can be multi-value 1`] = ` "\\"_id\\",sku \\"my-cool-id\\",\\"This is a cool SKU., This is also a cool SKU.\\" " `; -exports[`fields provides top-level underscored fields as columns 1`] = ` +exports[`fields from job.searchSource.getFields() (7.12 generated) provides top-level underscored fields as columns 1`] = ` "\\"_id\\",\\"_index\\",date,message \\"my-cool-id\\",\\"my-cool-index\\",\\"2020-12-31T00:14:28.000Z\\",\\"it's nice to see you\\" " `; -exports[`fields sorts the fields when they are to be used as table column names 1`] = ` +exports[`fields from job.searchSource.getFields() (7.12 generated) sorts the fields when they are to be used as table column names 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",date,\\"message_t\\",\\"message_u\\",\\"message_v\\",\\"message_w\\",\\"message_x\\",\\"message_y\\",\\"message_z\\" \\"my-cool-id\\",\\"my-cool-index\\",\\"'-\\",\\"'-\\",\\"2020-12-31T00:14:28.000Z\\",\\"test field T\\",\\"test field U\\",\\"test field V\\",\\"test field W\\",\\"test field X\\",\\"test field Y\\",\\"test field Z\\" " diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index 0193eaaff2c8d..8694eddce7967 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -326,7 +326,7 @@ it('uses the scrollId to page all the data', async () => { expect(csvResult.content).toMatchSnapshot(); }); -describe('fields', () => { +describe('fields from job.searchSource.getFields() (7.12 generated)', () => { it('cells can be multi-value', async () => { searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { if (key === 'fields') { @@ -497,6 +497,140 @@ describe('fields', () => { }); }); +describe('fields from job.columns (7.13+ generated)', () => { + it('cells can be multi-value', async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + product: 'coconut', + category: [`cool`, `rad`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {}, columns: ['product', 'category'] }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); + + it('columns can be top-level fields such as _id and _index', async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + product: 'coconut', + category: [`cool`, `rad`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {}, columns: ['_id', '_index', 'product', 'category'] }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); + + it('empty columns defaults to using searchSource.getFields()', async () => { + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['product']; + } + return mockSearchSourceGetFieldDefault(key); + }); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + product: 'coconut', + category: [`cool`, `rad`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {}, columns: [] }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); +}); + describe('formulas', () => { const TEST_FORMULA = '=SUM(A1:A2)'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 01959ed08036d..7517396961c00 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -20,6 +20,7 @@ import { ISearchSource, ISearchStartSearchSource, SearchFieldValue, + SearchSourceFields, tabifyDocs, } from '../../../../../../../src/plugins/data/common'; import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server'; @@ -60,7 +61,8 @@ function isPlainStringArray( } export class CsvGenerator { - private _formatters: Record | null = null; + private _columns?: string[]; + private _formatters?: Record; private csvContainsFormulas = false; private maxSizeReached = false; private csvRowCount = 0; @@ -135,27 +137,36 @@ export class CsvGenerator { }; } - // use fields/fieldsFromSource from the searchSource to get the ordering of columns - // otherwise use the table columns as they are - private getFields(searchSource: ISearchSource, table: Datatable): string[] { - const fieldValues: Record = { - fields: searchSource.getField('fields'), - fieldsFromSource: searchSource.getField('fieldsFromSource'), - }; - const fieldSource = fieldValues.fieldsFromSource ? 'fieldsFromSource' : 'fields'; - this.logger.debug(`Getting search source fields from: '${fieldSource}'`); - - const fields = fieldValues[fieldSource]; - // Check if field name values are string[] and if the fields are user-defined - if (isPlainStringArray(fields)) { - return fields; + private getColumns(searchSource: ISearchSource, table: Datatable) { + if (this._columns != null) { + return this._columns; } - // Default to using the table column IDs as the fields - const columnIds = table.columns.map((c) => c.id); - // Fields in the API response don't come sorted - they need to be sorted client-side - columnIds.sort(); - return columnIds; + // if columns is not provided in job params, + // default to use fields/fieldsFromSource from the searchSource to get the ordering of columns + const getFromSearchSource = (): string[] => { + const fieldValues: Pick = { + fields: searchSource.getField('fields'), + fieldsFromSource: searchSource.getField('fieldsFromSource'), + }; + const fieldSource = fieldValues.fieldsFromSource ? 'fieldsFromSource' : 'fields'; + this.logger.debug(`Getting columns from '${fieldSource}' in search source.`); + + const fields = fieldValues[fieldSource]; + // Check if field name values are string[] and if the fields are user-defined + if (isPlainStringArray(fields)) { + return fields; + } + + // Default to using the table column IDs as the fields + const columnIds = table.columns.map((c) => c.id); + // Fields in the API response don't come sorted - they need to be sorted client-side + columnIds.sort(); + return columnIds; + }; + this._columns = this.job.columns?.length ? this.job.columns : getFromSearchSource(); + + return this._columns; } private formatCellValues(formatters: Record) { @@ -202,16 +213,16 @@ export class CsvGenerator { } /* - * Use the list of fields to generate the header row + * Use the list of columns to generate the header row */ private generateHeader( - fields: string[], + columns: string[], table: Datatable, builder: MaxSizeStringBuilder, settings: CsvExportSettings ) { this.logger.debug(`Building CSV header row...`); - const header = fields.map(this.escapeValues(settings)).join(settings.separator) + '\n'; + const header = columns.map(this.escapeValues(settings)).join(settings.separator) + '\n'; if (!builder.tryAppend(header)) { return { @@ -227,7 +238,7 @@ export class CsvGenerator { * Format a Datatable into rows of CSV content */ private generateRows( - fields: string[], + columns: string[], table: Datatable, builder: MaxSizeStringBuilder, formatters: Record, @@ -240,7 +251,7 @@ export class CsvGenerator { } const row = - fields + columns .map((f) => ({ column: f, data: dataTableRow[f] })) .map(this.formatCellValues(formatters)) .map(this.escapeValues(settings)) @@ -338,11 +349,13 @@ export class CsvGenerator { break; } - const fields = this.getFields(searchSource, table); + // If columns exists in the job params, use it to order the CSV columns + // otherwise, get the ordering from the searchSource's fields / fieldsFromSource + const columns = this.getColumns(searchSource, table); if (first) { first = false; - this.generateHeader(fields, table, builder, settings); + this.generateHeader(columns, table, builder, settings); } if (table.rows.length < 1) { @@ -350,7 +363,7 @@ export class CsvGenerator { } const formatters = this.getFormatters(table); - this.generateRows(fields, table, builder, formatters, settings); + this.generateRows(columns, table, builder, formatters, settings); // update iterator currentRecord += table.rows.length; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts index f0ad4e00ebd5c..d2a9e2b5bf783 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts @@ -5,13 +5,15 @@ * 2.0. */ -import { BaseParams, BasePayload } from '../../types'; +import type { SearchSourceFields } from 'src/plugins/data/common'; +import type { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; interface BaseParamsCSV { browserTimezone: string; - searchSource: any; + searchSource: SearchSourceFields; + columns?: string[]; } export type JobParamsCSV = BaseParamsCSV & BaseParams; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts index 276016dd61233..cb1dd659ee2c2 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { TimeRangeParams } from '../common'; +import type { SearchSourceFields } from 'src/plugins/data/common'; export interface FakeRequest { headers: Record; @@ -14,7 +14,8 @@ export interface FakeRequest { export interface JobParamsDownloadCSV { browserTimezone: string; title: string; - searchSource: any; + searchSource: SearchSourceFields; + columns?: string[]; } export interface SavedObjectServiceError { diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 55092b5236ce6..5d2b77c082ca5 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -44,6 +44,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( path: `${API_BASE_GENERATE_V1}/immediate/csv_searchsource`, validate: { body: schema.object({ + columns: schema.maybe(schema.arrayOf(schema.string())), searchSource: schema.object({}, { unknowns: 'allow' }), browserTimezone: schema.string({ defaultValue: 'UTC' }), title: schema.string(), diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index c437cfaa8f5dc..d4a909f6a0474 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -50,8 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel }; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 - describe.skip('Download CSV', () => { + describe('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await browser.setWindowSize(1600, 850); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts index ebc7badd88f42..f381bc1edd28e 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts @@ -10,9 +10,9 @@ import supertest from 'supertest'; import { JobParamsDownloadCSV } from '../../../plugins/reporting/server/export_types/csv_searchsource_immediate/types'; import { FtrProviderContext } from '../ftr_provider_context'; -const getMockJobParams = (obj: Partial): JobParamsDownloadCSV => ({ +const getMockJobParams = (obj: any): JobParamsDownloadCSV => ({ title: `Mock CSV Title`, - ...(obj as any), + ...obj, }); // eslint-disable-next-line import/no-default-export @@ -31,8 +31,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 - describe.skip('CSV Generation from SearchSource', () => { + describe('CSV Generation from SearchSource', () => { before(async () => { await kibanaServer.uiSettings.update({ 'csv:quoteValues': false, @@ -387,9 +386,9 @@ export default function ({ getService }: FtrProviderContext) { version: true, index: '907bc200-a294-11e9-a900-ef10e0ac769e', sort: [{ date: 'desc' }], - fields: ['date', 'message', '_id', '_index'], filter: [], }, + columns: ['date', 'message', '_id', '_index'], }) ); const { status: resStatus, text: resText, type: resType } = res;