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. */}
+
+
+ >
+ );
+};
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;
+}