From e582439751653f34c9ef07e72463544ef059ec2e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 1 Jun 2021 15:36:22 +0200 Subject: [PATCH] reintroduced pdfv2 export type, see https://github.com/elastic/kibana/issues/99890\#issuecomment-851527878 --- .../application/top_nav/show_share_modal.tsx | 3 +- x-pack/plugins/reporting/common/constants.ts | 6 +- x-pack/plugins/reporting/common/job_utils.ts | 13 ++ x-pack/plugins/reporting/common/types.ts | 7 + .../register_pdf_png_reporting.tsx | 49 +++--- .../chromium/driver/chromium_driver.ts | 41 ++--- .../printable_pdf/create_job/index.ts | 3 +- .../printable_pdf/execute_job/index.ts | 5 +- .../printable_pdf/lib/generate_pdf.ts | 4 +- .../export_types/printable_pdf/types.d.ts | 22 +-- .../printable_pdf/v2/create_job.ts | 36 ++++ .../printable_pdf/v2/execute_job.ts | 80 +++++++++ .../export_types/printable_pdf/v2/index.ts | 39 +++++ .../printable_pdf/v2/lib/generate_pdf.ts | 119 +++++++++++++ .../v2/lib/get_custom_logo.test.ts | 57 +++++++ .../printable_pdf/v2/lib/get_custom_logo.ts | 29 ++++ .../v2/lib/pdf/get_doc_options.ts | 26 +++ .../printable_pdf/v2/lib/pdf/get_font.test.ts | 34 ++++ .../printable_pdf/v2/lib/pdf/get_font.ts | 18 ++ .../printable_pdf/v2/lib/pdf/get_template.ts | 137 +++++++++++++++ .../printable_pdf/v2/lib/pdf/index.test.ts | 77 +++++++++ .../printable_pdf/v2/lib/pdf/index.ts | 157 ++++++++++++++++++ .../printable_pdf/v2/lib/tracker.ts | 88 ++++++++++ .../printable_pdf/v2/lib/uri_encode.js | 32 ++++ .../export_types/printable_pdf/v2/metadata.ts | 11 ++ .../export_types/printable_pdf/v2/types.ts | 34 ++++ .../reporting/server/lib/screenshots/index.ts | 1 - .../server/lib/screenshots/observable.ts | 4 +- .../server/lib/screenshots/open_url.ts | 5 +- 29 files changed, 1058 insertions(+), 79 deletions(-) create mode 100644 x-pack/plugins/reporting/common/job_utils.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/create_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/execute_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/index.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/generate_pdf.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_doc_options.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_template.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/tracker.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/uri_encode.js create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/metadata.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/v2/types.ts diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx index 091c52b2182da..adb553df6df7f 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx @@ -114,8 +114,9 @@ export function ShowShareModal({ objectType: 'dashboard', sharingData: { title: savedDashboard.title, - body: { + locator: { version: '1', + id: 'test', // TODO: Use real dashboard locator ID value: dashboardStateManager.appState, }, }, diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index ad5bd7e2176e2..4b7ceffc9e78d 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -62,16 +62,18 @@ export const CSV_REPORT_TYPE = 'CSV'; export const CSV_JOB_TYPE = 'csv_searchsource'; export const PDF_REPORT_TYPE = 'printablePdf'; +export const PDF_REPORT_TYPE_V2 = 'printablePdfV2'; export const PDF_JOB_TYPE = 'printable_pdf'; export const PDF_JOB_TYPE_V2 = 'printable_pdf_v2'; export const PNG_REPORT_TYPE = 'PNG'; +export const PNG_REPORT_TYPE_V2 = 'PNGV2'; export const PNG_JOB_TYPE = 'PNG'; -export const PNG_JOB_TYPE_V2 = 'PNG_v2'; +export const PNG_JOB_TYPE_V2 = 'PNGV2'; export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate'; -export const REPORT_BODY_STORE_KEY = '__reportBodyStore__'; +export const REPORT_LOCATOR_STORE_KEY = '__reportLocatorStore__'; // This is deprecated because it lacks support for runtime fields // but the extension points are still needed for pre-existing scripted automation, until 8.0 diff --git a/x-pack/plugins/reporting/common/job_utils.ts b/x-pack/plugins/reporting/common/job_utils.ts new file mode 100644 index 0000000000000..6637b513a8866 --- /dev/null +++ b/x-pack/plugins/reporting/common/job_utils.ts @@ -0,0 +1,13 @@ +/* + * 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 { isObject } from 'lodash'; + +// TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy +// export type entirely +export const isJobV2Params = ({ sharingData }: { sharingData: Record }): boolean => + isObject(sharingData.locator); diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 2148cf983d889..9054a783f0869 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -163,3 +163,10 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; + +// TODO: review once real locator types are available +export interface Locator { + id: string; + version?: string; + params: object; +} 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 caefa21a70d01..40c5aaa880a8d 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 @@ -13,9 +13,10 @@ import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { CoreStart } 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 { LayoutParams, Locator } from '../../common/types'; +import { isJobV2Params } from '../../common/job_utils'; import type { JobParamsPNG } from '../../server/export_types/png/types'; -import type { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; +import type { JobParamsPDF, JobParamsPDFV2 } from '../../server/export_types/printable_pdf/types'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -26,10 +27,6 @@ interface JobParamsProviderOptions { objectType: string; browserTimezone: string; sharingData: Record; - body?: { - version?: string; - value: object; - }; } const jobParamsProvider = ({ @@ -42,11 +39,10 @@ const jobParamsProvider = ({ browserTimezone, layout: sharingData.layout as LayoutParams, title: sharingData.title as string, - body: sharingData.body as { version?: string; value: object }, }; }; -const getPdfJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDF => { +const getPdfV1JobParams = (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( @@ -60,6 +56,17 @@ const getPdfJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDF => }; }; +const getPdfV2JobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDFV2 => { + const locator = opts.sharingData.locator as Locator; + return { + ...jobParamsProvider(opts), + locator, + }; +}; + +const getPdfJobParams = (opts: JobParamsProviderOptions) => + isJobV2Params(opts) ? getPdfV2JobParams(opts) : getPdfV1JobParams(opts); + const getPngJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPNG => { // Replace hashes with original RISON values. const relativeUrl = opts.shareableUrl.replace( @@ -154,6 +161,14 @@ export const reportingScreenshotShareProvider = ({ defaultMessage: 'PNG Reports', }); + const jobProviderOptions: JobParamsProviderOptions = { + shareableUrl, + apiClient, + objectType, + browserTimezone, + sharingData, + }; + const panelPng = { shareMenuItem: { name: pngPanelTitle, @@ -172,13 +187,7 @@ export const reportingScreenshotShareProvider = ({ toasts={toasts} reportType="png" objectId={objectId} - getJobParams={getPngJobParams({ - shareableUrl, - apiClient, - objectType, - browserTimezone, - sharingData, - })} + getJobParams={getPngJobParams(jobProviderOptions)} isDirty={isDirty} onClose={onClose} /> @@ -206,15 +215,9 @@ export const reportingScreenshotShareProvider = ({ diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index dcbdc16fd1e8b..63b3d220479c1 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -13,8 +13,9 @@ import { parse as parseUrl } from 'url'; import { getDisallowedOutgoingUrlError } from '../'; import { ReportingCore } from '../../..'; import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server'; -import { REPORT_BODY_STORE_KEY } from '../../../../common/constants'; +import { REPORT_LOCATOR_STORE_KEY } from '../../../../common/constants'; import { ConditionalHeaders, ConditionalHeadersConditions } from '../../../export_types/common'; +import { Locator } from '../../../../common/types'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ElementPosition } from '../../../lib/screenshots'; @@ -95,12 +96,12 @@ export class HeadlessChromiumDriver { conditionalHeaders, waitForSelector: pageLoadSelector, timeout, - body, + locator, }: { conditionalHeaders: ConditionalHeaders; waitForSelector: string; timeout: number; - body?: object; + locator?: Locator; }, logger: LevelLogger ): Promise { @@ -115,22 +116,24 @@ export class HeadlessChromiumDriver { */ await this.page.evaluateOnNewDocument(this.core.getEnableScreenshotMode()); - /** - * Create the "body" store in the page's context. This value is provided by the client and - * should be considered fully opaque to us. - */ - await this.page.evaluateOnNewDocument( - (storeName: string, value?: object) => { - Object.defineProperty(window, storeName, { - enumerable: true, - writable: false, - configurable: false, - value, - }); - }, - REPORT_BODY_STORE_KEY, - body - ); + if (locator) { + /** + * Create the "locator" store in the page's context. This value is provided by the client and + * should be considered fully opaque to us. + */ + await this.page.evaluateOnNewDocument( + (storeName: string, value?: object) => { + Object.defineProperty(window, storeName, { + enumerable: true, + writable: false, + configurable: false, + value, + }); + }, + REPORT_LOCATOR_STORE_KEY, + locator + ); + } await this.page.setRequestInterception(true); 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 5b4fedab0468f..c0f30f96415f4 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 @@ -17,7 +17,7 @@ export const createJobFnFactory: CreateJobFnFactory< const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJob( - { title, relativeUrls, browserTimezone, layout, objectType, body }, + { title, relativeUrls, browserTimezone, layout, objectType }, context, req ) { @@ -33,7 +33,6 @@ export const createJobFnFactory: CreateJobFnFactory< layout, relativeUrls, title, - body, objectType, }; }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 741cfbc71ef2c..60ad2cbe3e4d3 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -45,7 +45,7 @@ export const runTaskFnFactory: RunTaskFnFactory< mergeMap(({ logo, conditionalHeaders }) => { const urls = getFullUrls(config, job); - const { browserTimezone, layout, title, body } = job; + const { browserTimezone, layout, title } = job; if (apmGetAssets) apmGetAssets.end(); @@ -57,8 +57,7 @@ export const runTaskFnFactory: RunTaskFnFactory< browserTimezone, conditionalHeaders, layout, - logo, - body + logo ); }), map(({ buffer, warnings }) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 6ea1534c8bb4b..9b1a1820b002a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -38,8 +38,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { browserTimezone: string | undefined, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams, - logo?: string, - body?: object + logo?: string ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { const tracker = getTracker(); tracker.startLayout(); @@ -52,7 +51,6 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { const screenshots$ = getScreenshots({ logger, urls, - body, conditionalHeaders, layout, browserTimezone, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index a65c843788740..0eb95a1f01630 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -8,29 +8,11 @@ import { LayoutParams } from '../../lib/layouts'; import { BaseParams, BasePayload } from '../../types'; -interface BaseParamsPDF { +interface BaseParamsPDF { layout: LayoutParams; forceNow?: string; // TODO: Add comment explaining this field relativeUrls: string[]; - - /** - * An optional value that will provided to the renderer (browser). - * - * This provides a way for capturing and forwarding any unsaved state from the consumer requesting a report to the - * server-side report generator. - * - * Please note: this value will be stored and re-used whenever a specific report is generated. Therefore it is - * advisable to assign a version to the body value or to implement some way of migrating values for use over time - * as a report may be requested again in future with a body value created in legacy versions of the reporting consumer. - */ - body?: { - version?: string; - /** - * This value must be serializable for sending over the wire. - */ - value: B; - }; } // Job params: structure of incoming user request data, after being parsed from RISON @@ -38,3 +20,5 @@ export type JobParamsPDF = BaseParamsPDF & BaseParams; // Job payload: structure of stored job data provided by create_job export type TaskPayloadPDF = BaseParamsPDF & BasePayload; + +export * from './v2/types'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/create_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/create_job.ts new file mode 100644 index 0000000000000..54ddacfc410b8 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/create_job.ts @@ -0,0 +1,36 @@ +/* + * 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 { cryptoFactory } from '../../../lib'; +import { CreateJobFn, CreateJobFnFactory } from '../../../types'; +import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types'; + +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn +> = function createJobFactoryFn(reporting, logger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + + return async function createJob( + { title, locator, browserTimezone, layout, objectType }, + context, + req + ) { + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + + return { + headers: serializedEncryptedHeaders, + spaceId: reporting.getSpaceId(req, logger), + browserTimezone, + forceNow: new Date().toISOString(), + layout, + locator, + title, + objectType, + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/execute_job.ts new file mode 100644 index 0000000000000..f2ce70543c2f6 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/execute_job.ts @@ -0,0 +1,80 @@ +/* + * 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 apm from 'elastic-apm-node'; +import * as Rx from 'rxjs'; +import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; +import { PDF_JOB_TYPE_V2 } from '../../../../common/constants'; +import { TaskRunResult } from '../../../lib/tasks'; +import { RunTaskFn, RunTaskFnFactory } from '../../../types'; +import { decryptJobHeaders, getConditionalHeaders, omitBlockedHeaders } from '../../common'; +import { generatePdfObservableFactory } from './lib/generate_pdf'; +import { getCustomLogo } from '../lib/get_custom_logo'; +import { TaskPayloadPDFV2 } from './types'; + +export const runTaskFnFactory: RunTaskFnFactory< + RunTaskFn +> = function executeJobFactoryFn(reporting, parentLogger) { + const config = reporting.getConfig(); + const encryptionKey = config.get('encryptionKey'); + + return async function runTask(jobId, job, cancellationToken) { + const jobLogger = parentLogger.clone([PDF_JOB_TYPE_V2, 'execute-job', jobId]); + const apmTrans = apm.startTransaction('reporting execute_job pdf_v2', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePdf: { end: () => void } | null | undefined; + + const generatePdfObservable = await generatePdfObservableFactory(reporting); + + const process$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), + map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), + map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), + mergeMap((conditionalHeaders) => + getCustomLogo(reporting, conditionalHeaders, job.spaceId, jobLogger) + ), + mergeMap(({ logo, conditionalHeaders }) => { + const { browserTimezone, layout, title, locator } = job; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); + return generatePdfObservable( + jobLogger, + title, + locator, + browserTimezone, + conditionalHeaders, + layout, + logo + ); + }), + map(({ buffer, warnings }) => { + if (apmGeneratePdf) apmGeneratePdf.end(); + + const apmEncode = apmTrans?.startSpan('encode_pdf', 'output'); + const content = buffer?.toString('base64') || null; + if (apmEncode) apmEncode.end(); + + return { + content_type: 'application/pdf', + content, + size: buffer?.byteLength || 0, + warnings, + }; + }), + catchError((err) => { + jobLogger.error(err); + return Rx.throwError(err); + }) + ); + + const stop$ = Rx.fromEventPattern(cancellationToken.on); + + if (apmTrans) apmTrans.end(); + return process$.pipe(takeUntil(stop$)).toPromise(); + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/index.ts new file mode 100644 index 0000000000000..605f0859fb53c --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, + PDF_JOB_TYPE_V2 as jobType, +} from '../../../../common/constants'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../../types'; +import { createJobFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; +import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types'; + +export const getExportType = (): ExportTypeDefinition< + CreateJobFn, + RunTaskFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + createJobFnFactory, + runTaskFnFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/generate_pdf.ts new file mode 100644 index 0000000000000..9b1a1820b002a --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/generate_pdf.ts @@ -0,0 +1,119 @@ +/* + * 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 { groupBy } from 'lodash'; +import * as Rx from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { ReportingCore } from '../../../'; +import { LevelLogger } from '../../../lib'; +import { createLayout, LayoutParams } from '../../../lib/layouts'; +import { ScreenshotResults } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../common'; +import { PdfMaker } from './pdf'; +import { getTracker } from './tracker'; + +const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { + const grouped = groupBy(urlScreenshots.map((u) => u.timeRange)); + const values = Object.values(grouped); + if (values.length === 1) { + return values[0][0]; + } + + return null; +}; + +export async function generatePdfObservableFactory(reporting: ReportingCore) { + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const getScreenshots = await reporting.getScreenshotsObservable(); + + return function generatePdfObservable( + logger: LevelLogger, + title: string, + urls: string[], + browserTimezone: string | undefined, + conditionalHeaders: ConditionalHeaders, + layoutParams: LayoutParams, + logo?: string + ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { + const tracker = getTracker(); + tracker.startLayout(); + + const layout = createLayout(captureConfig, layoutParams); + logger.debug(`Layout: width=${layout.width} height=${layout.height}`); + tracker.endLayout(); + + tracker.startScreenshots(); + const screenshots$ = getScreenshots({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, + }).pipe( + mergeMap(async (results: ScreenshotResults[]) => { + tracker.endScreenshots(); + + tracker.startSetup(); + const pdfOutput = new PdfMaker(layout, logo); + if (title) { + const timeRange = getTimeRange(results); + title += timeRange ? ` - ${timeRange}` : ''; + pdfOutput.setTitle(title); + } + tracker.endSetup(); + + results.forEach((r) => { + r.screenshots.forEach((screenshot) => { + logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.base64EncodedData?.length || 0}`); // prettier-ignore + tracker.startAddImage(); + tracker.endAddImage(); + pdfOutput.addImage(screenshot.base64EncodedData, { + title: screenshot.title, + description: screenshot.description, + }); + }); + }); + + let buffer: Buffer | null = null; + try { + tracker.startCompile(); + logger.debug(`Compiling PDF using "${layout.id}" layout...`); + pdfOutput.generate(); + tracker.endCompile(); + + tracker.startGetBuffer(); + logger.debug(`Generating PDF Buffer...`); + buffer = await pdfOutput.getBuffer(); + + const byteLength = buffer?.byteLength ?? 0; + logger.debug(`PDF buffer byte length: ${byteLength}`); + tracker.setByteLength(byteLength); + + tracker.endGetBuffer(); + } catch (err) { + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); + } + + tracker.end(); + + return { + buffer, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + return found; + }, [] as string[]), + }; + }) + ); + + return screenshots$; + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.test.ts new file mode 100644 index 0000000000000..ebdceda0820b9 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { ReportingCore } from '../../../'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../../test_helpers'; +import { getConditionalHeaders } from '../../common'; +import { getCustomLogo } from './get_custom_logo'; + +let mockReportingPlugin: ReportingCore; + +const logger = createMockLevelLogger(); + +beforeEach(async () => { + mockReportingPlugin = await createMockReportingCore(createMockConfigSchema()); +}); + +test(`gets logo from uiSettings`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', + }; + + const mockGet = jest.fn(); + mockGet.mockImplementationOnce((...args: string[]) => { + if (args[0] === 'xpackReporting:customPdfLogo') { + return 'purple pony'; + } + throw new Error('wrong caller args!'); + }); + mockReportingPlugin.getUiSettingsServiceFactory = jest.fn().mockResolvedValue({ + get: mockGet, + }); + + const conditionalHeaders = getConditionalHeaders( + createMockConfig(createMockConfigSchema()), + permittedHeaders + ); + + const { logo } = await getCustomLogo( + mockReportingPlugin, + conditionalHeaders, + 'spaceyMcSpaceIdFace', + logger + ); + + expect(mockGet).toBeCalledWith('xpackReporting:customPdfLogo'); + expect(logo).toBe('purple pony'); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.ts new file mode 100644 index 0000000000000..d829c3483c466 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/get_custom_logo.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReportingCore } from '../../../'; +import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; +import { ConditionalHeaders } from '../../common'; + +export const getCustomLogo = async ( + reporting: ReportingCore, + conditionalHeaders: ConditionalHeaders, + spaceId: string | undefined, + logger: LevelLogger +) => { + const fakeRequest = reporting.getFakeRequest( + { headers: conditionalHeaders.headers }, + spaceId, + logger + ); + const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); + const logo: string = await uiSettingsClient.get(UI_SETTINGS_CUSTOM_PDF_LOGO); + + // continue the pipeline + return { conditionalHeaders, logo }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_doc_options.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_doc_options.ts new file mode 100644 index 0000000000000..cea068ecf21ae --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_doc_options.ts @@ -0,0 +1,26 @@ +/* + * 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 { BufferOptions } from 'pdfmake/interfaces'; + +export const REPORTING_TABLE_LAYOUT = 'noBorder'; + +export function getDocOptions(tableBorderWidth: number): BufferOptions { + return { + tableLayouts: { + [REPORTING_TABLE_LAYOUT]: { + // format is function (i, node) { ... }; + hLineWidth: () => 0, + vLineWidth: () => 0, + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.test.ts new file mode 100644 index 0000000000000..e2d44b43b674b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { getFont } from './get_font'; + +describe('getFont', () => { + it(`returns 'noto-cjk' when matching cjk characters`, () => { + const cjkStrings = [ + 'vi-Hani: 关', + 'ko: 全', + 'ja: 入', + 'zh-Hant-HK: 免', + 'zh-Hant: 令', + 'zh-Hans: 令', + 'random: おあいい 漢字 あい 抵 令', + String.fromCharCode(0x4ee4), + String.fromCodePoint(0x9aa8), + ]; + + for (const cjkString of cjkStrings) { + expect(getFont(cjkString)).toBe('noto-cjk'); + } + }); + + it(`returns 'Roboto' for non Han characters`, () => { + expect(getFont('English text')).toBe('Roboto'); + expect(getFont('')).toBe('Roboto'); + expect(getFont(undefined!)).toBe('Roboto'); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.ts new file mode 100644 index 0000000000000..4997d37327102 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_font.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export function getFont(text: string) { + // We are matching Han characters which is one of the supported unicode scripts + // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). + // This will match Chinese, Japanese, Korean and some other Asian languages. + const isCKJ = /\p{Script=Han}/gu.test(text); + if (isCKJ) { + return 'noto-cjk'; + } else { + return 'Roboto'; + } +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_template.ts new file mode 100644 index 0000000000000..7813584f26e3c --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/get_template.ts @@ -0,0 +1,137 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import path from 'path'; +import { + ContentText, + DynamicContent, + StyleDictionary, + TDocumentDefinitions, +} from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { REPORTING_TABLE_LAYOUT } from './get_doc_options'; +import { getFont } from './get_font'; + +export function getTemplate( + layout: LayoutInstance, + logo: string | undefined, + title: string, + tableBorderWidth: number, + assetPath: string +): Partial { + const pageMarginTop = 40; + const pageMarginBottom = 80; + const pageMarginWidth = 40; + const headingFontSize = 14; + const headingMarginTop = 10; + const headingMarginBottom = 5; + const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; + const subheadingFontSize = 12; + const subheadingMarginTop = 0; + const subheadingMarginBottom = 5; + const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; + + const getStyle = (): StyleDictionary => ({ + heading: { + alignment: 'left', + fontSize: headingFontSize, + bold: true, + margin: [headingMarginTop, 0, headingMarginBottom, 0], + }, + subheading: { + alignment: 'left', + fontSize: subheadingFontSize, + italics: true, + margin: [0, 0, subheadingMarginBottom, 20], + }, + warning: { + color: '#f39c12', // same as @brand-warning in Kibana colors.less + }, + }); + const getHeader = (): ContentText => ({ + margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], + text: title, + font: getFont(title), + style: { + color: '#aaa', + }, + fontSize: 10, + alignment: 'center', + }); + const getFooter = (): DynamicContent => (currentPage: number, pageCount: number) => { + const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); // Default Elastic Logo + return { + margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], + layout: REPORTING_TABLE_LAYOUT, + table: { + widths: [100, '*', 100], + body: [ + [ + { + fit: [100, 35], + image: logo || logoPath, + }, + { + alignment: 'center', + text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { + defaultMessage: 'Page {currentPage} of {pageCount}', + values: { currentPage: currentPage.toString(), pageCount }, + }), + style: { + color: '#aaa', + }, + }, + '', + ], + [ + logo + ? { + text: i18n.translate('xpack.reporting.exportTypes.printablePdf.logoDescription', { + defaultMessage: 'Powered by Elastic', + }), + fontSize: 10, + style: { + color: '#aaa', + }, + margin: [0, 2, 0, 0], + } + : '', + '', + '', + ], + ], + }, + }; + }; + + return { + // define page size + pageOrientation: layout.getPdfPageOrientation(), + pageSize: layout.getPdfPageSize({ + pageMarginTop, + pageMarginBottom, + pageMarginWidth, + tableBorderWidth, + headingHeight, + subheadingHeight, + }), + pageMargins: layout.useReportingBranding + ? [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom] + : [0, 0, 0, 0], + + header: layout.hasHeader ? getHeader() : undefined, + footer: layout.hasFooter ? getFooter() : undefined, + + styles: layout.useReportingBranding ? getStyle() : undefined, + + defaultStyle: { + fontSize: 12, + font: 'Roboto', + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.test.ts new file mode 100644 index 0000000000000..090ca995a15fc --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { PreserveLayout, PrintLayout } from '../../../../lib/layouts'; +import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers'; +import { PdfMaker } from './'; + +const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`; + +describe('PdfMaker', () => { + it('makes PDF using PrintLayout mode', async () => { + const config = createMockConfig(createMockConfigSchema()); + const layout = new PrintLayout(config.get('capture')); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the best PDF in the world')).toBe(undefined); + expect([ + pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }), + pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }), + ]).toEqual([undefined, undefined]); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + captureConfig: { browser: { chromium: { disableSandbox: true } } }, // NOTE: irrelevant data? + groupCount: 2, + id: 'print', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-item]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the best PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); + + it('makes PDF using PreserveLayout mode', async () => { + const layout = new PreserveLayout({ width: 400, height: 300 }); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the finest PDF in the world')).toBe(undefined); + expect(pdf.addImage(imageBase64, { title: 'cool times', description: '☃️' })).toBe(undefined); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + groupCount: 1, + id: 'preserve_layout', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-items-container]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the finest PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.ts new file mode 100644 index 0000000000000..4056de6cbb111 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/pdf/index.ts @@ -0,0 +1,157 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +// @ts-ignore: no module definition +import concat from 'concat-stream'; +import _ from 'lodash'; +import path from 'path'; +import Printer from 'pdfmake'; +import { Content, ContentImage, ContentText } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options'; +import { getFont } from './get_font'; +import { getTemplate } from './get_template'; + +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); +const tableBorderWidth = 1; + +export class PdfMaker { + private _layout: LayoutInstance; + private _logo: string | undefined; + private _title: string; + private _content: Content[]; + private _printer: Printer; + private _pdfDoc: PDFKit.PDFDocument | undefined; + + constructor(layout: LayoutInstance, logo: string | undefined) { + const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); + const fonts = { + Roboto: { + normal: fontPath('roboto/Roboto-Regular.ttf'), + bold: fontPath('roboto/Roboto-Medium.ttf'), + italics: fontPath('roboto/Roboto-Italic.ttf'), + bolditalics: fontPath('roboto/Roboto-Italic.ttf'), + }, + 'noto-cjk': { + // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. + normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + }, + }; + + this._layout = layout; + this._logo = logo; + this._title = ''; + this._content = []; + this._printer = new Printer(fonts); + } + + _addContents(contents: Content[]) { + const groupCount = this._content.length; + + // inject a page break for every 2 groups on the page + if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { + contents = [ + ({ + text: '', + pageBreak: 'after', + } as ContentText) as Content, + ].concat(contents); + } + this._content.push(contents); + } + + addBrandedImage(img: ContentImage, { title = '', description = '' }) { + const contents: Content[] = []; + + if (title && title.length > 0) { + contents.push({ + text: title, + style: 'heading', + font: getFont(title), + noWrap: true, + }); + } + + if (description && description.length > 0) { + contents.push({ + text: description, + style: 'subheading', + font: getFont(description), + noWrap: true, + }); + } + + const wrappedImg = { + table: { + body: [[img]], + }, + layout: REPORTING_TABLE_LAYOUT, + }; + + contents.push(wrappedImg); + + this._addContents(contents); + } + + addImage(base64EncodedData: string, opts = { title: '', description: '' }) { + const size = this._layout.getPdfImageSize(); + const img = { + image: `data:image/png;base64,${base64EncodedData}`, + alignment: 'center' as 'center', + height: size.height, + width: size.width, + }; + + if (this._layout.useReportingBranding) { + return this.addBrandedImage(img, opts); + } + + this._addContents([img]); + } + + setTitle(title: string) { + this._title = title; + } + + generate() { + const docTemplate = _.assign( + getTemplate(this._layout, this._logo, this._title, tableBorderWidth, assetPath), + { + content: this._content, + } + ); + this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions(tableBorderWidth)); + return this; + } + + getBuffer(): Promise { + return new Promise((resolve, reject) => { + if (!this._pdfDoc) { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', + { + defaultMessage: 'Document stream has not been generated', + } + ) + ); + } + + const concatStream = concat(function (pdfBuffer: Buffer) { + resolve(pdfBuffer); + }); + + this._pdfDoc.on('error', reject); + this._pdfDoc.pipe(concatStream); + this._pdfDoc.end(); + }); + } +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/tracker.ts new file mode 100644 index 0000000000000..4b5a0a7bdade7 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/tracker.ts @@ -0,0 +1,88 @@ +/* + * 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 apm from 'elastic-apm-node'; + +interface PdfTracker { + setByteLength: (byteLength: number) => void; + startLayout: () => void; + endLayout: () => void; + startScreenshots: () => void; + endScreenshots: () => void; + startSetup: () => void; + endSetup: () => void; + startAddImage: () => void; + endAddImage: () => void; + startCompile: () => void; + endCompile: () => void; + startGetBuffer: () => void; + endGetBuffer: () => void; + end: () => void; +} + +const SPANTYPE_SETUP = 'setup'; +const SPANTYPE_OUTPUT = 'output'; + +interface ApmSpan { + end: () => void; +} + +export function getTracker(): PdfTracker { + const apmTrans = apm.startTransaction('reporting generate_pdf', 'reporting'); + + let apmLayout: ApmSpan | null = null; + let apmScreenshots: ApmSpan | null = null; + let apmSetup: ApmSpan | null = null; + let apmAddImage: ApmSpan | null = null; + let apmCompilePdf: ApmSpan | null = null; + let apmGetBuffer: ApmSpan | null = null; + + return { + startLayout() { + apmLayout = apmTrans?.startSpan('create_layout', SPANTYPE_SETUP) || null; + }, + endLayout() { + if (apmLayout) apmLayout.end(); + }, + startScreenshots() { + apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', SPANTYPE_SETUP) || null; + }, + endScreenshots() { + if (apmScreenshots) apmScreenshots.end(); + }, + startSetup() { + apmSetup = apmTrans?.startSpan('setup_pdf', SPANTYPE_SETUP) || null; + }, + endSetup() { + if (apmSetup) apmSetup.end(); + }, + startAddImage() { + apmAddImage = apmTrans?.startSpan('add_pdf_image', SPANTYPE_OUTPUT) || null; + }, + endAddImage() { + if (apmAddImage) apmAddImage.end(); + }, + startCompile() { + apmCompilePdf = apmTrans?.startSpan('compile_pdf', SPANTYPE_OUTPUT) || null; + }, + endCompile() { + if (apmCompilePdf) apmCompilePdf.end(); + }, + startGetBuffer() { + apmGetBuffer = apmTrans?.startSpan('get_buffer', SPANTYPE_OUTPUT) || null; + }, + endGetBuffer() { + if (apmGetBuffer) apmGetBuffer.end(); + }, + setByteLength(byteLength: number) { + apmTrans?.setLabel('byte_length', byteLength, false); + }, + end() { + if (apmTrans) apmTrans.end(); + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/uri_encode.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/uri_encode.js new file mode 100644 index 0000000000000..ddc6061185d89 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/lib/uri_encode.js @@ -0,0 +1,32 @@ +/* + * 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 { forEach, isArray } from 'lodash'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/server'; + +function toKeyValue(obj) { + const parts = []; + forEach(obj, function (value, key) { + if (isArray(value)) { + forEach(value, function (arrayValue) { + const keyStr = url.encodeUriQuery(key, true); + const valStr = arrayValue === true ? '' : '=' + url.encodeUriQuery(arrayValue, true); + parts.push(keyStr + valStr); + }); + } else { + const keyStr = url.encodeUriQuery(key, true); + const valStr = value === true ? '' : '=' + url.encodeUriQuery(value, true); + parts.push(keyStr + valStr); + } + }); + return parts.length ? parts.join('&') : ''; +} + +export const uriEncode = { + stringify: toKeyValue, + string: url.encodeUriQuery, +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/metadata.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/metadata.ts new file mode 100644 index 0000000000000..f4fc93a86821b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/metadata.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export const metadata = { + id: 'printablePdfV2', + name: 'PDF', +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/types.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/types.ts new file mode 100644 index 0000000000000..648137be45c09 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/v2/types.ts @@ -0,0 +1,34 @@ +/* + * 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 { LayoutParams } from '../../../lib/layouts'; +import { BaseParams, BasePayload } from '../../../types'; + +interface BaseParamsPDFV2

