diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 067b6b206fa14..4ba5e32eec8b5 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -181,6 +181,10 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s oss plugins. +|{kib-repo}blob/{branch}/src/plugins/screenshot_mode/README.md[screenshotMode] +|The service exposed by this plugin informs consumers whether they should optimize for non-interactivity. In this way plugins can avoid loading unnecessary code, data or other services. + + |{kib-repo}blob/{branch}/src/plugins/security_oss/README.md[securityOss] |securityOss is responsible for educating users about Elastic's free security features, so they can properly protect the data within their clusters. diff --git a/examples/screenshot_mode_example/.i18nrc.json b/examples/screenshot_mode_example/.i18nrc.json new file mode 100644 index 0000000000000..cce0f6b34fea2 --- /dev/null +++ b/examples/screenshot_mode_example/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "screenshotModeExample", + "paths": { + "screenshotModeExample": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/examples/screenshot_mode_example/README.md b/examples/screenshot_mode_example/README.md new file mode 100755 index 0000000000000..ebae7480ca5fe --- /dev/null +++ b/examples/screenshot_mode_example/README.md @@ -0,0 +1,9 @@ +# screenshotModeExample + +A Kibana plugin + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/examples/screenshot_mode_example/common/index.ts b/examples/screenshot_mode_example/common/index.ts new file mode 100644 index 0000000000000..c6b22bdb07785 --- /dev/null +++ b/examples/screenshot_mode_example/common/index.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 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 PLUGIN_NAME = 'Screenshot mode example app'; + +export const BASE_API_ROUTE = '/api/screenshot_mode_example'; diff --git a/examples/screenshot_mode_example/kibana.json b/examples/screenshot_mode_example/kibana.json new file mode 100644 index 0000000000000..4cb8c1a1393fb --- /dev/null +++ b/examples/screenshot_mode_example/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "screenshotModeExample", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["navigation", "screenshotMode", "usageCollection"], + "optionalPlugins": [] +} diff --git a/examples/screenshot_mode_example/public/application.tsx b/examples/screenshot_mode_example/public/application.tsx new file mode 100644 index 0000000000000..670468c77bd5f --- /dev/null +++ b/examples/screenshot_mode_example/public/application.tsx @@ -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 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 React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '../../../src/core/public'; +import { AppPluginSetupDependencies, AppPluginStartDependencies } from './types'; +import { ScreenshotModeExampleApp } from './components/app'; + +export const renderApp = ( + { notifications, http }: CoreStart, + { screenshotMode }: AppPluginSetupDependencies, + { navigation }: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters +) => { + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/screenshot_mode_example/public/components/app.tsx b/examples/screenshot_mode_example/public/components/app.tsx new file mode 100644 index 0000000000000..c50eaf5b52568 --- /dev/null +++ b/examples/screenshot_mode_example/public/components/app.tsx @@ -0,0 +1,122 @@ +/* + * 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 React, { useEffect } from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { CoreStart } from '../../../../src/core/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import { + ScreenshotModePluginSetup, + KBN_SCREENSHOT_MODE_HEADER, +} from '../../../../src/plugins/screenshot_mode/public'; + +import { PLUGIN_NAME, BASE_API_ROUTE } from '../../common'; + +interface ScreenshotModeExampleAppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; + screenshotMode: ScreenshotModePluginSetup; +} + +export const ScreenshotModeExampleApp = ({ + basename, + notifications, + http, + navigation, + screenshotMode, +}: ScreenshotModeExampleAppDeps) => { + const isScreenshotMode = screenshotMode.isScreenshotMode(); + + useEffect(() => { + // fire and forget + http.get(`${BASE_API_ROUTE}/check_is_screenshot`, { + headers: isScreenshotMode ? { [KBN_SCREENSHOT_MODE_HEADER]: 'true' } : undefined, + }); + notifications.toasts.addInfo({ + title: 'Welcome to the screenshot example app!', + text: isScreenshotMode + ? 'In screenshot mode we want this to remain visible' + : 'In normal mode this toast will disappear eventually', + toastLifeTimeMs: isScreenshotMode ? 360000 : 3000, + }); + }, [isScreenshotMode, notifications, http]); + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ {isScreenshotMode ? ( + + ) : ( + + )} +

+
+
+ + + {isScreenshotMode ? ( +

We detected screenshot mode. The chrome navbar should be hidden.

+ ) : ( +

+ This is how the app looks in normal mode. The chrome navbar should be + visible. +

+ )} +
+
+
+
+
+ +
+
+ ); +}; diff --git a/examples/screenshot_mode_example/public/index.scss b/examples/screenshot_mode_example/public/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/examples/screenshot_mode_example/public/index.ts b/examples/screenshot_mode_example/public/index.ts new file mode 100644 index 0000000000000..07768cbb1fdb7 --- /dev/null +++ b/examples/screenshot_mode_example/public/index.ts @@ -0,0 +1,17 @@ +/* + * 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 './index.scss'; + +import { ScreenshotModeExamplePlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new ScreenshotModeExamplePlugin(); +} diff --git a/examples/screenshot_mode_example/public/plugin.ts b/examples/screenshot_mode_example/public/plugin.ts new file mode 100644 index 0000000000000..91bcc2410b5fc --- /dev/null +++ b/examples/screenshot_mode_example/public/plugin.ts @@ -0,0 +1,48 @@ +/* + * 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 { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { AppPluginSetupDependencies, AppPluginStartDependencies } from './types'; +import { MetricsTracking } from './services'; +import { PLUGIN_NAME } from '../common'; + +export class ScreenshotModeExamplePlugin implements Plugin { + uiTracking = new MetricsTracking(); + + public setup(core: CoreSetup, depsSetup: AppPluginSetupDependencies): void { + const { screenshotMode, usageCollection } = depsSetup; + const isScreenshotMode = screenshotMode.isScreenshotMode(); + + this.uiTracking.setup({ + disableTracking: isScreenshotMode, // In screenshot mode there will be no user interactions to track + usageCollection, + }); + + // Register an application into the side navigation menu + core.application.register({ + id: 'screenshotModeExample', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + + // For screenshots we don't need to have the top bar visible + coreStart.chrome.setIsVisible(!isScreenshotMode); + + // Render the application + return renderApp(coreStart, depsSetup, depsStart as AppPluginStartDependencies, params); + }, + }); + } + + public start(core: CoreStart): void {} + + public stop() {} +} diff --git a/examples/screenshot_mode_example/public/services/index.ts b/examples/screenshot_mode_example/public/services/index.ts new file mode 100644 index 0000000000000..5725e52e65097 --- /dev/null +++ b/examples/screenshot_mode_example/public/services/index.ts @@ -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 { MetricsTracking } from './metrics_tracking'; diff --git a/examples/screenshot_mode_example/public/services/metrics_tracking.ts b/examples/screenshot_mode_example/public/services/metrics_tracking.ts new file mode 100644 index 0000000000000..e40b6bbf09e44 --- /dev/null +++ b/examples/screenshot_mode_example/public/services/metrics_tracking.ts @@ -0,0 +1,37 @@ +/* + * 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 { UiCounterMetricType, METRIC_TYPE } from '@kbn/analytics'; +import { PLUGIN_NAME } from '../../common'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; + +export class MetricsTracking { + private trackingDisabled = false; + private usageCollection?: UsageCollectionSetup; + + private track(eventName: string, type: UiCounterMetricType) { + if (this.trackingDisabled) return; + + this.usageCollection?.reportUiCounter(PLUGIN_NAME, type, eventName); + } + + public setup({ + disableTracking, + usageCollection, + }: { + disableTracking?: boolean; + usageCollection: UsageCollectionSetup; + }) { + this.usageCollection = usageCollection; + if (disableTracking) this.trackingDisabled = true; + } + + public trackInit() { + this.track('init', METRIC_TYPE.LOADED); + } +} diff --git a/examples/screenshot_mode_example/public/types.ts b/examples/screenshot_mode_example/public/types.ts new file mode 100644 index 0000000000000..88812a4a507c9 --- /dev/null +++ b/examples/screenshot_mode_example/public/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; +import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/public'; +import { UsageCollectionSetup } from '../../../src/plugins/usage_collection/public'; + +export interface AppPluginSetupDependencies { + usageCollection: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; +} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; +} diff --git a/examples/screenshot_mode_example/server/index.ts b/examples/screenshot_mode_example/server/index.ts new file mode 100644 index 0000000000000..af23ea893a755 --- /dev/null +++ b/examples/screenshot_mode_example/server/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { ScreenshotModeExamplePlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new ScreenshotModeExamplePlugin(ctx); diff --git a/examples/screenshot_mode_example/server/plugin.ts b/examples/screenshot_mode_example/server/plugin.ts new file mode 100644 index 0000000000000..5738f4a583a1a --- /dev/null +++ b/examples/screenshot_mode_example/server/plugin.ts @@ -0,0 +1,31 @@ +/* + * 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 { Plugin, PluginInitializerContext, CoreSetup, Logger } from 'kibana/server'; +import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/server'; +import { RouteDependencies } from './types'; +import { registerRoutes } from './routes'; + +export class ScreenshotModeExamplePlugin implements Plugin { + log: Logger; + constructor(ctx: PluginInitializerContext) { + this.log = ctx.logger.get(); + } + setup(core: CoreSetup, { screenshotMode }: { screenshotMode: ScreenshotModePluginSetup }): void { + const deps: RouteDependencies = { + screenshotMode, + router: core.http.createRouter(), + log: this.log, + }; + + registerRoutes(deps); + } + + start() {} + stop() {} +} diff --git a/examples/screenshot_mode_example/server/routes.ts b/examples/screenshot_mode_example/server/routes.ts new file mode 100644 index 0000000000000..adf4c2e2b6fc5 --- /dev/null +++ b/examples/screenshot_mode_example/server/routes.ts @@ -0,0 +1,21 @@ +/* + * 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 { RouteDependencies } from './types'; +import { BASE_API_ROUTE } from '../common'; + +export const registerRoutes = ({ router, log, screenshotMode }: RouteDependencies) => { + router.get( + { path: `${BASE_API_ROUTE}/check_is_screenshot`, validate: false }, + async (ctx, req, res) => { + log.info(`Reading screenshot mode from a request: ${screenshotMode.isScreenshotMode(req)}`); + log.info(`Reading is screenshot mode from ctx: ${ctx.screenshotMode.isScreenshot}`); + return res.ok(); + } + ); +}; diff --git a/examples/screenshot_mode_example/server/types.ts b/examples/screenshot_mode_example/server/types.ts new file mode 100644 index 0000000000000..9d8d5888c3ab1 --- /dev/null +++ b/examples/screenshot_mode_example/server/types.ts @@ -0,0 +1,19 @@ +/* + * 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 { IRouter, Logger } from 'kibana/server'; +import { ScreenshotModeRequestHandlerContext } from '../../../src/plugins/screenshot_mode/server'; +import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/server'; + +export type ScreenshotModeExampleRouter = IRouter; + +export interface RouteDependencies { + screenshotMode: ScreenshotModePluginSetup; + router: ScreenshotModeExampleRouter; + log: Logger; +} diff --git a/examples/screenshot_mode_example/tsconfig.json b/examples/screenshot_mode_example/tsconfig.json new file mode 100644 index 0000000000000..dfb436e7377ac --- /dev/null +++ b/examples/screenshot_mode_example/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "common/**/*.ts", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [], + "references": [{ "path": "../../src/core/tsconfig.json" }] +} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 348ecb7eea7f2..2639f6fd273f7 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -110,3 +110,4 @@ pageLoadAssetSize: mapsEms: 26072 timelines: 28613 cases: 162385 + screenshotMode: 17856 diff --git a/src/plugins/screenshot_mode/.i18nrc.json b/src/plugins/screenshot_mode/.i18nrc.json new file mode 100644 index 0000000000000..79643fbb63d30 --- /dev/null +++ b/src/plugins/screenshot_mode/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "screenshotMode", + "paths": { + "screenshotMode": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/src/plugins/screenshot_mode/README.md b/src/plugins/screenshot_mode/README.md new file mode 100755 index 0000000000000..faa298b33d5fa --- /dev/null +++ b/src/plugins/screenshot_mode/README.md @@ -0,0 +1,27 @@ +# Screenshot Mode + +The service exposed by this plugin informs consumers whether they should optimize for non-interactivity. In this way plugins can avoid loading unnecessary code, data or other services. + +The primary intention is to inform other lower-level plugins (plugins that don't depend on other plugins) that we do not expect an actual user to interact with browser. In this way we can avoid loading unnecessary resources (code and data). + +**NB** This plugin should have no other dependencies to avoid any possibility of circular dependencies. + +--- + +## Development + +### How to test in screenshot mode + +Please note: the following information is subject to change over time. + +In order to test whether we are correctly detecting screenshot mode, developers can run the following JS snippet: + +```js +window.localStorage.setItem('__KBN_SCREENSHOT_MODE_ENABLED_KEY__', true); +``` + +To get out of screenshot mode, run the following snippet: + +```js +window.localStorage.removeItem('__KBN_SCREENSHOT_MODE_ENABLED_KEY__'); +``` diff --git a/src/plugins/screenshot_mode/common/constants.ts b/src/plugins/screenshot_mode/common/constants.ts new file mode 100644 index 0000000000000..d5073f5920c0e --- /dev/null +++ b/src/plugins/screenshot_mode/common/constants.ts @@ -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 KBN_SCREENSHOT_MODE_HEADER = 'x-kbn-screenshot-mode'.toLowerCase(); diff --git a/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts b/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.ts new file mode 100644 index 0000000000000..7714f88cebeec --- /dev/null +++ b/src/plugins/screenshot_mode/common/get_set_browser_screenshot_mode.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 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. + */ + +// **PLEASE NOTE** +// The functionality in this file targets a browser environment and is intended to be used both in public and server. +// For instance, reporting uses these functions when starting puppeteer to set the current browser into "screenshot" mode. + +export const KBN_SCREENSHOT_MODE_ENABLED_KEY = '__KBN_SCREENSHOT_MODE_ENABLED_KEY__'; + +/** + * This function is responsible for detecting whether we are currently in screenshot mode. + * + * We check in the current window context whether screenshot mode is enabled, otherwise we check + * localStorage. The ability to set a value in localStorage enables more convenient development and testing + * in functionality that needs to detect screenshot mode. + */ +export const getScreenshotMode = (): boolean => { + return ( + ((window as unknown) as Record)[KBN_SCREENSHOT_MODE_ENABLED_KEY] === true || + window.localStorage.getItem(KBN_SCREENSHOT_MODE_ENABLED_KEY) === 'true' + ); +}; + +/** + * Use this function to set the current browser to screenshot mode. + * + * This function should be called as early as possible to ensure that screenshot mode is + * correctly detected for the first page load. It is not suitable for use inside any plugin + * code unless the plugin code is guaranteed to, somehow, load before any other code. + * + * Additionally, we don't know what environment this code will run in so we remove as many external + * references as possible to make it portable. For instance, running inside puppeteer. + */ +export const setScreenshotModeEnabled = () => { + Object.defineProperty( + window, + '__KBN_SCREENSHOT_MODE_ENABLED_KEY__', // Literal value to prevent adding an external reference + { + enumerable: true, + writable: true, + configurable: false, + value: true, + } + ); +}; + +export const setScreenshotModeDisabled = () => { + Object.defineProperty( + window, + '__KBN_SCREENSHOT_MODE_ENABLED_KEY__', // Literal value to prevent adding an external reference + { + enumerable: true, + writable: true, + configurable: false, + value: undefined, + } + ); +}; diff --git a/src/plugins/screenshot_mode/common/index.ts b/src/plugins/screenshot_mode/common/index.ts new file mode 100644 index 0000000000000..fd9ad6f70feba --- /dev/null +++ b/src/plugins/screenshot_mode/common/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { + getScreenshotMode, + setScreenshotModeEnabled, + setScreenshotModeDisabled, +} from './get_set_browser_screenshot_mode'; + +export { KBN_SCREENSHOT_MODE_HEADER } from './constants'; diff --git a/src/plugins/screenshot_mode/jest.config.js b/src/plugins/screenshot_mode/jest.config.js new file mode 100644 index 0000000000000..e84f3742f8c1d --- /dev/null +++ b/src/plugins/screenshot_mode/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/screenshot_mode'], +}; diff --git a/src/plugins/screenshot_mode/kibana.json b/src/plugins/screenshot_mode/kibana.json new file mode 100644 index 0000000000000..67c40b20be525 --- /dev/null +++ b/src/plugins/screenshot_mode/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "screenshotMode", + "version": "1.0.0", + "kibanaVersion": "kibana", + "ui": true, + "server": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts new file mode 100644 index 0000000000000..6a46b240d592e --- /dev/null +++ b/src/plugins/screenshot_mode/public/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { ScreenshotModePlugin } from './plugin'; + +export function plugin() { + return new ScreenshotModePlugin(); +} + +export { KBN_SCREENSHOT_MODE_HEADER, setScreenshotModeEnabled } from '../common'; + +export { ScreenshotModePluginSetup } from './types'; diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts new file mode 100644 index 0000000000000..33ae501466876 --- /dev/null +++ b/src/plugins/screenshot_mode/public/plugin.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { coreMock } from '../../../../src/core/public/mocks'; +import { ScreenshotModePlugin } from './plugin'; +import { setScreenshotModeEnabled, setScreenshotModeDisabled } from '../common'; + +describe('Screenshot mode public', () => { + let plugin: ScreenshotModePlugin; + + beforeEach(() => { + plugin = new ScreenshotModePlugin(); + }); + + afterAll(() => { + setScreenshotModeDisabled(); + }); + + describe('setup contract', () => { + it('detects screenshot mode "true"', () => { + setScreenshotModeEnabled(); + const screenshotMode = plugin.setup(coreMock.createSetup()); + expect(screenshotMode.isScreenshotMode()).toBe(true); + }); + + it('detects screenshot mode "false"', () => { + setScreenshotModeDisabled(); + const screenshotMode = plugin.setup(coreMock.createSetup()); + expect(screenshotMode.isScreenshotMode()).toBe(false); + }); + }); + + describe('start contract', () => { + it('returns nothing', () => { + expect(plugin.start(coreMock.createStart())).toBe(undefined); + }); + }); +}); diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts new file mode 100644 index 0000000000000..7a166566a0173 --- /dev/null +++ b/src/plugins/screenshot_mode/public/plugin.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 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 { CoreSetup, CoreStart, Plugin } from '../../../core/public'; + +import { ScreenshotModePluginSetup } from './types'; + +import { getScreenshotMode } from '../common'; + +export class ScreenshotModePlugin implements Plugin { + public setup(core: CoreSetup): ScreenshotModePluginSetup { + return { + isScreenshotMode: () => getScreenshotMode() === true, + }; + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts new file mode 100644 index 0000000000000..744ea8615f2a7 --- /dev/null +++ b/src/plugins/screenshot_mode/public/types.ts @@ -0,0 +1,17 @@ +/* + * 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 interface IScreenshotModeService { + /** + * Returns a boolean indicating whether the current user agent (browser) would like to view UI optimized for + * screenshots or printing. + */ + isScreenshotMode: () => boolean; +} + +export type ScreenshotModePluginSetup = IScreenshotModeService; diff --git a/src/plugins/screenshot_mode/server/index.ts b/src/plugins/screenshot_mode/server/index.ts new file mode 100644 index 0000000000000..68714e9a21b87 --- /dev/null +++ b/src/plugins/screenshot_mode/server/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { ScreenshotModePlugin } from './plugin'; + +export { setScreenshotModeEnabled, KBN_SCREENSHOT_MODE_HEADER } from '../common'; + +export { + ScreenshotModeRequestHandlerContext, + ScreenshotModePluginSetup, + ScreenshotModePluginStart, +} from './types'; + +export function plugin() { + return new ScreenshotModePlugin(); +} diff --git a/src/plugins/screenshot_mode/server/is_screenshot_mode.test.ts b/src/plugins/screenshot_mode/server/is_screenshot_mode.test.ts new file mode 100644 index 0000000000000..6d783970bd362 --- /dev/null +++ b/src/plugins/screenshot_mode/server/is_screenshot_mode.test.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 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 { httpServerMock } from 'src/core/server/mocks'; +import { KBN_SCREENSHOT_MODE_HEADER } from '../common'; +import { isScreenshotMode } from './is_screenshot_mode'; + +const { createKibanaRequest } = httpServerMock; + +describe('isScreenshotMode', () => { + test('screenshot headers are present', () => { + expect( + isScreenshotMode(createKibanaRequest({ headers: { [KBN_SCREENSHOT_MODE_HEADER]: 'true' } })) + ).toBe(true); + }); + + test('screenshot headers are not present', () => { + expect(isScreenshotMode(createKibanaRequest())).toBe(false); + }); +}); diff --git a/src/plugins/screenshot_mode/server/is_screenshot_mode.ts b/src/plugins/screenshot_mode/server/is_screenshot_mode.ts new file mode 100644 index 0000000000000..79787bcd1fb50 --- /dev/null +++ b/src/plugins/screenshot_mode/server/is_screenshot_mode.ts @@ -0,0 +1,16 @@ +/* + * 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 { KibanaRequest } from 'src/core/server'; +import { KBN_SCREENSHOT_MODE_HEADER } from '../common'; + +export const isScreenshotMode = (request: KibanaRequest): boolean => { + return Object.keys(request.headers).some((header) => { + return header.toLowerCase() === KBN_SCREENSHOT_MODE_HEADER; + }); +}; diff --git a/src/plugins/screenshot_mode/server/plugin.ts b/src/plugins/screenshot_mode/server/plugin.ts new file mode 100644 index 0000000000000..9ef410d999ea5 --- /dev/null +++ b/src/plugins/screenshot_mode/server/plugin.ts @@ -0,0 +1,47 @@ +/* + * 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 { Plugin, CoreSetup } from '../../../core/server'; +import { + ScreenshotModeRequestHandlerContext, + ScreenshotModePluginSetup, + ScreenshotModePluginStart, +} from './types'; +import { isScreenshotMode } from './is_screenshot_mode'; + +export class ScreenshotModePlugin + implements Plugin { + public setup(core: CoreSetup): ScreenshotModePluginSetup { + core.http.registerRouteHandlerContext( + 'screenshotMode', + (ctx, req) => { + return { + isScreenshot: isScreenshotMode(req), + }; + } + ); + + // We use "require" here to ensure the import does not have external references due to code bundling that + // commonly happens during transpiling. External references would be missing in the environment puppeteer creates. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { setScreenshotModeEnabled } = require('../common'); + + return { + setScreenshotModeEnabled, + isScreenshotMode, + }; + } + + public start(): ScreenshotModePluginStart { + return { + isScreenshotMode, + }; + } + + public stop() {} +} diff --git a/src/plugins/screenshot_mode/server/types.ts b/src/plugins/screenshot_mode/server/types.ts new file mode 100644 index 0000000000000..4347252e58fce --- /dev/null +++ b/src/plugins/screenshot_mode/server/types.ts @@ -0,0 +1,35 @@ +/* + * 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 { RequestHandlerContext, KibanaRequest } from 'src/core/server'; + +/** + * Any context that requires access to the screenshot mode flag but does not have access + * to request context {@link ScreenshotModeRequestHandlerContext}, for instance if they are pre-context, + * can use this function to check whether the request originates from a client that is in screenshot mode. + */ +type IsScreenshotMode = (request: KibanaRequest) => boolean; + +export interface ScreenshotModePluginSetup { + isScreenshotMode: IsScreenshotMode; + + /** + * Set the current environment to screenshot mode. Intended to run in a browser-environment. + */ + setScreenshotModeEnabled: () => void; +} + +export interface ScreenshotModePluginStart { + isScreenshotMode: IsScreenshotMode; +} + +export interface ScreenshotModeRequestHandlerContext extends RequestHandlerContext { + screenshotMode: { + isScreenshot: boolean; + }; +} diff --git a/src/plugins/screenshot_mode/tsconfig.json b/src/plugins/screenshot_mode/tsconfig.json new file mode 100644 index 0000000000000..58194b385448b --- /dev/null +++ b/src/plugins/screenshot_mode/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + ] +} diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json index 22768338aec37..f7e351ba3f3bc 100644 --- a/x-pack/examples/reporting_example/kibana.json +++ b/x-pack/examples/reporting_example/kibana.json @@ -5,5 +5,5 @@ "server": false, "ui": true, "optionalPlugins": [], - "requiredPlugins": ["reporting", "developerExamples", "navigation"] + "requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode"] } diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index 25a1cc767f1f5..0a865d1c9e96b 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -8,18 +8,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; -import { StartDeps } from './types'; +import { SetupDeps, StartDeps } from './types'; import { ReportingExampleApp } from './components/app'; export const renderApp = ( coreStart: CoreStart, - startDeps: StartDeps, + deps: Omit, { appBasePath, element }: AppMountParameters ) => { - ReactDOM.render( - , - element - ); + ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx index fd4a85dd06779..0174ec2a17ad4 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -26,6 +26,7 @@ import React, { useEffect, useState } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import * as Rx from 'rxjs'; import { takeWhile } from 'rxjs/operators'; +import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { CoreStart } from '../../../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public'; @@ -37,6 +38,7 @@ interface ReportingExampleAppDeps { http: CoreStart['http']; navigation: NavigationPublicPluginStart; reporting: ReportingStart; + screenshotMode: ScreenshotModePluginSetup; } const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana']; @@ -46,6 +48,7 @@ export const ReportingExampleApp = ({ notifications, http, reporting, + screenshotMode, }: ReportingExampleAppDeps) => { const { getDefaultLayoutSelectors, ReportingAPIClient } = reporting; const [logos, setLogos] = useState([]); @@ -125,6 +128,8 @@ export const ReportingExampleApp = ({ ))} + +

Screenshot Mode is {screenshotMode.isScreenshotMode() ? 'ON' : 'OFF'}!

diff --git a/x-pack/examples/reporting_example/public/plugin.ts b/x-pack/examples/reporting_example/public/plugin.ts index 6ac1cbe01db92..644ac7cc8d8a8 100644 --- a/x-pack/examples/reporting_example/public/plugin.ts +++ b/x-pack/examples/reporting_example/public/plugin.ts @@ -16,7 +16,7 @@ import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { SetupDeps, StartDeps } from './types'; export class ReportingExamplePlugin implements Plugin { - public setup(core: CoreSetup, { developerExamples, ...depsSetup }: SetupDeps): void { + public setup(core: CoreSetup, { developerExamples, screenshotMode }: SetupDeps): void { core.application.register({ id: PLUGIN_ID, title: PLUGIN_NAME, @@ -30,7 +30,7 @@ export class ReportingExamplePlugin implements Plugin { unknown ]; // Render the application - return renderApp(coreStart, { ...depsSetup, ...depsStart }, params); + return renderApp(coreStart, { ...depsStart, screenshotMode }, params); }, }); diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts index 56e8c34d9dae4..55a573285e24f 100644 --- a/x-pack/examples/reporting_example/public/types.ts +++ b/x-pack/examples/reporting_example/public/types.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; -import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; import { ReportingStart } from '../../../plugins/reporting/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -16,6 +17,7 @@ export interface PluginStart {} export interface SetupDeps { developerExamples: DeveloperExamplesSetup; + screenshotMode: ScreenshotModePluginSetup; } export interface StartDeps { navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 31f679a4ec8d0..ddba61e9a0b8d 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -2,11 +2,7 @@ "id": "reporting", "version": "8.0.0", "kibanaVersion": "kibana", - "optionalPlugins": [ - "security", - "spaces", - "usageCollection" - ], + "optionalPlugins": ["security", "spaces", "usageCollection"], "configPath": ["xpack", "reporting"], "requiredPlugins": [ "data", @@ -16,13 +12,11 @@ "uiActions", "taskManager", "embeddable", + "screenshotMode", "share", "features" ], "server": true, "ui": true, - "requiredBundles": [ - "kibanaReact", - "discover" - ] + "requiredBundles": ["kibanaReact", "discover"] } 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 914a39fdf1268..30b351ff90b6f 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 @@ -11,6 +11,8 @@ import open from 'opn'; import puppeteer, { ElementHandle, EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; import { getDisallowedOutgoingUrlError } from '../'; +import { ReportingCore } from '../../..'; +import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server'; import { ConditionalHeaders, ConditionalHeadersConditions } from '../../../export_types/common'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; @@ -59,8 +61,14 @@ export class HeadlessChromiumDriver { private listenersAttached = false; private interceptedCount = 0; + private core: ReportingCore; - constructor(page: puppeteer.Page, { inspect, networkPolicy }: ChromiumDriverOptions) { + constructor( + core: ReportingCore, + page: puppeteer.Page, + { inspect, networkPolicy }: ChromiumDriverOptions + ) { + this.core = core; this.page = page; this.inspect = inspect; this.networkPolicy = networkPolicy; @@ -98,6 +106,8 @@ export class HeadlessChromiumDriver { // Reset intercepted request count this.interceptedCount = 0; + const enableScreenshotMode = this.core.getEnableScreenshotMode(); + await this.page.evaluateOnNewDocument(enableScreenshotMode); await this.page.setRequestInterception(true); this.registerListeners(conditionalHeaders, logger); @@ -261,6 +271,7 @@ export class HeadlessChromiumDriver { { ...interceptedRequest.request.headers, ...conditionalHeaders.headers, + [KBN_SCREENSHOT_MODE_HEADER]: 'true', }, (value, name) => ({ name, diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 5fe2050ddb6f1..2005541b81ead 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -15,6 +15,7 @@ import * as Rx from 'rxjs'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { getChromiumDisconnectedError } from '../'; +import { ReportingCore } from '../../..'; import { BROWSER_TYPE } from '../../../../common/constants'; import { durationToNumber } from '../../../../common/schema_utils'; import { CaptureConfig } from '../../../../server/types'; @@ -32,11 +33,14 @@ export class HeadlessChromiumDriverFactory { private browserConfig: BrowserConfig; private userDataDir: string; private getChromiumArgs: (viewport: ViewportConfig) => string[]; + private core: ReportingCore; - constructor(binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) { + constructor(core: ReportingCore, binaryPath: string, logger: LevelLogger) { + this.core = core; this.binaryPath = binaryPath; - this.captureConfig = captureConfig; - this.browserConfig = captureConfig.browser.chromium; + const config = core.getConfig(); + this.captureConfig = config.get('capture'); + this.browserConfig = this.captureConfig.browser.chromium; if (this.browserConfig.disableSandbox) { logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`); @@ -138,7 +142,7 @@ export class HeadlessChromiumDriverFactory { this.getProcessLogger(browser, logger).subscribe(); // HeadlessChromiumDriver: object to "drive" a browser page - const driver = new HeadlessChromiumDriver(page, { + const driver = new HeadlessChromiumDriver(this.core, page, { inspect: !!this.browserConfig.inspect, networkPolicy: this.captureConfig.networkPolicy, }); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts index 0d5639254b816..e0d043f821ab4 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/index.ts @@ -7,15 +7,15 @@ import { i18n } from '@kbn/i18n'; import { BrowserDownload } from '../'; -import { CaptureConfig } from '../../../server/types'; +import { ReportingCore } from '../../../server'; import { LevelLogger } from '../../lib'; import { HeadlessChromiumDriverFactory } from './driver_factory'; import { ChromiumArchivePaths } from './paths'; export const chromium: BrowserDownload = { paths: new ChromiumArchivePaths(), - createDriverFactory: (binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) => - new HeadlessChromiumDriverFactory(binaryPath, captureConfig, logger), + createDriverFactory: (core: ReportingCore, binaryPath: string, logger: LevelLogger) => + new HeadlessChromiumDriverFactory(core, binaryPath, logger), }; export const getChromiumDisconnectedError = () => diff --git a/x-pack/plugins/reporting/server/browsers/index.ts b/x-pack/plugins/reporting/server/browsers/index.ts index df95b69d9d254..c47514960bb09 100644 --- a/x-pack/plugins/reporting/server/browsers/index.ts +++ b/x-pack/plugins/reporting/server/browsers/index.ts @@ -6,9 +6,8 @@ */ import { first } from 'rxjs/operators'; -import { ReportingConfig } from '../'; +import { ReportingCore } from '../'; import { LevelLogger } from '../lib'; -import { CaptureConfig } from '../types'; import { chromium, ChromiumArchivePaths } from './chromium'; import { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; import { installBrowser } from './install'; @@ -18,8 +17,8 @@ export { HeadlessChromiumDriver } from './chromium/driver'; export { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; type CreateDriverFactory = ( + core: ReportingCore, binaryPath: string, - captureConfig: CaptureConfig, logger: LevelLogger ) => HeadlessChromiumDriverFactory; @@ -28,12 +27,8 @@ export interface BrowserDownload { paths: ChromiumArchivePaths; } -export const initializeBrowserDriverFactory = async ( - config: ReportingConfig, - logger: LevelLogger -) => { +export const initializeBrowserDriverFactory = async (core: ReportingCore, logger: LevelLogger) => { const { binaryPath$ } = installBrowser(logger); const binaryPath = await binaryPath$.pipe(first()).toPromise(); - const captureConfig = config.get('capture'); - return chromium.createDriverFactory(binaryPath, captureConfig, logger); + return chromium.createDriverFactory(core, binaryPath, logger); }; diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 62cab5a8fef19..2d55a4aa7fa6d 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -8,6 +8,7 @@ import Hapi from '@hapi/hapi'; import * as Rx from 'rxjs'; import { first, map, take } from 'rxjs/operators'; +import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { BasePath, IClusterClient, @@ -41,6 +42,7 @@ export interface ReportingInternalSetup { security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; + screenshotMode: ScreenshotModePluginSetup; logger: LevelLogger; } @@ -237,6 +239,11 @@ export class ReportingCore { return screenshotsObservableFactory(config.get('capture'), browserDriverFactory); } + public getEnableScreenshotMode() { + const { screenshotMode } = this.getPluginSetupDeps(); + return screenshotMode.setScreenshotModeEnabled; + } + /* * Gives synchronous access to the setupDeps */ diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index a10f1f7a3788d..dd8aadb49a5ba 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -19,6 +19,7 @@ jest.mock('puppeteer', () => ({ import moment from 'moment'; import * as Rx from 'rxjs'; +import { ReportingCore } from '../..'; import { HeadlessChromiumDriver } from '../../browsers'; import { ConditionalHeaders } from '../../export_types/common'; import { @@ -27,6 +28,7 @@ import { createMockConfigSchema, createMockLayoutInstance, createMockLevelLogger, + createMockReportingCore, } from '../../test_helpers'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; @@ -37,7 +39,7 @@ import { screenshotsObservableFactory } from './observable'; */ const logger = createMockLevelLogger(); -const reportingConfig = { +const mockSchema = createMockConfigSchema({ capture: { loadDelay: moment.duration(2, 's'), timeouts: { @@ -46,12 +48,13 @@ const reportingConfig = { renderComplete: moment.duration(10, 's'), }, }, -}; -const mockSchema = createMockConfigSchema(reportingConfig); +}); const mockConfig = createMockConfig(mockSchema); const captureConfig = mockConfig.get('capture'); const mockLayout = createMockLayoutInstance(captureConfig); +let core: ReportingCore; + /* * Tests */ @@ -59,7 +62,8 @@ describe('Screenshot Observable Pipeline', () => { let mockBrowserDriverFactory: any; beforeEach(async () => { - mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, {}); + core = await createMockReportingCore(mockSchema); + mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {}); }); it('pipelines a single url into screenshot and timeRange', async () => { @@ -118,7 +122,7 @@ describe('Screenshot Observable Pipeline', () => { const mockOpen = jest.fn(); // mocks - mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { screenshot: mockScreenshot, open: mockOpen, }); @@ -218,7 +222,7 @@ describe('Screenshot Observable Pipeline', () => { }); // mocks - mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { waitForSelector: mockWaitForSelector, }); @@ -312,7 +316,7 @@ describe('Screenshot Observable Pipeline', () => { return Rx.never().toPromise(); }); - mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { getCreatePage: mockGetCreatePage, waitForSelector: mockWaitForSelector, }); @@ -345,7 +349,7 @@ describe('Screenshot Observable Pipeline', () => { return Promise.resolve(); } }); - mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger, { + mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { evaluate: mockBrowserEvaluate, }); mockLayout.getViewport = () => null; diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 26a9be2b15c3f..fc52e10dd0cf9 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -48,12 +48,13 @@ export class ReportingPlugin registerUiSettings(core); const { http } = core; - const { features, licensing, security, spaces, taskManager } = plugins; + const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins; const router = http.createRouter(); const basePath = http.basePath; reportingCore.pluginSetup({ + screenshotMode, features, licensing, basePath, @@ -91,9 +92,8 @@ export class ReportingPlugin // async background start (async () => { await reportingCore.pluginSetsUp(); - const config = reportingCore.getConfig(); - const browserDriverFactory = await initializeBrowserDriverFactory(config, this.logger); + const browserDriverFactory = await initializeBrowserDriverFactory(reportingCore, this.logger); const store = new ReportingStore(reportingCore, this.logger); await reportingCore.pluginStart({ diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 3446160c0d7f5..7dd7c246e9a04 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -8,6 +8,7 @@ import moment from 'moment'; import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; +import { ReportingCore } from '..'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; import { LevelLogger } from '../lib'; import { ElementsPositionAndAttribute } from '../lib/screenshots'; @@ -96,6 +97,7 @@ const defaultOpts: CreateMockBrowserDriverFactoryOpts = { }; export const createMockBrowserDriverFactory = async ( + core: ReportingCore, logger: LevelLogger, opts: Partial = {} ): Promise => { @@ -122,9 +124,9 @@ export const createMockBrowserDriverFactory = async ( }; const binaryPath = '/usr/local/share/common/secure/super_awesome_binary'; - const mockBrowserDriverFactory = chromium.createDriverFactory(binaryPath, captureConfig, logger); + const mockBrowserDriverFactory = chromium.createDriverFactory(core, binaryPath, logger); const mockPage = ({ setViewport: () => {} } as unknown) as Page; - const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { + const mockBrowserDriver = new HeadlessChromiumDriver(core, mockPage, { inspect: true, networkPolicy: captureConfig.networkPolicy, }); diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 757d1a68075a8..7df1dce597d56 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,6 +8,7 @@ import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; +import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -32,6 +33,7 @@ export interface ReportingSetupDeps { spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; } export interface ReportingStartDeps { diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 88e8d343f4700..c28086b96aea2 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -20,6 +20,7 @@ { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/screenshot_mode/tsconfig.json" }, { "path": "../../../src/plugins/share/tsconfig.json" }, { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" },