diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 156a306b12e89..e98ba1d451ff3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,6 +83,7 @@ /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services /x-pack/plugins/runtime_fields @elastic/kibana-app-services /x-pack/test/search_sessions_integration/ @elastic/kibana-app-services +/src/plugins/dashboard/public/application/embeddable/viewport/print_media @elastic/kibana-app-services ### Observability Plugins diff --git a/package.json b/package.json index dabdec586151a..a85ef5327f285 100644 --- a/package.json +++ b/package.json @@ -338,6 +338,7 @@ "p-retry": "^4.2.0", "papaparse": "^5.2.0", "pbf": "3.2.1", + "pdfjs-dist": "^2.13.216", "pdfmake": "^0.2.4", "peggy": "^1.2.0", "pluralize": "3.1.0", diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 8aa2d6f1cfe55..ced601d0f3981 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -146,10 +146,10 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png', + 'x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/screenshotting/server/assets/img/logo-grey.png', ]; diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f3aedf36b66a7..c95a2308c3965 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -119,13 +119,12 @@ export function DashboardApp({ <> {isCompleteDashboardAppState(dashboardAppState) && ( <> - {!printMode && ( - - )} + {dashboardAppState.savedDashboard.outcome === 'conflict' && dashboardAppState.savedDashboard.id && 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..cd9c41f392a0b 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,68 @@ -.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 './print_media/styling/index'; - // Some vertical space between vis, but center horizontally - margin: 10px auto; +$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 { + // Open space from page margin + padding-left: $visPadding; + padding-right: $visPadding; + + // Last vis on the page + &:nth-child(#{$visualisationsPerPage}n) { + page-break-after: always; + padding-top: $visPadding; + padding-bottom: $visPadding; + } + + &:last-child { + page-break-after: avoid; + } +} + +@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/print_media/README.md b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/README.md new file mode 100644 index 0000000000000..9bd8bfc3a0944 --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/README.md @@ -0,0 +1,7 @@ +# Print media + +The code here is designed to be movable outside the domain of Dashboard. Currently, +the components and styles are only used by Dashboard but we may choose to move them to, +for example, a Kibana package in the future. + +Any changes to this code must be tested by generating a print-optimized PDF in dashboard. \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_index.scss b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_index.scss new file mode 100644 index 0000000000000..16c4dd85ea45e --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_index.scss @@ -0,0 +1,52 @@ +@import './vars'; + +/* +This styling contains utility and minimal layout styles to help plugins create +print-ready HTML. + +Observations: +1. We currently do not control the user-agent's header and footer content + (including the style of fonts) for client-side printing. + +2. 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 may get split across two + pages. + +3. page-break-* is your friend! +*/ + +// 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; + margin: 0; + margin-top: $a4PageHeaderHeight; + margin-bottom: $a4PageFooterHeight; +} + +@media print { + + html { + background-color: #FFF; + } + + // It is good practice to show the full URL in the final, printed output + a[href]:after { + content: ' [' attr(href) ']'; + } + + figure { + page-break-inside: avoid; + } + + * { + -webkit-print-color-adjust: exact !important; /* Chrome, Safari, Edge */ + color-adjust: exact !important; /*Firefox*/ + } + +} diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_vars.scss b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_vars.scss new file mode 100644 index 0000000000000..d7addc7afb261 --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_vars.scss @@ -0,0 +1,10 @@ + +$a4PageHeight: 297mm; +$a4PageWidth: 210mm; +$a4PageMargin: 0; +$a4PagePadding: 0; +$a4PageHeaderHeight: 15mm; +$a4PageFooterHeight: 20mm; + +$a4PageContentHeight: $a4PageHeight - $a4PageHeaderHeight - $a4PageFooterHeight; +$a4PageContentWidth: $a4PageWidth; 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/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 7095ad34cd189..5cbbd30c79a24 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -82,6 +82,7 @@ export interface DashboardTopNavProps { dashboardAppState: CompleteDashboardAppState; embedSettings?: DashboardEmbedSettings; redirectTo: DashboardRedirect; + printMode: boolean; } const LabsFlyout = withSuspense(LazyLabsFlyout, null); @@ -90,6 +91,7 @@ export function DashboardTopNav({ dashboardAppState, embedSettings, redirectTo, + printMode, }: DashboardTopNavProps) { const { core, @@ -488,7 +490,9 @@ export function DashboardTopNav({ const isFullScreenMode = dashboardState.fullScreenMode; const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu)); - const showQueryInput = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowQueryInput)); + const showQueryInput = shouldShowNavBarComponent( + Boolean(embedSettings?.forceShowQueryInput || printMode) + ); const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker)); const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar)); const showQueryBar = showQueryInput || showDatePicker || showFilterBar; @@ -535,6 +539,7 @@ export function DashboardTopNav({ useDefaultBehaviors: true, savedQuery: state.savedQuery, savedQueryId: dashboardState.savedQuery, + visible: printMode !== true, onQuerySubmit: (_payload, isUpdate) => { if (isUpdate === false) { dashboardAppState.$triggerDashboardRefresh.next({ force: true }); @@ -585,10 +590,10 @@ export function DashboardTopNav({ return ( <> - {isLabsEnabled && isLabsShown ? ( + {!printMode && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} - {dashboardState.viewMode !== ViewMode.VIEW ? ( + {dashboardState.viewMode !== ViewMode.VIEW && !printMode ? ( <> diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index db6cf1bc3d006..5ae2a4498b55b 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -4,6 +4,12 @@ } } +.kbnTopNavMenu__wrapper { + &--hidden { + display: none; + } +} + .kbnTopNavMenu__badgeWrapper { display: flex; align-items: baseline; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 62dc67a3ee941..86c83a6b48be5 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -28,6 +28,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & showFilterBar?: boolean; unifiedSearch?: UnifiedSearchPublicPluginStart; className?: string; + visible?: boolean; /** * If provided, the menu part of the component will be rendered as a portal inside the given mount point. * @@ -105,9 +106,11 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { } function renderLayout() { - const { setMenuMountPoint } = props; + const { setMenuMountPoint, visible } = props; const menuClassName = classNames('kbnTopNavMenu', props.className); - const wrapperClassName = 'kbnTopNavMenu__wrapper'; + const wrapperClassName = classNames('kbnTopNavMenu__wrapper', { + 'kbnTopNavMenu__wrapper--hidden': visible === false, + }); if (setMenuMountPoint) { return ( <> diff --git a/src/plugins/unified_search/kibana.json b/src/plugins/unified_search/kibana.json index b947141a0c68a..07e438ab52174 100755 --- a/src/plugins/unified_search/kibana.json +++ b/src/plugins/unified_search/kibana.json @@ -9,7 +9,7 @@ }, "server": true, "ui": true, - "requiredPlugins": ["dataViews", "data", "uiActions"], + "requiredPlugins": ["dataViews", "data", "uiActions", "screenshotMode"], "requiredBundles": ["kibanaUtils", "kibanaReact", "data"], "serviceFolders": ["autocomplete"], "configPath": ["unifiedSearch"] diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts index 26727b56094a0..08f07e507d96b 100755 --- a/src/plugins/unified_search/public/plugin.ts +++ b/src/plugins/unified_search/public/plugin.ts @@ -55,7 +55,7 @@ export class UnifiedSearchPublicPlugin public start( core: CoreStart, - { data, dataViews, uiActions }: UnifiedSearchStartDependencies + { data, dataViews, uiActions, screenshotMode }: UnifiedSearchStartDependencies ): UnifiedSearchPublicPluginStart { setTheme(core.theme); setOverlays(core.overlays); @@ -68,6 +68,7 @@ export class UnifiedSearchPublicPlugin data, storage: this.storage, usageCollection: this.usageCollection, + isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()), }); uiActions.addTriggerAction( diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index bb01338d8d5a0..0bff12ac78798 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -87,6 +87,7 @@ export interface QueryBarTopRowProps { filterBar?: React.ReactNode; showDatePickerAsBadge?: boolean; showSubmitButton?: boolean; + isScreenshotMode?: boolean; } const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -474,6 +475,8 @@ export const QueryBarTopRow = React.memo( ); } + const isScreenshotMode = props.isScreenshotMode === true; + return ( <> - - {renderDataViewsPicker()} - - {renderQueryInput()} - - {shouldShowDatePickerAsBadge() && props.filterBar} - {renderUpdateButton()} - - {!shouldShowDatePickerAsBadge() && props.filterBar} + {!isScreenshotMode && ( + <> + + {renderDataViewsPicker()} + + {renderQueryInput()} + + {shouldShowDatePickerAsBadge() && props.filterBar} + {renderUpdateButton()} + + {!shouldShowDatePickerAsBadge() && props.filterBar} + + )} ); }, diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index c4e54995b5979..c73aa258863ed 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -26,6 +26,7 @@ interface StatefulSearchBarDeps { data: Omit; storage: IStorageWrapper; usageCollection?: UsageCollectionSetup; + isScreenshotMode?: boolean; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -110,7 +111,13 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) { +export function createSearchBar({ + core, + storage, + data, + usageCollection, + isScreenshotMode = false, +}: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { @@ -197,6 +204,7 @@ export function createSearchBar({ core, storage, data, usageCollection }: Statef {...overrideDefaultBehaviors(props)} dataViewPickerComponentProps={props.dataViewPickerComponentProps} displayStyle={props.displayStyle} + isScreenshotMode={isScreenshotMode} /> ); diff --git a/src/plugins/unified_search/public/search_bar/search_bar.styles.ts b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts index 1072a684eeaad..36d06d1cb9c7f 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.styles.ts +++ b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts @@ -20,5 +20,8 @@ export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => { inPage: css` padding: 0; `, + hidden: css` + display: none; + `, }; }; diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index a684e5ba928a8..8c5abc1bf4c2c 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -87,6 +87,7 @@ export interface SearchBarOwnProps { fillSubmitButton?: boolean; dataViewPickerComponentProps?: DataViewPickerProps; showSubmitButton?: boolean; + isScreenshotMode?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -341,13 +342,16 @@ class SearchBarUI extends Component { public render() { const { theme } = this.props; + const isScreenshotMode = this.props.isScreenshotMode === true; const styles = searchBarStyles(theme); const cssStyles = [ styles.uniSearchBar, this.props.displayStyle && styles[this.props.displayStyle], + isScreenshotMode && styles.hidden, ]; const classes = classNames('uniSearchBar', { + [`uniSearchBar--hidden`]: isScreenshotMode, [`uniSearchBar--${this.props.displayStyle}`]: this.props.displayStyle, }); @@ -470,6 +474,7 @@ class SearchBarUI extends Component { dataViewPickerComponentProps={this.props.dataViewPickerComponentProps} showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()} filterBar={filterBar} + isScreenshotMode={this.props.isScreenshotMode} /> ); diff --git a/src/plugins/unified_search/public/types.ts b/src/plugins/unified_search/public/types.ts index 29cf59f41a871..fa0fc9e826e37 100755 --- a/src/plugins/unified_search/public/types.ts +++ b/src/plugins/unified_search/public/types.ts @@ -8,6 +8,7 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; @@ -29,6 +30,7 @@ export interface UnifiedSearchStartDependencies { fieldFormats: FieldFormatsStart; data: DataPublicPluginStart; uiActions: UiActionsStart; + screenshotMode?: ScreenshotModePluginStart; } /** diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 4c4ca64f7ac07..edbf4df979f7b 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -25,7 +25,8 @@ "mapsEms", "savedObjects", "share", - "presentationUtil" + "presentationUtil", + "screenshotMode" ], "optionalPlugins": [ "cloud", diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 5e9662c543641..b5b232aeeaae6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -41,6 +41,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { LensPublicSetup } from '@kbn/lens-plugin/public'; +import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; import { createRegionMapFn, regionMapRenderer, @@ -88,6 +89,7 @@ export interface MapsPluginSetupDependencies { share: SharePluginSetup; licensing: LicensingPluginSetup; usageCollection?: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; } export interface MapsPluginStartDependencies { @@ -144,7 +146,15 @@ export class MapsPlugin registerLicensedFeatures(plugins.licensing); const config = this._initializerContext.config.get(); - setMapAppConfig(config); + setMapAppConfig({ + ...config, + + // Override this when we know we are taking a screenshot (i.e. no user interaction) + // to avoid a blank-canvas issue when rendering maps on a PDF + 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/formats/pdf/pdf_maker/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/screenshotting/server/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/index.js b/x-pack/plugins/screenshotting/server/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/index.js rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/index.js diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png b/x-pack/plugins/screenshotting/server/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png rename to x-pack/plugins/screenshotting/server/assets/img/logo-grey.png diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index fd09396d6c86d..87968c4471f59 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -17,6 +17,7 @@ import { import { ConfigType } from '../../config'; import { allowRequest } from '../network_policy'; import { stripUnsafeHeaders } from './strip_unsafe_headers'; +import { getFooterTemplate, getHeaderTemplate } from './templates'; export type Context = Record; @@ -155,6 +156,18 @@ export class HeadlessChromiumDriver { return !this.page.isClosed(); } + async printA4Pdf({ title, logo }: { title: string; logo?: string }): Promise { + return this.page.pdf({ + format: 'a4', + preferCSSPageSize: true, + scale: 1, + landscape: false, + displayHeaderFooter: true, + headerTemplate: await getHeaderTemplate({ title }), + footerTemplate: await getFooterTemplate({ logo }), + }); + } + /* * Call Page.screenshot and return a base64-encoded string of the image */ diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts index d74313fa5ace1..1a04574155c1e 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts @@ -67,8 +67,8 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'x64', - archiveFilename: 'chromium-70f5d88-linux_x64.zip', - archiveChecksum: '7b1c9c2fb613444fbdf004a3b75a58df', + archiveFilename: 'chromium-70f5d88-locales-linux_x64.zip', + archiveChecksum: '759bda5e5d32533cb136a85e37c0d102', binaryChecksum: '82e80f9727a88ba3836ce230134bd126', binaryRelativePath: 'headless_shell-linux_x64/headless_shell', location: 'custom', @@ -78,8 +78,8 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'arm64', - archiveFilename: 'chromium-70f5d88-linux_arm64.zip', - archiveChecksum: '4a0217cfe7da86ad1e3d0e9e5895ddb5', + archiveFilename: 'chromium-70f5d88-locales-linux_arm64.zip', + archiveChecksum: '33613b8dc5212c0457210d5a37ea4b43', binaryChecksum: '29e943fbee6d87a217abd6cb6747058e', binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', location: 'custom', diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/templates/footer.handlebars.html b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/footer.handlebars.html new file mode 100644 index 0000000000000..ddd85a50fc6a5 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/footer.handlebars.html @@ -0,0 +1,47 @@ + +
+ + + {{#if hasCustomLogo}} +
{{poweredByElasticCopy}}
+ {{/if}} +
+  of  +
+
diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/templates/header.handlebars.html b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/header.handlebars.html new file mode 100644 index 0000000000000..616e5f753b233 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/header.handlebars.html @@ -0,0 +1,10 @@ + +{{title}} diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/templates/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/index.ts new file mode 100644 index 0000000000000..7034dac76cfca --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/index.ts @@ -0,0 +1,63 @@ +/* + * 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 fs from 'fs/promises'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { assetPath } from '../../../constants'; + +async function compileTemplate(pathToTemplate: string): Promise> { + const contentsBuffer = await fs.readFile(pathToTemplate); + return Handlebars.compile(contentsBuffer.toString()); +} + +interface HeaderTemplateInput { + title: string; +} +interface GetHeaderArgs { + title: string; +} + +export async function getHeaderTemplate({ title }: GetHeaderArgs): Promise { + const template = await compileTemplate( + path.resolve(__dirname, './header.handlebars.html') + ); + return template({ title }); +} + +async function getDefaultFooterLogo(): Promise { + const logoBuffer = await fs.readFile(path.resolve(assetPath, 'img', 'logo-grey.png')); + return `data:image/png;base64,${logoBuffer.toString('base64')}`; +} + +interface FooterTemplateInput { + base64FooterLogo: string; + hasCustomLogo: boolean; + poweredByElasticCopy: string; +} + +interface GetFooterArgs { + logo?: string; +} + +export async function getFooterTemplate({ logo }: GetFooterArgs): Promise { + const template = await compileTemplate( + path.resolve(__dirname, './footer.handlebars.html') + ); + const hasCustomLogo = Boolean(logo); + return template({ + base64FooterLogo: hasCustomLogo ? logo! : await getDefaultFooterLogo(), + hasCustomLogo, + poweredByElasticCopy: i18n.translate( + 'xpack.screenshotting.exportTypes.printablePdf.footer.logoDescription', + { + defaultMessage: 'Powered by Elastic', + } + ), + }); +} diff --git a/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts index 6805996fb1a5a..74a80cf10b58b 100644 --- a/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts @@ -88,11 +88,11 @@ describe('ensureDownloaded', () => { expect.arrayContaining([ 'chrome-mac.zip', 'chrome-win.zip', - 'chromium-70f5d88-linux_x64.zip', + 'chromium-70f5d88-locales-linux_x64.zip', ]) ); expect(readdirSync(path.resolve(`${paths.archivesPath}/arm64`))).toEqual( - expect.arrayContaining(['chrome-mac.zip', 'chromium-70f5d88-linux_arm64.zip']) + expect.arrayContaining(['chrome-mac.zip', 'chromium-70f5d88-locales-linux_arm64.zip']) ); }); diff --git a/x-pack/plugins/screenshotting/server/constants.ts b/x-pack/plugins/screenshotting/server/constants.ts new file mode 100644 index 0000000000000..38fde163778ec --- /dev/null +++ b/x-pack/plugins/screenshotting/server/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 path from 'path'; + +export const assetPath = path.resolve(__dirname, 'assets'); diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts index ce28c53bb5f88..716b2bd46352f 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts @@ -5,6 +5,10 @@ * 2.0. */ +// FIXME: Once/if we have the ability to get page count directly from Chrome/puppeteer +// we should get rid of this lib. +import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js'; + import type { Values } from '@kbn/utility-types'; import { groupBy } from 'lodash'; import type { PackageInfo } from '@kbn/core/server'; @@ -99,30 +103,51 @@ export async function toPdf( { logo, title }: PdfScreenshotOptions, { metrics, results }: CaptureResult ): Promise { - const timeRange = getTimeRange(results); - try { - const { buffer, pages } = await pngsToPdf({ - title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined, - results, - layout, - logo, - packageInfo, - eventLogger, + let buffer: Buffer; + let pages: number; + const shouldConvertPngsToPdf = layout.id !== LayoutTypes.PRINT; + if (shouldConvertPngsToPdf) { + const timeRange = getTimeRange(results); + try { + ({ buffer, pages } = await pngsToPdf({ + title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined, + results, + layout, + logo, + packageInfo, + eventLogger, + })); + + return { + metrics: { + ...(metrics ?? {}), + pages, + }, + data: buffer, + errors: results.flatMap(({ error }) => (error ? [error] : [])), + renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), + }; + } catch (error) { + eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`); + eventLogger.error(error, Transactions.PDF); + throw error; + } + } else { + buffer = results[0].screenshots[0].data; // This buffer is already the PDF + pages = await PDFJS.getDocument({ data: buffer }).promise.then((doc) => { + const numPages = doc.numPages; + doc.destroy(); + return numPages; }); - - return { - metrics: { - ...(metrics ?? {}), - pages, - }, - data: buffer, - errors: results.flatMap(({ error }) => (error ? [error] : [])), - renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), - }; - } catch (error) { - eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`); - eventLogger.error(error, Transactions.PDF); - - throw error; } + + 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/formats/pdf/pdf_maker/constants.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts index 3e44a53a7f3c0..03192aacd887f 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts @@ -5,9 +5,8 @@ * 2.0. */ -import path from 'path'; +import { assetPath } from '../../../constants'; -export const assetPath = path.resolve(__dirname, 'assets'); export const tableBorderWidth = 1; export const pageMarginTop = 40; export const pageMarginBottom = 80; @@ -21,3 +20,4 @@ export const subheadingMarginTop = 0; export const subheadingMarginBottom = 5; export const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; +export { assetPath }; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts index afd9e294e9ae0..7dd964594ca53 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts @@ -83,6 +83,8 @@ export class PdfMaker { const groupCount = this.content.length; // inject a page break for every 2 groups on the page + // TODO: Remove this code since we are now using Chromium to drive this + // layout via native print functionality. if (groupCount > 0 && groupCount % this.layout.groupCount === 0) { contents = [ { diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts index 033fb24c80685..64027ffbd3cf2 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts @@ -12,7 +12,7 @@ import { CaptureResult } from '..'; import { PLUGIN_ID } from '../../../common'; import { ConfigType } from '../../config'; import { ElementPosition } from '../get_element_position_data'; -import { Screenshot } from '../get_screenshots'; +import type { Screenshot } from '../types'; export enum Actions { OPEN_URL = 'open-url', @@ -25,6 +25,7 @@ export enum Actions { WAIT_RENDER = 'wait-for-render', WAIT_VISUALIZATIONS = 'wait-for-visualizations', GET_SCREENSHOT = 'get-screenshots', + PRINT_A4_PDF = 'print-a4-pdf', ADD_IMAGE = 'add-pdf-image', COMPILE = 'compile-pdf', } 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..026d62ada876c --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts @@ -0,0 +1,33 @@ +/* + * 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 { Actions, EventLogger } from './event_logger'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { Screenshot } from './types'; + +export async function getPdf( + browser: HeadlessChromiumDriver, + logger: EventLogger, + title: string, + logo?: string +): Promise { + logger.kbnLogger.info('printing PDF'); + + const spanEnd = logger.logPdfEvent('printing A4 PDF', Actions.PRINT_A4_PDF, 'output'); + + const result = [ + { + data: await browser.printA4Pdf({ title, logo }), + title: null, + description: null, + }, + ]; + + spanEnd(); + + return result; +} diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts index f157649bbb848..67cfbd111e750 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts @@ -8,23 +8,7 @@ import type { HeadlessChromiumDriver } from '../browsers'; import { Actions, EventLogger } from './event_logger'; 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 5048d3f0a3be6..d06014c82ecc7 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -8,7 +8,7 @@ import type { Headers } from '@kbn/core/server'; import { defer, forkJoin, Observable, throwError } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; -import { errors } from '../../common'; +import { errors, LayoutTypes } from '../../common'; import type { Context, HeadlessChromiumDriver } from '../browsers'; import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers'; import { ConfigType, durationToNumber as toNumber } from '../config'; @@ -18,13 +18,15 @@ import type { ElementsPositionAndAttribute } from './get_element_position_data'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getRenderErrors } from './get_render_errors'; -import type { Screenshot } from './get_screenshots'; +import type { Screenshot } from './types'; import { getScreenshots } from './get_screenshots'; +import { getPdf } from './get_pdf'; import { getTimeRange } from './get_time_range'; import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; +import type { PdfScreenshotOptions } from '../formats'; type CaptureTimeouts = ConfigType['capture']['timeouts']; export interface PhaseTimeouts extends CaptureTimeouts { @@ -237,6 +239,26 @@ export class ScreenshotObservableHandler { ); } + /** + * Given a title and time range value look like: + * + * "[Logs] Web Traffic - Apr 14, 2022 @ 120742.318 to Apr 21, 2022 @ 120742.318" + * + * Otherwise closest thing to that or a blank string. + */ + private getTitle(timeRange: null | string): string { + return `${(this.options as PdfScreenshotOptions).title ?? ''} ${ + timeRange ? `- ${timeRange}` : '' + }`.trim(); + } + + private shouldCapturePdf(): boolean { + return ( + this.layout.id === LayoutTypes.PRINT && + (this.options as PdfScreenshotOptions).format === 'pdf' + ); + } + public getScreenshots() { return (withRenderComplete: Observable) => withRenderComplete.pipe( @@ -247,7 +269,14 @@ export class ScreenshotObservableHandler { getDefaultElementPosition(this.layout.getViewport(1)); let screenshots: Screenshot[] = []; try { - screenshots = await getScreenshots(this.driver, this.eventLogger, elements); + screenshots = this.shouldCapturePdf() + ? await getPdf( + this.driver, + this.eventLogger, + this.getTitle(data.timeRange), + (this.options as PdfScreenshotOptions).logo + ) + : await getScreenshots(this.driver, this.eventLogger, 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; +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts index 510e94cf95f0d..9e6919c7a00e6 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts @@ -19,7 +19,7 @@ export const PDF_PRESERVE_PIE_VISUALIZATION_6_3 = `/api/reporting/generate/print )}`; export const PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 = `/api/reporting/generate/printablePdf?jobParams=${encodeURIComponent( - `(browserTimezone:America/New_York,layout:(id:print),objectType:visualization,relativeUrls:!('/app/kibana#/visualize/edit/befdb6b0-3e59-11e8-9fc3-39e49624228e?_g=(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!'Mon+Apr+09+2018+17:56:08+GMT-0400!',mode:absolute,to:!'Wed+Apr+11+2018+17:56:08+GMT-0400!'))&_a=(filters:!!((!'$state!':(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!'!'),uiState:(),vis:(aggs:!!((enabled:!!t,id:!'1!',params:(),schema:metric,type:count),(enabled:!!t,id:!'2!',params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!'1!',otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!'Filter+Test:+animals:+linked+to+search+with+filter!',type:pie))'),title:'Filter Test: animals: linked to search with filter')` + `(browserTimezone:America/New_York,layout:(dimensions:(height:588,width:1038),id:preserve_layout),objectType:visualization,relativeUrls:!('/app/kibana#/visualize/edit/befdb6b0-3e59-11e8-9fc3-39e49624228e?_g=(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!'Mon+Apr+09+2018+17:56:08+GMT-0400!',mode:absolute,to:!'Wed+Apr+11+2018+17:56:08+GMT-0400!'))&_a=(filters:!!((!'$state!':(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!'!'),uiState:(),vis:(aggs:!!((enabled:!!t,id:!'1!',params:(),schema:metric,type:count),(enabled:!!t,id:!'2!',params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!'1!',otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!'Filter+Test:+animals:+linked+to+search+with+filter!',type:pie))'),title:'Filter Test: animals: linked to search with filter')` )}`; export const JOB_PARAMS_CSV_DEFAULT_SPACE = `/api/reporting/generate/csv_searchsource?jobParams=${encodeURIComponent( diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts index 086f3373e2c71..e702be05f9bd8 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts @@ -74,15 +74,15 @@ export default function ({ getService }: FtrProviderContext) { const usage = await usageAPI.getUsageStats(); reportingAPI.expectRecentPdfAppStats(usage, 'visualization', 1); reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1); - reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 0); - reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 2); + reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 1); + reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 1); reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); reportingAPI.expectAllTimePdfAppStats(usage, 'visualization', 1); reportingAPI.expectAllTimePdfAppStats(usage, 'dashboard', 1); - reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 0); - reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 2); + reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 1); + reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 1); reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'printable_pdf', 2); }); diff --git a/x-pack/test/reporting_api_integration/services/usage.ts b/x-pack/test/reporting_api_integration/services/usage.ts index fd16f3859fa11..80204875cd6d6 100644 --- a/x-pack/test/reporting_api_integration/services/usage.ts +++ b/x-pack/test/reporting_api_integration/services/usage.ts @@ -101,31 +101,47 @@ export function createUsageServices({ getService }: FtrProviderContext) { }, expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) { - expect( - stats.reporting.last_7_days.printable_pdf.app![app as keyof AvailableTotal['app']] - ).to.be(count); + const actual = + stats.reporting.last_7_days.printable_pdf.app![app as keyof AvailableTotal['app']]; + log.info(`expecting recent ${app} stats to have ${count} printable pdfs (actual: ${actual})`); + expect(actual).to.be(count); }, expectAllTimePdfAppStats(stats: UsageStats, app: string, count: number) { - expect(stats.reporting.printable_pdf.app![app as keyof AvailableTotal['app']]).to.be(count); + const actual = stats.reporting.printable_pdf.app![app as keyof AvailableTotal['app']]; + log.info( + `expecting all time pdf ${app} stats to have ${count} printable pdfs (actual: ${actual})` + ); + expect(actual).to.be(count); }, expectRecentPdfLayoutStats(stats: UsageStats, layout: string, count: number) { - expect(stats.reporting.last_7_days.printable_pdf.layout![layout as keyof LayoutCounts]).to.be( - count - ); + const actual = + stats.reporting.last_7_days.printable_pdf.layout![layout as keyof LayoutCounts]; + log.info(`expecting recent stats to report ${count} ${layout} layouts (actual: ${actual})`); + expect(actual).to.be(count); }, expectAllTimePdfLayoutStats(stats: UsageStats, layout: string, count: number) { - expect(stats.reporting.printable_pdf.layout![layout as keyof LayoutCounts]).to.be(count); + const actual = stats.reporting.printable_pdf.layout![layout as keyof LayoutCounts]; + log.info(`expecting all time stats to report ${count} ${layout} layouts (actual: ${actual})`); + expect(actual).to.be(count); }, expectRecentJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) { - expect(stats.reporting.last_7_days[jobType as keyof JobTypes].total).to.be(count); + const actual = stats.reporting.last_7_days[jobType as keyof JobTypes].total; + log.info( + `expecting recent stats to report ${count} ${jobType} job types (actual: ${actual})` + ); + expect(actual).to.be(count); }, expectAllTimeJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) { - expect(stats.reporting[jobType as keyof JobTypes].total).to.be(count); + const actual = stats.reporting[jobType as keyof JobTypes].total; + log.info( + `expecting all time stats to report ${count} ${jobType} job types (actual: ${actual})` + ); + expect(actual).to.be(count); }, getCompletedReportCount(stats: UsageStats) { diff --git a/yarn.lock b/yarn.lock index f770f30d3687d..7c1a6e7fa9680 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22194,6 +22194,13 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pdfjs-dist@^2.13.216: + version "2.13.216" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.13.216.tgz#251a11c9c8c6db19baacd833a4e6986c517d1ab3" + integrity sha512-qn/9a/3IHIKZarTK6ajeeFXBkG15Lg1Fx99PxU09PAU2i874X8mTcHJYyDJxu7WDfNhV6hM7bRQBZU384anoqQ== + dependencies: + web-streams-polyfill "^3.2.0" + pdfmake@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.2.4.tgz#7d58d64b59f8e9b9ed0b2494b17a9d94c575825b" @@ -29749,6 +29756,11 @@ web-streams-polyfill@^3.0.0: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.0.1.tgz#1f836eea307e8f4af15758ee473c7af755eb879e" integrity sha512-M+EmTdszMWINywOZaqpZ6VIEDUmNpRaTOuizF0ZKPjSDC8paMRe/jBBwFv0Yeyn5WYnM5pMqMQa82vpaE+IJRw== +web-streams-polyfill@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965" + integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"