{ + layout: LayoutParams; + forceNow?: string; + /** + * The parameters for the rendering job as specified by the consumer that requested the + * report to be created. This value is typically used by the plugin client to re-create + * the same visual state as when the report was requested. + */ + locator: { + /** + * The ID provided by the client so that they can discover this locator state when we start the browser and navigate to + * their app. + */ + id: string; + version?: string; + params: P; + }; +} + +// Job params: structure of incoming user request data, after being parsed from RISON +export type JobParamsPDFV2 = BaseParamsPDFV2 & BaseParams; + +// Job payload: structure of stored job data provided by create_job +export type TaskPayloadPDFV2 = BaseParamsPDFV2 & BasePayload; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index 74bd7e06b43a3..d924b45c1e016 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -17,7 +17,6 @@ export interface ScreenshotObservableOpts { urls: string[]; conditionalHeaders: ConditionalHeaders; layout: LayoutInstance; - body?: object; browserTimezone?: string; } diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 67121621ad585..3692678064415 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -45,7 +45,6 @@ export function screenshotsObservableFactory( urls, conditionalHeaders, layout, - body, browserTimezone, }: ScreenshotObservableOpts): Rx.Observable { const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); @@ -77,8 +76,7 @@ export function screenshotsObservableFactory( url, pageLoadSelector, conditionalHeaders, - logger, - body + logger ); }), mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index d8150cc6e2595..377897bcc381f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -18,15 +18,14 @@ export const openUrl = async ( url: string, pageLoadSelector: string, conditionalHeaders: ConditionalHeaders, - logger: LevelLogger, - body?: object + logger: LevelLogger ): Promise => { const endTrace = startTrace('open_url', 'wait'); try { const timeout = durationToNumber(captureConfig.timeouts.openUrl); await browser.open( url, - { conditionalHeaders, waitForSelector: pageLoadSelector, timeout, body }, + { conditionalHeaders, waitForSelector: pageLoadSelector, timeout }, logger ); } catch (err) {