diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index 0a865d1c9e96b..5e72a9bd8fbbc 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -8,13 +8,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; -import { SetupDeps, StartDeps } from './types'; import { ReportingExampleApp } from './components/app'; +import { SetupDeps, StartDeps } from './types'; export const renderApp = ( coreStart: CoreStart, deps: Omit, - { appBasePath, element }: AppMountParameters + { appBasePath, element }: AppMountParameters // FIXME: appBasePath is deprecated ) => { ReactDOM.render(, element); diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx index 5f6f5d17afafb..a29c5d2e018d0 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -28,16 +28,10 @@ import { BrowserRouter as Router } from 'react-router-dom'; import * as Rx from 'rxjs'; import { takeWhile } from 'rxjs/operators'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; -import { CoreStart } from '../../../../../src/core/public'; -import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public'; -import { JobParamsPDF } from '../../../../plugins/reporting/server/export_types/printable_pdf/types'; -interface ReportingExampleAppDeps { +interface ReportingExampleAppProps { basename: string; - notifications: CoreStart['notifications']; - http: CoreStart['http']; - navigation: NavigationPublicPluginStart; reporting: ReportingStart; screenshotMode: ScreenshotModePluginSetup; } @@ -46,11 +40,9 @@ const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana']; export const ReportingExampleApp = ({ basename, - notifications, - http, reporting, screenshotMode, -}: ReportingExampleAppDeps) => { +}: ReportingExampleAppProps) => { const { getDefaultLayoutSelectors } = reporting; // Context Menu @@ -74,7 +66,7 @@ export const ReportingExampleApp = ({ }); }); - const getPDFJobParamsDefault = (): JobParamsPDF => { + const getPDFJobParamsDefault = () => { return { layout: { id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts index fd6f4bb894991..51d1b9abc5664 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts @@ -22,7 +22,6 @@ test('getPdfJobParams returns the correct job params for canvas layout', () => { const jobParams = getPdfJobParams(workpadSharingData, basePath); expect(jobParams).toMatchInlineSnapshot(` Object { - "browserTimezone": "America/New_York", "layout": Object { "dimensions": Object { "height": 0, diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts index 9586242bc7386..bbd6b42a38333 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts @@ -6,9 +6,7 @@ */ import { IBasePath } from 'kibana/public'; -import moment from 'moment-timezone'; import rison from 'rison-node'; -import { BaseParams } from '../../../../../reporting/common/types'; import { CanvasWorkpad } from '../../../../types'; export interface CanvasWorkpadSharingData { @@ -16,13 +14,10 @@ export interface CanvasWorkpadSharingData { pageCount: number; } -// TODO: get the correct type from Reporting plugin -type JobParamsPDF = BaseParams & { relativeUrls: string[] }; - export function getPdfJobParams( { workpad: { id, name: title, width, height }, pageCount }: CanvasWorkpadSharingData, basePath: IBasePath -): JobParamsPDF { +) { const urlPrefix = basePath.get().replace(basePath.serverBasePath, ''); // for Spaces prefix, which is included in basePath.get() const canvasEntry = `${urlPrefix}/app/canvas#`; @@ -43,7 +38,6 @@ export function getPdfJobParams( } return { - browserTimezone: moment.tz.guess(), layout: { dimensions: { width, height }, id: 'canvas', diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 16839173708e3..be543b3908b68 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -111,6 +111,11 @@ export enum JOB_STATUSES { export const REPORT_TABLE_ID = 'reportJobListing'; export const REPORT_TABLE_ROW_ID = 'reportJobRow'; +// Job params require a `version` field as of 7.15.0. For older jobs set with +// automation that have no version value in the job params, we assume the +// intended version is 7.14.0 +export const UNVERSIONED_VERSION = '7.14.0'; + // hacky endpoint: download CSV without queueing a report // FIXME: find a way to make these endpoints "generic" instead of hardcoded, as are the queued report export types export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`; diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index dfd4f75508494..f3a0e9192cf7d 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -97,10 +97,11 @@ export interface ReportDocument extends ReportDocumentHead { } export interface BaseParams { - browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface layout?: LayoutParams; objectType: string; title: string; + browserTimezone: string; // to format dates in the user's time zone + version: string; // to handle any state migrations } export type JobId = string; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 9396f23277496..5c618ba8261fa 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -6,34 +6,45 @@ */ import { i18n } from '@kbn/i18n'; +import moment from 'moment'; import { stringify } from 'query-string'; -import rison from 'rison-node'; -import { HttpSetup } from 'src/core/public'; +import rison, { RisonObject } from 'rison-node'; +import { HttpSetup, IUiSettingsClient } from 'src/core/public'; import { API_BASE_GENERATE, API_BASE_URL, + API_GENERATE_IMMEDIATE, API_LIST_URL, API_MIGRATE_ILM_POLICY_URL, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; -import { DownloadReportFn, JobId, ManagementLinkFn, ReportApiJSON } from '../../../common/types'; +import { + BaseParams, + DownloadReportFn, + JobId, + ManagementLinkFn, + ReportApiJSON, +} from '../../../common/types'; import { add } from '../../notifier/job_completion_notifications'; import { Job } from '../job'; +/* + * For convenience, apps do not have to provide the browserTimezone and Kibana version. + * Those fields are added in this client as part of the service. + * TODO: export a type like this to other plugins: https://github.com/elastic/kibana/issues/107085 + */ +type AppParams = Omit; + export interface DiagnoseResponse { help: string[]; success: boolean; logs: string; } -interface JobParams { - [paramName: string]: any; -} - interface IReportingAPI { // Helpers getReportURL(jobId: string): string; - getReportingJobPath(exportType: string, jobParams: JobParams): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL + getReportingJobPath(exportType: string, jobParams: BaseParams & T): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL createReportingJob(exportType: string, jobParams: any): Promise; // Sends a request to queue a job, with the job params in the POST body getServerBasePath(): string; // Provides the raw server basePath to allow it to be stripped out from relativeUrls in job params @@ -57,11 +68,11 @@ interface IReportingAPI { } export class ReportingAPIClient implements IReportingAPI { - private http: HttpSetup; - - constructor(http: HttpSetup) { - this.http = http; - } + constructor( + private http: HttpSetup, + private uiSettings: IUiSettingsClient, + private kibanaVersion: string + ) {} public getReportURL(jobId: string) { const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); @@ -132,13 +143,15 @@ export class ReportingAPIClient implements IReportingAPI { return reports.map((report) => new Job(report)); } - public getReportingJobPath(exportType: string, jobParams: JobParams) { - const params = stringify({ jobParams: rison.encode(jobParams) }); + public getReportingJobPath(exportType: string, jobParams: BaseParams) { + const risonObject: RisonObject = jobParams as Record; + const params = stringify({ jobParams: rison.encode(risonObject) }); return `${this.http.basePath.prepend(API_BASE_GENERATE)}/${exportType}?${params}`; } - public async createReportingJob(exportType: string, jobParams: any) { - const jobParamsRison = rison.encode(jobParams); + public async createReportingJob(exportType: string, jobParams: BaseParams) { + const risonObject: RisonObject = jobParams as Record; + const jobParamsRison = rison.encode(risonObject); const resp: { job: ReportApiJSON } = await this.http.post( `${API_BASE_GENERATE}/${exportType}`, { @@ -154,6 +167,27 @@ export class ReportingAPIClient implements IReportingAPI { return new Job(resp.job); } + public async createImmediateReport(baseParams: BaseParams) { + const { objectType: _objectType, ...params } = baseParams; // objectType is not needed for immediate download api + return this.http.post(`${API_GENERATE_IMMEDIATE}`, { body: JSON.stringify(params) }); + } + + public getDecoratedJobParams(baseParams: T): BaseParams { + // If the TZ is set to the default "Browser", it will not be useful for + // server-side export. We need to derive the timezone and pass it as a param + // to the export API. + const browserTimezone: string = + this.uiSettings.get('dateFormat:tz') === 'Browser' + ? moment.tz.guess() + : this.uiSettings.get('dateFormat:tz'); + + return { + browserTimezone, + version: this.kibanaVersion, + ...baseParams, + }; + } + public getManagementLink: ManagementLinkFn = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 661370446011f..518c8ef11857a 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -26,7 +26,8 @@ const mockJobsFound: Job[] = [ { id: 'job-source-mock3', status: 'pending', output: { csv_contains_formulas: false, max_size_reached: false }, payload: { title: 'specimen' } }, ].map((j) => new Job(j as ReportApiJSON)); // prettier-ignore -const jobQueueClientMock = new ReportingAPIClient(coreMock.createSetup().http); +const coreSetup = coreMock.createSetup(); +const jobQueueClientMock = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); jobQueueClientMock.findForJobIds = async () => mockJobsFound; jobQueueClientMock.getInfo = () => Promise.resolve(({ content: 'this is the completed report data' } as unknown) as Job); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx index 147e18410200b..c52027355ac5e 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { Job } from '../lib/job'; import { ReportInfoButton } from './report_info_button'; @@ -14,8 +15,9 @@ jest.mock('../lib/reporting_api_client'); import { ReportingAPIClient } from '../lib/reporting_api_client'; -const httpSetup = {} as any; -const apiClient = new ReportingAPIClient(httpSetup); +const coreSetup = coreMock.createSetup(); +const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); + const job = new Job({ id: 'abc-123', index: '.reporting-2020.04.12', @@ -29,6 +31,7 @@ const job = new Job({ meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', + version: '7.15.0-test', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index 47969edb72fda..dd8b60801066f 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -32,15 +32,15 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { }); const mockJobs: ReportApiJSON[] = [ - { id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000 }, // prettier-ignore - { id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 }, - { id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 }, - { id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 }, - { id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 }, - { id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 }, - { id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 }, - { id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 }, - { id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 }, + { id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000}, // prettier-ignore + { id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 }, + { id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 }, + { id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded' ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 }, + { id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 }, + { id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 }, + { id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 }, + { id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 }, + { id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count', version: '7.14.0' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 }, ]; // prettier-ignore const reportingAPIClient = { diff --git a/x-pack/plugins/reporting/public/mocks.ts b/x-pack/plugins/reporting/public/mocks.ts index a6b6d835499c6..41b4d26dc5a59 100644 --- a/x-pack/plugins/reporting/public/mocks.ts +++ b/x-pack/plugins/reporting/public/mocks.ts @@ -6,6 +6,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingSetup } from '.'; import { getDefaultLayoutSelectors } from '../common'; import { getSharedComponents } from './shared'; @@ -14,10 +15,11 @@ type Setup = jest.Mocked; const createSetupContract = (): Setup => { const coreSetup = coreMock.createSetup(); + const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); return { getDefaultLayoutSelectors: jest.fn().mockImplementation(getDefaultLayoutSelectors), usesUiCapabilities: jest.fn().mockImplementation(() => true), - components: getSharedComponents(coreSetup), + components: getSharedComponents(coreSetup, apiClient), }; }; 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 dbd0421fdf9b0..45bd20df85660 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 @@ -8,13 +8,17 @@ import * as Rx from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { LicensingPluginSetup } from '../../../licensing/public'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingCsvPanelAction } from './get_csv_panel_action'; type LicenseResults = 'valid' | 'invalid' | 'unavailable' | 'expired'; +const core = coreMock.createSetup(); +let apiClient: ReportingAPIClient; + describe('GetCsvReportPanelAction', () => { - let core: any; let context: any; let mockLicense$: any; let mockSearchSource: any; @@ -32,6 +36,9 @@ describe('GetCsvReportPanelAction', () => { }); beforeEach(() => { + apiClient = new ReportingAPIClient(core.http, core.uiSettings, '7.15.0'); + jest.spyOn(apiClient, 'createImmediateReport'); + mockLicense$ = (state: LicenseResults = 'valid') => { return (Rx.of({ check: jest.fn().mockImplementation(() => ({ state })), @@ -47,21 +54,6 @@ describe('GetCsvReportPanelAction', () => { null, ]; - core = { - http: { - post: jest.fn().mockImplementation(() => Promise.resolve(true)), - }, - notifications: { - toasts: { - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }, - uiSettings: { - get: () => 'Browser', - }, - } as any; - mockSearchSource = { createCopy: () => mockSearchSource, removeField: jest.fn(), @@ -92,6 +84,7 @@ describe('GetCsvReportPanelAction', () => { it('translates empty embeddable context into job params', async () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -101,12 +94,14 @@ describe('GetCsvReportPanelAction', () => { await panel.execute(context); - expect(core.http.post).toHaveBeenCalledWith( - '/api/reporting/v1/generate/immediate/csv_searchsource', - { - body: '{"searchSource":{},"columns":[],"browserTimezone":"America/New_York"}', - } - ); + expect(apiClient.createImmediateReport).toHaveBeenCalledWith({ + browserTimezone: undefined, + columns: [], + objectType: 'downloadCsv', + searchSource: {}, + title: undefined, + version: '7.15.0', + }); }); it('translates embeddable context into job params', async () => { @@ -126,6 +121,7 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -135,18 +131,20 @@ describe('GetCsvReportPanelAction', () => { 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"}', - } - ); + expect(apiClient.createImmediateReport).toHaveBeenCalledWith({ + browserTimezone: undefined, + columns: ['column_a', 'column_b'], + objectType: 'downloadCsv', + searchSource: { testData: 'testDataValue' }, + title: undefined, + version: '7.15.0', + }); }); it('allows downloading for valid licenses', async () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -162,6 +160,7 @@ describe('GetCsvReportPanelAction', () => { it('shows a good old toastie when it successfully starts', async () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -176,14 +175,10 @@ describe('GetCsvReportPanelAction', () => { }); it('shows a bad old toastie when it successfully fails', async () => { - const coreFails = { - ...core, - http: { - post: jest.fn().mockImplementation(() => Promise.reject('No more ram!')), - }, - }; + apiClient.createImmediateReport = jest.fn().mockRejectedValue('No more ram!'); const panel = new ReportingCsvPanelAction({ - core: coreFails, + core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -200,6 +195,7 @@ describe('GetCsvReportPanelAction', () => { const licenseMock$ = mockLicense$('invalid'); const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: licenseMock$, startServices$: mockStartServices$, usesUiCapabilities: true, @@ -215,6 +211,7 @@ describe('GetCsvReportPanelAction', () => { it('sets a display and icon type', () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -230,6 +227,7 @@ describe('GetCsvReportPanelAction', () => { it(`doesn't allow downloads when UI capability is not enabled`, async () => { const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -248,6 +246,7 @@ describe('GetCsvReportPanelAction', () => { mockStartServices$ = new Rx.Subject(); const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -261,6 +260,7 @@ describe('GetCsvReportPanelAction', () => { it(`allows download when license is valid and deprecated roles config is enabled`, async () => { const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: false, 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 8a863e1ceaa65..8b6e258c06535 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 @@ -6,9 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; import * as Rx from 'rxjs'; -import type { CoreSetup } from 'src/core/public'; +import type { CoreSetup, IUiSettingsClient, NotificationsSetup } from 'src/core/public'; import { CoreStart } from 'src/core/public'; import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public'; import { @@ -20,9 +19,9 @@ 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 type { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; +import { CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable @@ -35,6 +34,7 @@ interface ActionContext { } interface Params { + apiClient: ReportingAPIClient; core: CoreSetup; startServices$: Rx.Observable<[CoreStart, object, unknown]>; license$: LicensingPluginSetup['license$']; @@ -47,11 +47,16 @@ export class ReportingCsvPanelAction implements ActionDefinition public readonly id = CSV_REPORTING_ACTION; private licenseHasDownloadCsv: boolean = false; private capabilityHasDownloadCsv: boolean = false; - private core: CoreSetup; + private uiSettings: IUiSettingsClient; + private notifications: NotificationsSetup; + private apiClient: ReportingAPIClient; - constructor({ core, startServices$, license$, usesUiCapabilities }: Params) { + constructor({ core, startServices$, license$, usesUiCapabilities, apiClient }: Params) { this.isDownloading = false; - this.core = core; + + this.uiSettings = core.uiSettings; + this.notifications = core.notifications; + this.apiClient = apiClient; license$.subscribe((license) => { const results = license.check('reporting', 'basic'); @@ -83,7 +88,7 @@ export class ReportingCsvPanelAction implements ActionDefinition return await getSharingData( savedSearch.searchSource, savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977 - this.core.uiSettings + this.uiSettings ); } @@ -111,24 +116,16 @@ export class ReportingCsvPanelAction implements ActionDefinition const savedSearch = embeddable.getSavedSearch(); const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable); - // If the TZ is set to the default "Browser", it will not be useful for - // server-side export. We need to derive the timezone and pass it as a param - // to the export API. - // TODO: create a helper utility in Reporting. This is repeated in a few places. - const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); - const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; - const immediateJobParams: JobParamsDownloadCSV = { + const immediateJobParams = this.apiClient.getDecoratedJobParams({ searchSource, columns, - browserTimezone, title: savedSearch.title, - }; - - const body = JSON.stringify(immediateJobParams); + objectType: 'downloadCsv', // FIXME: added for typescript, but immediate download job does not need objectType + }); this.isDownloading = true; - this.core.notifications.toasts.addSuccess({ + this.notifications.toasts.addSuccess({ title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', { defaultMessage: `CSV Download Started`, }), @@ -138,9 +135,9 @@ export class ReportingCsvPanelAction implements ActionDefinition 'data-test-subj': 'csvDownloadStarted', }); - await this.core.http - .post(`${API_GENERATE_IMMEDIATE}`, { body }) - .then((rawResponse: string) => { + await this.apiClient + .createImmediateReport(immediateJobParams) + .then((rawResponse) => { this.isDownloading = false; const download = `${savedSearch.title}.csv`; @@ -166,7 +163,7 @@ export class ReportingCsvPanelAction implements ActionDefinition private onGenerationFail(error: Error) { this.isDownloading = false; - this.core.notifications.toasts.addDanger({ + this.notifications.toasts.addDanger({ title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', { defaultMessage: `CSV download failed`, }), diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 44ecc01bd1eb3..757f226532d95 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -11,6 +11,8 @@ import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { CoreSetup, CoreStart, + HttpSetup, + IUiSettingsClient, NotificationsSetup, Plugin, PluginInitializerContext, @@ -32,15 +34,14 @@ import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_ha import { getGeneralErrorToast } from './notifier'; import { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action'; import { getSharedComponents } from './shared'; -import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; -import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; - import type { SharePluginSetup, SharePluginStart, UiActionsSetup, UiActionsStart, } from './shared_imports'; +import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; +import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; @@ -89,6 +90,8 @@ export class ReportingPublicPlugin ReportingPublicPluginSetupDendencies, ReportingPublicPluginStartDendencies > { + private kibanaVersion: string; + private apiClient?: ReportingAPIClient; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -101,6 +104,17 @@ export class ReportingPublicPlugin constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + + /* + * Use a single instance of ReportingAPIClient for all the reporting code + */ + private getApiClient(http: HttpSetup, uiSettings: IUiSettingsClient) { + if (!this.apiClient) { + this.apiClient = new ReportingAPIClient(http, uiSettings, this.kibanaVersion); + } + return this.apiClient; } private getContract(core?: CoreSetup) { @@ -108,7 +122,7 @@ export class ReportingPublicPlugin this.contract = { getDefaultLayoutSelectors, usesUiCapabilities: () => this.config.roles?.enabled === false, - components: getSharedComponents(core), + components: getSharedComponents(core, this.getApiClient(core.http, core.uiSettings)), }; } @@ -120,11 +134,11 @@ export class ReportingPublicPlugin } public setup(core: CoreSetup, setupDeps: ReportingPublicPluginSetupDendencies) { - const { http, getStartServices, uiSettings } = core; + const { getStartServices, uiSettings } = core; const { home, management, - licensing: { license$ }, + licensing: { license$ }, // FIXME: 'license$' is deprecated share, uiActions, } = setupDeps; @@ -132,7 +146,7 @@ export class ReportingPublicPlugin const startServices$ = Rx.from(getStartServices()); const usesUiCapabilities = !this.config.roles.enabled; - const apiClient = new ReportingAPIClient(http); + const apiClient = this.getApiClient(core.http, core.uiSettings); home.featureCatalogue.register({ id: 'reporting', @@ -181,7 +195,7 @@ export class ReportingPublicPlugin uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, - new ReportingCsvPanelAction({ core, startServices$, license$, usesUiCapabilities }) + new ReportingCsvPanelAction({ core, apiClient, startServices$, license$, usesUiCapabilities }) ); const reportingStart = this.getContract(core); @@ -213,8 +227,8 @@ export class ReportingPublicPlugin } public start(core: CoreStart) { - const { http, notifications } = core; - const apiClient = new ReportingAPIClient(http); + const { notifications } = core; + const apiClient = this.getApiClient(core.http, core.uiSettings); const streamHandler = new StreamHandler(notifications, apiClient); const interval = durationToNumber(this.config.poll.jobsRefresh.interval); Rx.timer(0, interval) diff --git a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap index 83dc0c9e215b0..6f0fc18e90adc 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap +++ b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -349,7 +349,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout ; + usesUiCapabilities: boolean; +} + +export interface ReportingSharingData { + title: string; + layout: LayoutParams; +} + +export interface JobParamsProviderOptions { + sharingData: ReportingSharingData; + shareableUrl: string; + objectType: string; +} 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 7165fcf6f8681..040a1646ec1ba 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 @@ -6,35 +6,22 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; import React from 'react'; -import * as Rx from 'rxjs'; -import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { CoreStart } from 'src/core/public'; import type { SearchSourceFields } from 'src/plugins/data/common'; +import { ExportPanelShareOpts } from '.'; import type { ShareContext } from '../../../../../src/plugins/share/public'; -import type { LicensingPluginSetup } from '../../../licensing/public'; import { CSV_JOB_TYPE } from '../../common/constants'; -import type { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; import { checkLicense } from '../lib/license_check'; -import type { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingPanelContent } from './reporting_panel_content_lazy'; export const ReportingCsvShareProvider = ({ apiClient, toasts, + uiSettings, license$, startServices$, - uiSettings, usesUiCapabilities, -}: { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - license$: LicensingPluginSetup['license$']; - startServices$: Rx.Observable<[CoreStart, object, unknown]>; - uiSettings: IUiSettingsClient; - usesUiCapabilities: boolean; -}) => { +}: ExportPanelShareOpts) => { let licenseToolTipContent = ''; let licenseHasCsvReporting = false; let licenseDisabled = true; @@ -56,22 +43,12 @@ export const ReportingCsvShareProvider = ({ capabilityHasCsvReporting = true; // deprecated } - // If the TZ is set to the default "Browser", it will not be useful for - // server-side export. We need to derive the timezone and pass it as a param - // to the export API. - // TODO: create a helper utility in Reporting. This is repeated in a few places. - const browserTimezone = - uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : uiSettings.get('dateFormat:tz'); - const getShareMenuItems = ({ objectType, objectId, sharingData, onClose }: ShareContext) => { if ('search' !== objectType) { return []; } - const jobParams: JobParamsCSV = { - browserTimezone, + const jobParams = { title: sharingData.title as string, objectType, searchSource: sharingData.searchSource as SearchSourceFields, @@ -104,6 +81,7 @@ export const ReportingCsvShareProvider = ({ requiresSavedState={false} apiClient={apiClient} toasts={toasts} + uiSettings={uiSettings} reportType={CSV_JOB_TYPE} layoutId={undefined} objectId={objectId} 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 eb80f64be55e1..b37e31578be6d 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 @@ -6,83 +6,53 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; import React from 'react'; -import * as Rx from 'rxjs'; -import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { CoreStart } from 'src/core/public'; import { 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 { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.'; import { checkLicense } from '../lib/license_check'; -import type { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy'; -interface JobParamsProviderOptions { - shareableUrl: string; - apiClient: ReportingAPIClient; - objectType: string; - browserTimezone: string; - sharingData: Record; -} - -const jobParamsProvider = ({ - objectType, - browserTimezone, - sharingData, -}: JobParamsProviderOptions) => { - return { +const getJobParams = ( + apiClient: ReportingAPIClient, + opts: JobParamsProviderOptions, + type: 'pdf' | 'png' +) => () => { + const { objectType, - browserTimezone, - layout: sharingData.layout as LayoutParams, - title: sharingData.title as string, + sharingData: { title, layout }, + } = opts; + + const baseParams = { + objectType, + layout, + title, }; -}; -const getPdfJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDF => { // Relative URL must have URL prefix (Spaces ID prefix), but not server basePath // Replace hashes with original RISON values. const relativeUrl = opts.shareableUrl.replace( - window.location.origin + opts.apiClient.getServerBasePath(), + window.location.origin + apiClient.getServerBasePath(), '' ); - return { - ...jobParamsProvider(opts), - relativeUrls: [relativeUrl], // multi URL for PDF - }; -}; - -const getPngJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPNG => { - // Replace hashes with original RISON values. - const relativeUrl = opts.shareableUrl.replace( - window.location.origin + opts.apiClient.getServerBasePath(), - '' - ); + if (type === 'pdf') { + // multi URL for PDF + return { ...baseParams, relativeUrls: [relativeUrl] }; + } - return { - ...jobParamsProvider(opts), - relativeUrl, // single URL for PNG - }; + // single URL for PNG + return { ...baseParams, relativeUrl }; }; export const reportingScreenshotShareProvider = ({ apiClient, toasts, + uiSettings, license$, startServices$, - uiSettings, usesUiCapabilities, -}: { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - license$: LicensingPluginSetup['license$']; - startServices$: Rx.Observable<[CoreStart, object, unknown]>; - uiSettings: IUiSettingsClient; - usesUiCapabilities: boolean; -}) => { +}: ExportPanelShareOpts) => { let licenseToolTipContent = ''; let licenseDisabled = true; let licenseHasScreenshotReporting = false; @@ -110,22 +80,13 @@ export const reportingScreenshotShareProvider = ({ capabilityHasVisualizeScreenshotReporting = true; } - // If the TZ is set to the default "Browser", it will not be useful for - // server-side export. We need to derive the timezone and pass it as a param - // to the export API. - // TODO: create a helper utility in Reporting. This is repeated in a few places. - const browserTimezone = - uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : uiSettings.get('dateFormat:tz'); - const getShareMenuItems = ({ objectType, objectId, - sharingData, isDirty, onClose, shareableUrl, + ...shareOpts }: ShareContext) => { if (!licenseHasScreenshotReporting) { return []; @@ -143,6 +104,7 @@ export const reportingScreenshotShareProvider = ({ return []; } + const { sharingData } = (shareOpts as unknown) as { sharingData: ReportingSharingData }; const shareActions = []; const pngPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pngReportsButtonLabel', { @@ -165,16 +127,11 @@ export const reportingScreenshotShareProvider = ({ @@ -202,17 +159,12 @@ export const reportingScreenshotShareProvider = ({ diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx index 6c5b8df104ecd..6ad894bf3ac2f 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx @@ -7,26 +7,56 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; -import { notificationServiceMock } from 'src/core/public/mocks'; - -import { ReportingPanelContent, Props } from './reporting_panel_content'; +import { + httpServiceMock, + notificationServiceMock, + uiSettingsServiceMock, +} from 'src/core/public/mocks'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingPanelContent, ReportingPanelProps as Props } from './reporting_panel_content'; describe('ReportingPanelContent', () => { - const mountComponent = (props: Partial) => + const props: Partial = { + layoutId: 'super_cool_layout_id_X', + }; + const jobParams = { + appState: 'very_cool_app_state_X', + objectType: 'noice_object', + title: 'ultimate_title', + }; + const toasts = notificationServiceMock.createSetupContract().toasts; + const http = httpServiceMock.createSetupContract(); + const uiSettings = uiSettingsServiceMock.createSetupContract(); + let apiClient: ReportingAPIClient; + + beforeEach(() => { + props.layoutId = 'super_cool_layout_id_X'; + uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case 'dateFormat:tz': + return 'Mars'; + } + }); + apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0-test'); + }); + + const mountComponent = (newProps: Partial) => mountWithIntl( 'test' } as any} - toasts={notificationServiceMock.createSetupContract().toasts} + objectId="my-object-id" + layoutId={props.layoutId} + getJobParams={() => jobParams} + apiClient={apiClient} + toasts={toasts} + uiSettings={uiSettings} {...props} + {...newProps} /> ); + describe('saved state', () => { it('prevents generating reports when saving is required and we have unsaved changes', () => { const wrapper = mountComponent({ @@ -51,5 +81,20 @@ describe('ReportingPanelContent', () => { false ); }); + + it('changing the layout triggers refreshing the state with the latest job params', () => { + const wrapper = mountComponent({ requiresSavedState: false }); + wrapper.update(); + expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( + `"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_cool_app_state_X%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"` + ); + + jobParams.appState = 'very_NOT_cool_app_state_Y'; + wrapper.setProps({ layoutId: 'super_cool_layout_id_Y' }); // update the component internal state + wrapper.update(); + expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( + `"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_NOT_cool_app_state_Y%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"` + ); + }); }); }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx index 4d7828b789407..af6cd0010de09 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx @@ -18,29 +18,30 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; -import { ToastsSetup } from 'src/core/public'; +import { ToastsSetup, IUiSettingsClient } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -export interface Props { +export interface ReportingPanelProps { apiClient: ReportingAPIClient; toasts: ToastsSetup; + uiSettings: IUiSettingsClient; reportType: string; - /** Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. **/ - requiresSavedState: boolean; - layoutId: string | undefined; + requiresSavedState: boolean; // Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. + layoutId?: string; objectId?: string; - getJobParams: () => BaseParams; + getJobParams: () => Omit; options?: ReactElement | null; isDirty?: boolean; onClose?: () => void; - intl: InjectedIntl; } +export type Props = ReportingPanelProps & { intl: InjectedIntl }; + interface State { isStale: boolean; absoluteUrl: string; @@ -68,12 +69,12 @@ class ReportingPanelContentUi extends Component { private getAbsoluteReportGenerationUrl = (props: Props) => { const relativePath = this.props.apiClient.getReportingJobPath( props.reportType, - props.getJobParams() + this.props.apiClient.getDecoratedJobParams(this.props.getJobParams()) ); - return url.resolve(window.location.href, relativePath); + return url.resolve(window.location.href, relativePath); // FIXME: '(from: string, to: string): string' is deprecated }; - public componentDidUpdate(prevProps: Props, prevState: State) { + public componentDidUpdate(_prevProps: Props, prevState: State) { if (this.props.layoutId && this.props.layoutId !== prevState.layoutId) { this.setState({ ...prevState, @@ -231,9 +232,12 @@ class ReportingPanelContentUi extends Component { private createReportingJob = () => { const { intl } = this.props; + const decoratedJobParams = this.props.apiClient.getDecoratedJobParams( + this.props.getJobParams() + ); return this.props.apiClient - .createReportingJob(this.props.reportType, this.props.getJobParams()) + .createReportingJob(this.props.reportType, decoratedJobParams) .then(() => { this.props.toasts.addSuccess({ title: intl.formatMessage( diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx index a023eae512d54..3fdb2c7e98f82 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx @@ -8,25 +8,33 @@ import { mount } from 'enzyme'; import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; -import { coreMock } from '../../../../../src/core/public/mocks'; -import { BaseParams } from '../../common/types'; +import { coreMock } from 'src/core/public/mocks'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ScreenCapturePanelContent } from './screen_capture_panel_content'; -const getJobParamsDefault: () => BaseParams = () => ({ +const { http, uiSettings, ...coreSetup } = coreMock.createSetup(); +uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case 'dateFormat:tz': + return 'Mars'; + } +}); +const apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0'); + +const getJobParamsDefault = () => ({ objectType: 'test-object-type', title: 'Test Report Title', browserTimezone: 'America/New_York', }); test('ScreenCapturePanelContent renders the default view properly', () => { - const coreSetup = coreMock.createSetup(); const component = mount( @@ -38,14 +46,14 @@ test('ScreenCapturePanelContent renders the default view properly', () => { }); test('ScreenCapturePanelContent properly renders a view with "canvas" layout option', () => { - const coreSetup = coreMock.createSetup(); const component = mount( @@ -56,14 +64,14 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt }); test('ScreenCapturePanelContent properly renders a view with "print" layout option', () => { - const coreSetup = coreMock.createSetup(); const component = mount( @@ -72,3 +80,22 @@ test('ScreenCapturePanelContent properly renders a view with "print" layout opti expect(component.find('EuiForm')).toMatchSnapshot(); expect(component.text()).toMatch('Optimize for printing'); }); + +test('ScreenCapturePanelContent decorated job params are visible in the POST URL', () => { + const component = mount( + + + + ); + + expect(component.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( + `"http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%2Cversion%3A%277.15.0%27%29"` + ); +}); diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx index fd6003f8656e8..73c4d10856a53 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx @@ -7,24 +7,13 @@ import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; import React, { Component } from 'react'; -import { ToastsSetup } from 'src/core/public'; import { getDefaultLayoutSelectors } from '../../common'; -import { BaseParams, LayoutParams } from '../../common/types'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { ReportingPanelContent } from './reporting_panel_content'; - -export interface Props { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - reportType: string; +import { LayoutParams } from '../../common/types'; +import { ReportingPanelContent, ReportingPanelProps } from './reporting_panel_content'; + +export interface Props extends ReportingPanelProps { layoutOption?: 'canvas' | 'print'; - objectId?: string; - getJobParams: () => BaseParams; - requiresSavedState: boolean; - isDirty?: boolean; - onClose?: () => void; } interface State { @@ -45,16 +34,10 @@ export class ScreenCapturePanelContent extends Component { public render() { return ( ); } @@ -147,17 +130,10 @@ export class ScreenCapturePanelContent extends Component { return { id: 'preserve_layout', dimensions, selectors }; }; - private getJobParams = (): Required => { - const outerParams = this.props.getJobParams(); - let browserTimezone = outerParams.browserTimezone; - if (!browserTimezone) { - browserTimezone = moment.tz.guess(); - } - + private getJobParams = () => { return { ...this.props.getJobParams(), layout: this.getLayout(), - browserTimezone, }; }; } diff --git a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx index 87ddf0cfdb389..659eaf2678164 100644 --- a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx +++ b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx @@ -23,7 +23,7 @@ type PropsPDF = Pick & * This is not planned to expand, as work is to be done on moving the export-type implementations out of Reporting * Related Discuss issue: https://github.com/elastic/kibana/issues/101422 */ -export function getSharedComponents(core: CoreSetup) { +export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) { return { ReportingPanelPDF(props: PropsPDF) { return ( @@ -31,8 +31,9 @@ export function getSharedComponents(core: CoreSetup) { layoutOption={props.layoutOption} requiresSavedState={false} reportType={PDF_REPORT_TYPE} - apiClient={new ReportingAPIClient(core.http)} + apiClient={apiClient} toasts={core.notifications.toasts} + uiSettings={core.uiSettings} {...props} /> ); diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index b7f3ebe9dcfa8..708b9b1bdbea5 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -58,6 +58,7 @@ export interface ReportingInternalStart { } export class ReportingCore { + private kibanaVersion: string; private pluginSetupDeps?: ReportingInternalSetup; private pluginStartDeps?: ReportingInternalStart; private readonly pluginSetup$ = new Rx.ReplaySubject(); // observe async background setupDeps and config each are done @@ -72,6 +73,7 @@ export class ReportingCore { public getContract: () => ReportingSetup; constructor(private logger: LevelLogger, context: PluginInitializerContext) { + this.kibanaVersion = context.env.packageInfo.version; const syncConfig = context.config.get(); this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false; this.executeTask = new ExecuteReportTask(this, syncConfig, this.logger); @@ -84,6 +86,10 @@ export class ReportingCore { this.executing = new Set(); } + public getKibanaVersion() { + return this.kibanaVersion; + } + /* * Register setupDeps */ diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts index c9d57370ab766..b96828bb06334 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -59,6 +59,7 @@ test('gets the csv content from job parameters', async () => { searchSource: {}, objectType: 'search', title: 'Test Search', + version: '7.13.0', }, new CancellationToken() ); 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 a38e0d58abf89..7eaf1ef95c149 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 @@ -12,6 +12,7 @@ import { IScopedSearchClient } from 'src/plugins/data/server'; import { Datatable } from 'src/plugins/expressions/server'; import { ReportingConfig } from '../../..'; import { + cellHasFormulas, ES_SEARCH_STRATEGY, FieldFormat, FieldFormatConfig, @@ -22,7 +23,6 @@ import { SearchFieldValue, SearchSourceFields, tabifyDocs, - cellHasFormulas, } from '../../../../../../../src/plugins/data/common'; import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server'; import { CancellationToken } from '../../../../common'; @@ -68,7 +68,7 @@ export class CsvGenerator { private csvRowCount = 0; constructor( - private job: JobParamsCSV, + private job: Omit, private config: ReportingConfig, private clients: Clients, private dependencies: Dependencies, @@ -219,7 +219,6 @@ export class CsvGenerator { */ private generateHeader( columns: string[], - table: Datatable, builder: MaxSizeStringBuilder, settings: CsvExportSettings ) { @@ -357,7 +356,7 @@ export class CsvGenerator { if (first) { first = false; - this.generateHeader(columns, table, builder, settings); + this.generateHeader(columns, builder, settings); } if (table.rows.length < 1) { 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 d2a9e2b5bf783..170b03c2dfbff 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 @@ -11,7 +11,6 @@ import type { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; interface BaseParamsCSV { - browserTimezone: string; searchSource: SearchSourceFields; columns?: string[]; } diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts index c8475e85bd847..e59c38e16ab47 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -32,7 +32,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const config = reporting.getConfig(); const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']); - return async function runTask(jobId, immediateJobParams, context, req) { + return async function runTask(_jobId, immediateJobParams, context, req) { const job = { objectType: 'immediate-search', ...immediateJobParams, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 488a339e3ef4b..cbfbc4a4d34b2 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -16,24 +16,16 @@ export const createJobFnFactory: CreateJobFnFactory< const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJob( - { objectType, title, relativeUrl, browserTimezone, layout }, - context, - req - ) { + return async function createJob(jobParams, _context, req) { const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - validateUrls([relativeUrl]); + validateUrls([jobParams.relativeUrl]); return { headers: serializedEncryptedHeaders, spaceId: reporting.getSpaceId(req, logger), - objectType, - title, - relativeUrl, - browserTimezone, - layout, forceNow: new Date().toISOString(), + ...jobParams, }; }; }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index c0f30f96415f4..9dac1560ddbdc 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -16,24 +16,16 @@ export const createJobFnFactory: CreateJobFnFactory< const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJob( - { title, relativeUrls, browserTimezone, layout, objectType }, - context, - req - ) { + return async function createJob(jobParams, _context, req) { const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - validateUrls(relativeUrls); + validateUrls(jobParams.relativeUrls); return { headers: serializedEncryptedHeaders, spaceId: reporting.getSpaceId(req, logger), - browserTimezone, forceNow: new Date().toISOString(), - layout, - relativeUrls, - title, - objectType, + ...jobParams, }; }; }; diff --git a/x-pack/plugins/reporting/server/lib/check_params_version.ts b/x-pack/plugins/reporting/server/lib/check_params_version.ts new file mode 100644 index 0000000000000..7298384b87571 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/check_params_version.ts @@ -0,0 +1,20 @@ +/* + * 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 { UNVERSIONED_VERSION } from '../../common/constants'; +import type { BaseParams } from '../../common/types'; +import type { LevelLogger } from './'; + +export function checkParamsVersion(jobParams: BaseParams, logger: LevelLogger) { + if (jobParams.version) { + logger.debug(`Using reporting job params v${jobParams.version}`); + return jobParams.version; + } + + logger.warning(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); + return UNVERSIONED_VERSION; +} diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts index d9d1815835baa..abfa2b88258fc 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts @@ -14,17 +14,28 @@ import { createMockLevelLogger, createMockReportingCore, } from '../test_helpers'; -import { BasePayload, ReportingRequestHandlerContext } from '../types'; +import { ReportingRequestHandlerContext } from '../types'; import { ExportTypesRegistry, ReportingStore } from './'; import { enqueueJobFactory } from './enqueue_job'; import { Report } from './store'; -import { TaskRunResult } from './tasks'; describe('Enqueue Job', () => { const logger = createMockLevelLogger(); let mockReporting: ReportingCore; let mockExportTypesRegistry: ExportTypesRegistry; + const mockBaseParams = { + browserTimezone: 'UTC', + headers: 'cool_encrypted_headers', + objectType: 'cool_object_type', + title: 'cool_title', + version: 'unknown' as any, + }; + + beforeEach(() => { + mockBaseParams.version = '7.15.0-test'; + }); + beforeAll(async () => { mockExportTypesRegistry = new ExportTypesRegistry(); mockExportTypesRegistry.register({ @@ -34,10 +45,8 @@ describe('Enqueue Job', () => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['turquoise'], - createJobFnFactory: () => async () => - (({ createJobTest: { test1: 'yes' } } as unknown) as BasePayload), - runTaskFnFactory: () => async () => - (({ runParamsTest: { test2: 'yes' } } as unknown) as TaskRunResult), + createJobFnFactory: () => async () => mockBaseParams, + runTaskFnFactory: jest.fn(), }); mockReporting = await createMockReportingCore(createMockConfigSchema()); mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry; @@ -66,26 +75,59 @@ describe('Enqueue Job', () => { const enqueueJob = enqueueJobFactory(mockReporting, logger); const report = await enqueueJob( 'printablePdf', - { - objectType: 'visualization', - title: 'cool-viz', - }, + mockBaseParams, false, ({} as unknown) as ReportingRequestHandlerContext, ({} as unknown) as KibanaRequest ); - expect(report).toMatchObject({ - _id: expect.any(String), - _index: '.reporting-foo-index-234', - attempts: 0, - created_by: false, - created_at: expect.any(String), - jobtype: 'printable_pdf', - meta: { objectType: 'visualization' }, - output: null, - payload: { createJobTest: { test1: 'yes' } }, - status: 'pending', - }); + const { _id, created_at: _created_at, ...snapObj } = report; + expect(snapObj).toMatchInlineSnapshot(` + Object { + "_index": ".reporting-foo-index-234", + "_primary_term": undefined, + "_seq_no": undefined, + "attempts": 0, + "browser_type": undefined, + "completed_at": undefined, + "created_by": false, + "jobtype": "printable_pdf", + "kibana_id": undefined, + "kibana_name": undefined, + "max_attempts": undefined, + "meta": Object { + "layout": undefined, + "objectType": "cool_object_type", + }, + "migration_version": "7.14.0", + "output": null, + "payload": Object { + "browserTimezone": "UTC", + "headers": "cool_encrypted_headers", + "objectType": "cool_object_type", + "title": "cool_title", + "version": "7.15.0-test", + }, + "process_expiration": undefined, + "started_at": undefined, + "status": "pending", + "timeout": undefined, + } + `); + }); + + it('provides a default kibana version field for older POST URLs', async () => { + const enqueueJob = enqueueJobFactory(mockReporting, logger); + mockBaseParams.version = undefined; + const report = await enqueueJob( + 'printablePdf', + mockBaseParams, + false, + ({} as unknown) as ReportingRequestHandlerContext, + ({} as unknown) as KibanaRequest + ); + + const { _id, created_at: _created_at, ...snapObj } = report; + expect(snapObj.payload.version).toBe('7.14.0'); }); }); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index ec2e443d86c80..1c73b0d925ad0 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -7,10 +7,10 @@ import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; +import type { ReportingRequestHandlerContext } from '../types'; import { BaseParams, ReportingUser } from '../types'; -import { LevelLogger } from './'; +import { checkParamsVersion, LevelLogger } from './'; import { Report } from './store'; -import type { ReportingRequestHandlerContext } from '../types'; export type EnqueueJobFn = ( exportTypeId: string, @@ -47,6 +47,7 @@ export function enqueueJobFactory( reporting.getStore(), ]); + jobParams.version = checkParamsVersion(jobParams, logger); const job = await createJob!(jobParams, context, request); // 1. Add the report to ReportingStore to show as pending diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index b2a2a1edcd6a5..37f57d97d3d4c 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -6,6 +6,7 @@ */ export { checkLicense } from './check_license'; +export { checkParamsVersion } from './check_params_version'; export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; export { LevelLogger } from './level_logger'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index 4bc45fd745a56..f9cd413b3e5a7 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -15,7 +15,13 @@ describe('Class Report', () => { created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'cool report' }, + payload: { + headers: 'payload_test_field', + objectType: 'testOt', + title: 'cool report', + version: '7.14.0', + browserTimezone: 'UTC', + }, meta: { objectType: 'test' }, timeout: 30000, }); @@ -64,7 +70,13 @@ describe('Class Report', () => { created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'hot report' }, + payload: { + headers: 'payload_test_field', + objectType: 'testOt', + title: 'hot report', + version: '7.14.0', + browserTimezone: 'UTC', + }, meta: { objectType: 'stange' }, timeout: 30000, }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index f46e55c9cc41b..9bb9c8a113d3e 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -255,6 +255,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'ABC', + version: '7.14.0', }, timeout: 30000, }); @@ -285,6 +286,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'BCD', + version: '7.14.0', }, timeout: 30000, }); @@ -315,6 +317,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'CDE', + version: '7.14.0', }, timeout: 30000, }); @@ -345,6 +348,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'utc', + version: '7.14.0', }, timeout: 30000, }); @@ -390,6 +394,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'utc', + version: '7.14.0', }, timeout: 30000, }); 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 2da509f024c25..8d31c03c618c9 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -55,13 +55,14 @@ export function registerGenerateCsvFromSavedObjectImmediate( searchSource: schema.object({}, { unknowns: 'allow' }), browserTimezone: schema.string({ defaultValue: 'UTC' }), title: schema.string(), + version: schema.maybe(schema.string()), }), }, options: { tags: kibanaAccessControlTags, }, }, - userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { + userHandler(async (_user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['csv_searchsource_immediate']); const runTaskFn = runTaskFnFactory(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index 55d12e5c6d442..69b3f216886e6 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -95,7 +95,7 @@ export function registerGenerateFromJobParams( path: `${BASE_GENERATE}/{p*}`, validate: false, }, - (context, req, res) => { + (_context, _req, res) => { return res.customError({ statusCode: 405, body: 'GET is not allowed' }); } ); diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index c6889f3612b59..df5a85d71f49f 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import rison from 'rison-node'; import { UnwrapPromise } from '@kbn/utility-types'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { of } from 'rxjs'; @@ -129,7 +130,7 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/TonyHawksProSkater2') - .send({ jobParams: `abc` }) + .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(400) .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"') @@ -145,7 +146,7 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/printablePdf') - .send({ jobParams: `abc` }) + .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(500); }); @@ -157,7 +158,7 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/printablePdf') - .send({ jobParams: `abc` }) + .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(200) .then(({ body }) => { expect(body).toMatchObject({ diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts index 4dbf1b6fa5ebb..f260ada7fe15d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts @@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { }, browserTimezone: 'UTC', title: 'testfooyu78yt90-', + version: '7.13.0', } as any )) as supertest.Response; expect(res.status).to.eql(403); @@ -52,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { }, browserTimezone: 'UTC', title: 'testfooyu78yt90-', + version: '7.13.0', } as any )) as supertest.Response; expect(res.status).to.eql(200); @@ -69,6 +71,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'dashboard', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -84,6 +87,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'dashboard', + version: '7.14.0', } ); expect(res.status).to.eql(200); @@ -101,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'visualization', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -116,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'visualization', + version: '7.14.0', } ); expect(res.status).to.eql(200); @@ -133,6 +139,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'canvas', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -148,6 +155,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'canvas', + version: '7.14.0', } ); expect(res.status).to.eql(200); @@ -164,6 +172,7 @@ export default function ({ getService }: FtrProviderContext) { searchSource: {}, objectType: 'search', title: 'test disallowed', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -183,6 +192,7 @@ export default function ({ getService }: FtrProviderContext) { index: '5193f870-d861-11e9-a311-0fa548c5f953', } as any, columns: [], + version: '7.13.0', } ); expect(res.status).to.eql(200);