diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss index a451178cc46b0..91a6b166700bc 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss @@ -1,9 +1,75 @@ -.printViewport { - &__vis { - height: 600px; // These values might need to be passed in as dimensions for the report. I.e., print should use layout dimensions. - width: 975px; +@import './shared_poc_print'; // Bring in the shared print styling - // Some vertical space between vis, but center horizontally - margin: 10px auto; +/* +Dashboard styling + +It is up to dashboard to decide how the visualizations should look on an A4 page. The assumption +is that we want to preserve current behaviour of 2 visualizations per page. +*/ +$visualisationsPerPage: 2; +$visPadding: 4mm; + +/* +We set the same visual padding on the browser and print versions of the UI so that +we don't hit a race condition where padding is being updated while the print image +is being formed. This can result in parts of the vis being cut out. +*/ +@mixin visualizationPadding { + padding-left: $visPadding; + padding-right: $visPadding; + + &:nth-child(#{$visualisationsPerPage}n - #{$visualisationsPerPage - 1}) { + padding-top: $visPadding; + } + + &:nth-child(#{$visualisationsPerPage}n) { + page-break-after: always; + padding-top: $visPadding; + padding-bottom: $visPadding; + } +} + +@include globalSharedRules(); + +@media screen, projection { + .printViewport { + &__vis { + @include visualizationPadding(); + + & .embPanel__header button { + display: none; + } + + margin: $euiSizeL auto; + height: calc(#{$a4PageContentHeight} / #{$visualisationsPerPage}); + width: $a4PageContentWidth; + padding: $visPadding; + } + } +} + +@media print { + + .printViewport { + &__vis { + @include visualizationPadding(); + + height: calc(#{$a4PageContentHeight} / #{$visualisationsPerPage}); + width: $a4PageContentWidth; + + & .euiPanel { + box-shadow: none !important; + } + + & .embPanel__header button { + display: none; + } + + page-break-inside: avoid; + + & * { + overflow: hidden !important; + } + } } } diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_shared_poc_print.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_shared_poc_print.scss new file mode 100644 index 0000000000000..d4fae6dbc00a6 --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_shared_poc_print.scss @@ -0,0 +1,107 @@ + +/* +Global print styling + +This styling should be usable by an plugin/app that wants to provide print +functionality. The styling should be extremely unopinionated. + +Observations: + +1. It is possible to request a print before all ES searches have populated + visualizations, this results in a borked print preview with half-rendered + visualisations. The user will have to close the print dialog and re-open + it. Not sure how to "fix" this UX. + +2. We currently do not control the user-agent's header and footer content + (including the style of fonts) + +3. I could not see a way to get page numbering other than using the + browser-provided footers :'( + +4. Page box model is quite different from what we have in browsers - page + margins define where the "no-mans-land" exists for actual content. Moving + content into this space by, for example setting negative margins resulted + in slightly unpredictable behaviour because the browser wants to either + move this content to another page or it just looks broken/split across two + pages + +5. page-break-* is your friend! +*/ +$a4PageHeight: 297mm; +$a4PageWidth: 210mm; +$a4PageMargin: 0; +$a4PagePadding: 0; +$a4PageHeaderHeight: 5mm; +$a4PageFooterHeight: 10mm; +$a4PageMarginBottom: 9mm; + +$a4PageContentHeight: $a4PageHeight - $a4PageFooterHeight - $a4PageHeaderHeight - $a4PageMarginBottom; +$a4PageContentWidth: $a4PageWidth; + +// Currently we cannot control or style the content the browser places in +// margins, this might change in the future: +// See https://drafts.csswg.org/css-page-3/#margin-boxes +@page { + size: A4; + orientation: portrait; + padding: 0; + margin: 0; + margin-bottom: $a4PageMarginBottom; +} + +@media screen, projection { + .printFooter, .printHeader { + display: none; + } +} + +@media print { + + html { + background-color: #FFF; + } + + // When printing it is good practice to show the full url + a[href]:after { + content: ' [' attr(href) ']'; + } + + figure { + break-inside: avoid; + } + + * { + -webkit-print-color-adjust: exact !important; /* Chrome, Safari, Edge */ + color-adjust: exact !important; /*Firefox*/ + } + + .printHeader { + display: table-header-group; + position: fixed; + top: 0; + left: 50%; + padding-top: 2mm; + transform: translateX(-50%); + } + + .printFooter { + display: table-footer-group; + position: fixed; + bottom: 0; + margin: 0 2mm 2mm 5mm; + > img { + height: 8mm; + } + } + + // There is a known limitation that Chrome does not increment the counter of fixed elements even though + // they appear for each page. So we leave this out for now. + // See https://bugs.chromium.org/p/chromium/issues/detail?id=774830 + // .printFooter::after { + // position: fixed; + // bottom: 2mm; + // left: 50%; + // transform: translateX(-50%); + // content: 'Page ' counter(x); + // } +} diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index 0e700e058eef4..9c298fbf9be19 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -20,6 +20,8 @@ import { } from '../../../../../controls/public'; import { withSuspense } from '../../../services/presentation_util'; +import { SharedPocPrintUi } from './shared_poc_print_ui'; + export interface DashboardViewportProps { container: DashboardContainer; controlGroup?: ControlGroupContainer; @@ -147,6 +149,7 @@ export class DashboardViewport extends React.Component} + ); } diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/shared_poc_print_ui.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/shared_poc_print_ui.tsx new file mode 100644 index 0000000000000..2f3b77e32c43f --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/shared_poc_print_ui.tsx @@ -0,0 +1,38 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import { imgData } from './shared_poc_print_ui_logo'; + +interface Props { + title: string; + logo?: string; +} + +export const SharedPocPrintUi: FunctionComponent = ({ title, logo = imgData }) => { + return ( + <> + {/* NOTE: This UI is purely for test purposes, but it is easy to see how we could move this to some external place that shares this functionality. */} +
{title}
+
+ a cool logo for branding + {/* + Currently pageNumber is not being used, but the idea was to try and use JS to count the pages and populate + the repeated element, could not get this working + */} +
+
+ + ); +}; diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/shared_poc_print_ui_logo.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/shared_poc_print_ui_logo.tsx new file mode 100644 index 0000000000000..9356ccfa5692f --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/shared_poc_print_ui_logo.tsx @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const imgData = `data:image/png;base64,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`; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index a103c88843664..50c40e4863bee 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -183,8 +183,7 @@ export const useDashboardAppState = ({ savedDashboard, }); - // Backwards compatible way of detecting that we are taking a screenshot - const legacyPrintLayoutDetected = + const printLayoutDetected = screenshotModeService?.isScreenshotMode() && screenshotModeService.getScreenshotContext('layout') === 'print'; @@ -194,8 +193,7 @@ export const useDashboardAppState = ({ ...initialDashboardStateFromUrl, ...forwardedAppState, - // if we are in legacy print mode, dashboard needs to be in print viewMode - ...(legacyPrintLayoutDetected ? { viewMode: ViewMode.PRINT } : {}), + ...(printLayoutDetected ? { viewMode: ViewMode.PRINT } : {}), // if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it. ...(incomingEmbeddable ? { viewMode: ViewMode.EDIT } : {}), diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index a19f160127eb3..a17338d55de56 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -26,7 +26,8 @@ "savedObjects", "share", "presentationUtil", - "sharedUX" + "sharedUX", + "screenshotMode" ], "optionalPlugins": [ "cloud", diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 2ab91462cfe7e..0145971f96500 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -80,6 +80,7 @@ import type { LensPublicSetup } from '../../lens/public'; import { setupLensChoroplethChart } from './lens'; import { SharedUXPluginStart } from '../../../../src/plugins/shared_ux/public'; +import { ScreenshotModePluginSetup } from '../../../../src/plugins/screenshot_mode/public'; export interface MapsPluginSetupDependencies { cloud?: CloudSetup; @@ -92,6 +93,7 @@ export interface MapsPluginSetupDependencies { share: SharePluginSetup; licensing: LicensingPluginSetup; usageCollection?: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; } export interface MapsPluginStartDependencies { @@ -149,7 +151,12 @@ export class MapsPlugin registerLicensedFeatures(plugins.licensing); const config = this._initializerContext.config.get(); - setMapAppConfig(config); + setMapAppConfig({ + ...config, + preserveDrawingBuffer: plugins.screenshotMode.isScreenshotMode() + ? true + : config.preserveDrawingBuffer, + }); const locator = plugins.share.url.locators.create( new MapsAppLocatorDefinition({ diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 5d5f4223fab9a..57cc09dec4b16 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -33,6 +33,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/shared_ux/tsconfig.json" }, + { "path": "../../../src/plugins/screenshot_mode/tsconfig.json" }, { "path": "../cloud/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index f9bf31ad35f6f..390917db227be 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -155,6 +155,16 @@ export class HeadlessChromiumDriver { return !this.page.isClosed(); } + async printA4Pdf(): Promise { + return this.page.pdf({ + format: 'a4', + preferCSSPageSize: true, + scale: 1, + landscape: false, + displayHeaderFooter: true, + }); + } + /* * Call Page.screenshot and return a base64-encoded string of the image */ diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts index b7718155c5424..00112e13bbe66 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts @@ -99,28 +99,46 @@ export async function toPdf( { metrics, results }: CaptureResult ): Promise { const timeRange = getTimeRange(results); - try { - const { buffer, pages } = await pngsToPdf({ - title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined, - results, - layout, - logo, - packageInfo, - logger, - }); - - return { - metrics: { - ...(metrics ?? {}), - pages, - }, - data: buffer, - errors: results.flatMap(({ error }) => (error ? [error] : [])), - renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), - }; - } catch (error) { - logger.error(`Could not generate the PDF buffer!`); - - throw error; + let buffer: Buffer; + let pages: number; + const shouldConvertPngsToPdf = layout.id !== LayoutTypes.PRINT; + if (shouldConvertPngsToPdf) { + try { + ({ buffer, pages } = await pngsToPdf({ + title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined, + results, + layout, + logo, + packageInfo, + logger, + })); + + return { + metrics: { + ...(metrics ?? {}), + pages, + }, + data: buffer, + errors: results.flatMap(({ error }) => (error ? [error] : [])), + renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), + }; + } catch (error) { + logger.error(`Could not generate the PDF buffer!`); + + throw error; + } + } else { + buffer = results[0].screenshots[0].data; // This buffer is already the PDF + pages = -1; // TODO: Figure out how to get page numbers } + + return { + metrics: { + ...(metrics ?? {}), + pages, + }, + data: buffer, + errors: results.flatMap(({ error }) => (error ? [error] : [])), + renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), + }; } diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts b/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts new file mode 100644 index 0000000000000..9b9629c3ab9af --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts @@ -0,0 +1,25 @@ +/* + * 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 type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { Screenshot } from './types'; + +export async function getPdf( + browser: HeadlessChromiumDriver, + logger: Logger +): Promise { + logger.info('printing PDF'); + + return [ + { + data: await browser.printA4Pdf(), + title: null, + description: null, + }, + ]; +} diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts index 26ef272e7f18e..6899b9328736d 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts @@ -9,23 +9,7 @@ import apm from 'elastic-apm-node'; import type { Logger } from 'src/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; - -export interface Screenshot { - /** - * Screenshot PNG image data. - */ - data: Buffer; - - /** - * Screenshot title. - */ - title: string | null; - - /** - * Screenshot description. - */ - description: string | null; -} +import type { Screenshot } from './types'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index fbc147102e0af..ec08c61456c0d 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -9,7 +9,7 @@ import type { Transaction } from 'elastic-apm-node'; import { defer, forkJoin, throwError, Observable } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; import type { Headers, Logger } from 'src/core/server'; -import { errors } from '../../common'; +import { errors, LayoutTypes } from '../../common'; import type { Context, HeadlessChromiumDriver } from '../browsers'; import { getChromiumDisconnectedError, DEFAULT_VIEWPORT } from '../browsers'; import type { Layout } from '../layouts'; @@ -18,7 +18,8 @@ import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getRenderErrors } from './get_render_errors'; import { getScreenshots } from './get_screenshots'; -import type { Screenshot } from './get_screenshots'; +import { getPdf } from './get_pdf'; +import type { Screenshot } from './types'; import { getTimeRange } from './get_time_range'; import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; @@ -247,7 +248,10 @@ export class ScreenshotObservableHandler { getDefaultElementPosition(this.layout.getViewport(1)); let screenshots: Screenshot[] = []; try { - screenshots = await getScreenshots(this.driver, this.logger, elements); + screenshots = + this.layout.id === LayoutTypes.PRINT + ? await getPdf(this.driver, this.logger) + : await getScreenshots(this.driver, this.logger, elements); } catch (e) { throw new errors.FailedToCaptureScreenshot(e.message); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/types.ts b/x-pack/plugins/screenshotting/server/screenshots/types.ts new file mode 100644 index 0000000000000..d4a408313fc43 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/types.ts @@ -0,0 +1,23 @@ +/* + * 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 interface Screenshot { + /** + * Screenshot PNG image data. + */ + data: Buffer; + + /** + * Screenshot title. + */ + title: string | null; + + /** + * Screenshot description. + */ + description: string | null; +}