[Screenshotting] Documentation (#129794)

* Provide defaults for the screenshotting options to make usage closer to the zero-conf
* Add screenshotting example integration
* Add integration tests to cover screenshotting example
* Add screenshotting plugin readme
* Add tutorial to the developer guide
This commit is contained in:
Michael Dokolin 2022-04-15 08:29:05 +02:00 committed by GitHub
parent 0b2da10a7d
commit 3627b866a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 732 additions and 38 deletions

2
.github/CODEOWNERS vendored
View file

@ -462,10 +462,12 @@ x-pack/plugins/session_view @elastic/awp-platform
# Reporting
/x-pack/examples/reporting_example/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/examples/screenshotting_example/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/plugins/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/functional/apps/dashboard/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/functional/apps/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/functional/apps/reporting_management/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/examples/screenshotting/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/functional/es_archives/lens/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/functional/es_archives/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services
/x-pack/test/functional/fixtures/kbn_archiver/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services

View file

@ -0,0 +1,87 @@
---
id: kibDevTutorialScreenshotting
slug: /kibana-dev-docs/tutorials/screenshotting
title: Kibana Screenshotting Service
summary: Kibana Screenshotting Service
date: 2022-04-12
tags: ['kibana', 'onboarding', 'dev', 'architecture']
---
## Screenshotting Plugin
This plugin provides functionality to take screenshots of the Kibana pages.
It uses Chromium and Puppeteer underneath to run the browser in headless mode.
If you are planning to integrate with the screenshotting plugin, please get in touch with the App Services team to know all the limitations.
### Capabilities
- Canvas workpads screenshots.
- Dashboards screenshots.
- Expressions screenshots.
- PDF generation.
- Batch screenshotting.
### Usage
After listing the `screenshotting` plugin in your dependencies, the plugin will be intitalized on the setup stage.
The intitalization process downloads (if it is not already present) and verifies the Chromium build.
The start contract exposes a public API to interact with the plugin.
Apart from the actual screenshotting functionality, it also provides a way for self-diagnostics.
Here is an example of how you can take a screenshot of a Kibana URL.
```typescript
import { lastValueFrom } from 'rxjs';
import type { CoreSetup, Plugin } from 'src/core/server';
import type { ScreenshottingStart } from 'x-pack/plugins/screenshotting/server';
interface StartDeps {
screenshotting: ScreenshottingStart;
}
class ExamplePlugin implements Plugin<void, void, void, StartDeps> {
setup({ http, getStartServices }: CoreSetup<StartDeps>) {
const router = http.createRouter();
router.get(
{
path: '/api/capture',
validate: {
query: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response) => {
const [, { screenshotting }] = await getStartServices();
const { metrics, results } = await lastValueFrom(
screenshotting.getScreenshots({
request,
urls: [`http://localhost/app/canvas#/workpad/workpad-${request.query.id}`],
})
);
return response.ok({
body: JSON.stringify({
metrics,
image: results[0]?.screenshots[0]?.data.toString('base64'),
errors: results[0]?.renderErrors,
} as ScreenshottingExpressionResponse),
});
}
);
}
start() {}
}
export function plugin() {
return new ExamplePlugin();
}
```
<DocCallOut>
Check the complete API reference <DocLink id="kibScreenshottingPluginApi" text="here" />.
</DocCallOut>

View file

@ -63,6 +63,7 @@
{ "id": "kibDevTutorialServerEndpoint" },
{ "id": "kibDevTutorialAdvancedSettings" },
{ "id": "kibDevSharePluginReadme" }
{ "id": "kibDevTutorialScreenshotting" }
]
},
{
@ -146,6 +147,7 @@
{ "id": "kibSavedObjectsTaggingOssPluginApi" },
{ "id": "kibSavedObjectsTaggingPluginApi" },
{ "id": "kibSavedObjectsPluginApi" },
{ "id": "kibScreenshottingPluginApi" },
{ "id": "kibSecuritySolutionPluginApi" },
{ "id": "kibSecurityPluginApi" },
{ "id": "kibSharePluginApi" },

View file

@ -0,0 +1,13 @@
# Screenshotting Example
Screenshotting integration example plugin.
This plugin demonstrates the usage of the screenshotting plugin capabilities. The interaction happens on the backend via an API endpoint exposed by the plugin.
The plugin provides a way to input an expression on the front-end side. The expression will be rendered by an internal screenshotting application in Chromium on the backend.
To run this example, use the following command:
```bash
$ yarn start --run-examples
```

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ScreenshotResult } from '../../../plugins/screenshotting/server';
export interface ScreenshottingExpressionResponse {
errors?: string[];
image?: string;
metrics?: ScreenshotResult['metrics'];
}
export const API_ENDPOINT = '/api/examples/screenshotting/expression';

View file

@ -0,0 +1,19 @@
{
"id": "screenshottingExample",
"version": "0.1.0",
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"owner": {
"name": "Kibana Reporting Services",
"githubTeam": "kibana-reporting-services"
},
"description": "An example integration with the screenshotting plugin.",
"optionalPlugins": [],
"requiredPlugins": [
"screenshotting",
"developerExamples",
"kibanaReact",
"navigation"
]
}

View file

@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useContext, useState } from 'react';
import {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiImage,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiSpacer,
EuiStat,
EuiText,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import { API_ENDPOINT, ScreenshottingExpressionResponse } from '../../common';
import { HttpContext } from './http_context';
export function App() {
const http = useContext(HttpContext);
const [expression, setExpression] = useState<string>();
const [loading, setLoading] = useState(false);
const [response, setResponse] = useState<ScreenshottingExpressionResponse>();
const handleClick = useCallback(async () => {
try {
setLoading(true);
setResponse(
await http?.get<ScreenshottingExpressionResponse>(API_ENDPOINT, {
query: { expression },
})
);
} finally {
setLoading(false);
}
}, [expression, http]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setExpression(e.target.value);
}, []);
return (
<EuiPage>
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Screenshotting Demo</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiText>
<p>This example captures a screenshot of an expression provided below.</p>
</EuiText>
<EuiSpacer size={'m'} />
<EuiTextArea
placeholder="Expression to render"
fullWidth
onChange={handleChange}
data-test-subj="expression"
/>
<EuiSpacer size={'m'} />
<EuiButton
iconType="play"
onClick={handleClick}
isDisabled={!expression}
isLoading={loading}
data-test-subj="run"
>
Run
</EuiButton>
{!!response && <EuiHorizontalRule />}
{response?.errors && (
<>
<EuiCallOut
title="Sorry, there was an error"
color="danger"
iconType="alert"
data-test-subj="error"
>
<p>{response.errors.join('\n')}</p>
</EuiCallOut>
<EuiSpacer size={'m'} />
</>
)}
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{response?.image && (
<EuiImage
src={`data:image/png;base64,${response.image}`}
alt="Screenshot"
size="xl"
allowFullScreen
hasShadow
data-test-subj="image"
/>
)}
</EuiFlexItem>
<EuiFlexItem>
{response?.metrics && (
<>
<EuiStat
title={`${response.metrics.cpuInPercentage ?? 'N/A'}%`}
description="CPU"
titleColor="primary"
data-test-subj="cpu"
/>
<EuiSpacer size={'m'} />
<EuiStat
title={`${response.metrics.memoryInMegabytes ?? 'N/A'} MB`}
description="Memory"
titleColor="primary"
data-test-subj="memory"
/>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createContext } from 'react';
import type { HttpStart } from 'src/core/public';
export const HttpContext = createContext<HttpStart | undefined>(undefined);

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './app';
export * from './http_context';

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ScreenshottingExamplePlugin } from './plugin';
export function plugin() {
return new ScreenshottingExamplePlugin();
}

View file

@ -0,0 +1,50 @@
/*
* 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 React from 'react';
import ReactDOM from 'react-dom';
import type { AppMountParameters, CoreSetup, Plugin } from 'src/core/public';
import type { DeveloperExamplesSetup } from 'examples/developer_examples/public';
import { AppNavLinkStatus } from '../../../../src/core/public';
import { App, HttpContext } from './app';
interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
}
const APPLICATION_ID = 'screenshottingExample';
const APPLICATION_NAME = 'Screenshotting Example';
export class ScreenshottingExamplePlugin implements Plugin<void, void> {
setup({ application, getStartServices }: CoreSetup, { developerExamples }: SetupDeps): void {
application.register({
id: APPLICATION_ID,
title: APPLICATION_NAME,
navLinkStatus: AppNavLinkStatus.hidden,
mount: async ({ element }: AppMountParameters) => {
const [{ http }] = await getStartServices();
ReactDOM.render(
<HttpContext.Provider value={http}>
<App />
</HttpContext.Provider>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
},
});
developerExamples.register({
appId: APPLICATION_ID,
title: APPLICATION_NAME,
description: 'An example integration with the screenshotting plugin.',
});
}
start() {}
}

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ScreenshottingExamplePlugin } from './plugin';
export function plugin() {
return new ScreenshottingExamplePlugin();
}

View file

@ -0,0 +1,52 @@
/*
* 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 { lastValueFrom } from 'rxjs';
import { schema } from '@kbn/config-schema';
import type { CoreSetup, Plugin } from 'src/core/server';
import type { ScreenshottingStart } from '../../../plugins/screenshotting/server';
import { API_ENDPOINT, ScreenshottingExpressionResponse } from '../common';
interface StartDeps {
screenshotting: ScreenshottingStart;
}
export class ScreenshottingExamplePlugin implements Plugin<void, void> {
setup({ http, getStartServices }: CoreSetup<StartDeps>) {
const router = http.createRouter();
router.get(
{
path: API_ENDPOINT,
validate: {
query: schema.object({
expression: schema.string(),
}),
},
},
async (context, request, response) => {
const [, { screenshotting }] = await getStartServices();
const { metrics, results } = await lastValueFrom(
screenshotting.getScreenshots({
request,
expression: request.query.expression,
})
);
return response.ok({
body: JSON.stringify({
metrics,
image: results[0]?.screenshots[0]?.data.toString('base64'),
errors: results[0]?.renderErrors,
} as ScreenshottingExpressionResponse),
});
}
);
}
start() {}
}

View file

@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types"
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"common/**/*.ts",
"../../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/navigation/tsconfig.json" },
{ "path": "../../../src/plugins/screenshot_mode/tsconfig.json" },
{ "path": "../../../examples/developer_examples/tsconfig.json" },
{ "path": "../../plugins/screenshotting/tsconfig.json" }
]
}

View file

@ -27,7 +27,7 @@ export function generatePngObservable(
options: Omit<PngScreenshotOptions, 'format'>
): Rx.Observable<PngResult> {
const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE);
if (!options.layout.dimensions) {
if (!options.layout?.dimensions) {
throw new Error(`LayoutParams.Dimensions is undefined.`);
}

View file

@ -41,8 +41,8 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNG>> =
...job.layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: job.layout?.id as PngScreenshotOptions['layout']['id'],
},
id: job.layout?.id,
} as PngScreenshotOptions['layout'],
});
}),
tap(({ buffer }) => stream.write(buffer)),

View file

@ -42,8 +42,8 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNGV2>> =
...job.layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: job.layout?.id as PngScreenshotOptions['layout']['id'],
},
id: job.layout?.id,
} as PngScreenshotOptions['layout'],
urls: [[url, locatorParams]],
});
}),

View file

@ -47,8 +47,8 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDF>> =
...layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: layout?.id as PdfScreenshotOptions['layout']['id'],
},
id: layout?.id,
} as PdfScreenshotOptions['layout'],
});
}),
tap(({ buffer }) => {

View file

@ -45,8 +45,8 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDFV2>> =
...layout,
// TODO: We do not do a runtime check for supported layout id types for now. But technically
// we should.
id: layout?.id as PdfScreenshotOptions['layout']['id'],
},
id: layout?.id,
} as PdfScreenshotOptions['layout'],
});
}),
tap(({ buffer }) => {

View file

@ -3,18 +3,154 @@
This plugin provides functionality to take screenshots of the Kibana pages.
It uses Chromium and Puppeteer underneath to run the browser in headless mode.
## API
## Capabilities
- Canvas workpads screenshots.
- Dashboards screenshots.
- Expressions screenshots.
- PDF generation.
- Batch screenshotting.
The plugin exposes most of the functionality in the start contract.
The Chromium download and setup is happening during the setup stage.
## Usage
To learn more about the public API, please use automatically generated API reference or generated TypeDoc comments.
### Getting started
After listing the `screenshotting` plugin in your dependencies, the plugin will be intitalized on the setup stage.
The intitalization process downloads (if it is not already present) and verifies the Chromium build.
## Testing Chromium downloads
The start contract exposes a public API to interact with the plugin.
Apart from the actual screenshotting functionality, it also provides a way for self-diagnostics.
Here is an example of how you can take a screenshot of a Kibana URL.
```typescript
import { lastValueFrom } from 'rxjs';
import type { CoreSetup, Plugin } from 'src/core/server';
import type { ScreenshottingStart } from 'x-pack/plugins/screenshotting/server';
interface StartDeps {
screenshotting: ScreenshottingStart;
}
class ExamplePlugin implements Plugin<void, void, void, StartDeps> {
setup({ http, getStartServices }: CoreSetup<StartDeps>) {
const router = http.createRouter();
router.get(
{
path: '/api/capture',
validate: {
query: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response) => {
const [, { screenshotting }] = await getStartServices();
const { metrics, results } = await lastValueFrom(
screenshotting.getScreenshots({
request,
urls: [`http://localhost/app/canvas#/workpad/workpad-${request.query.id}`],
})
);
return response.ok({
body: JSON.stringify({
metrics,
image: results[0]?.screenshots[0]?.data.toString('base64'),
errors: results[0]?.renderErrors,
} as ScreenshottingExpressionResponse),
});
}
);
}
start() {}
}
export function plugin() {
return new ExamplePlugin();
}
```
### API
Please use automatically generated API reference or generated TypeDoc comments to find the complete documentation.
#### `getScreenshots(options): Observable`
Takes screenshots of multiple pages or an expression and returns an observable with the screenshotting results.
The `options` parameter is an object with parameters of the screenshotting session.
Option | Required | Default | Description
--- | :---: | --- | ---
`browserTimezone` | no | _none_ | The browser timezone that will be emulated in the browser instance. This option should be used to keep timezone on server and client in sync.
`expression` | no | _none_ | An expression to capture screenshot of. Mutually exclusive with the `urls` parameter.
`format` | no | `'png'` | An output format. It can either be PDF or PNG. In case of capturing multiple URLs, all the screenshots will be combined into one document for PDF format. For PNG format, an array of screenshots will be returned.
`headers` | no | _none_ | Custom headers to be sent with each request. The headers will be used for authorization.
`input` | no | `undefined` | The expression input.
`layout` | no | `{}` | Page layout parameters describing characteristics of the capturing screenshot (e.g., dimensions, zoom, etc.).
`request` | no | _none_ | Kibana Request reference to extract headers from.
`timeouts` | no | _none_ | Timeouts for each phase of the screenshot.
`timeouts.loadDelay` | no | `3000` | The amount of time in milliseconds before taking a screenshot when visualizations are not evented. All visualizations that ship with Kibana are evented, so this setting should not have much effect. If you are seeing empty images instead of visualizations, try increasing this value.
`timeouts.openUrl` | no | `60000` | The timeout in milliseconds to allow the Chromium browser to wait for the "Loading…" screen to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current page, and the result structure contains an error message.
`timeouts.renderComplete` | no | `30000` | The timeout in milliseconds to allow the Chromium browser to wait for all visualizations to fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the result structure contains an error message.
`timeouts.waitForElements` | no | `30000` | The timeout in milliseconds to allow the Chromium browser to wait for all visualization panels to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the result structure contains an error message.
`urls` | no | `[]` | The list or URL to take screenshots of. Every item can either be a string or a tuple containing a URL and a context. The contextual data can be gathered using the screenshot mode plugin. Mutually exclusive with the `expression` parameter.
#### `diagnose(flags?: string[]): Observable`
Runs browser diagnostics.
The diagnostic implementation launches Chromium and emits the output in the resulting observable.
There is a way to override some Chromium command line arguments using the `flags` parameter.
### Configuration
Option | Default | Description
--- | --- | ---
`xpack.screenshotting.networkPolicy.enabled` | `true` | Capturing a screenshot from a Kibana page involves sending out requests for all the linked web assets. For example, a Markdown visualization can show an image from a remote server.
`xpack.screenshotting.networkPolicy.rules` | Allow http, https, ws, wss, and data. | A policy is specified as an array of objects that describe what to allow or deny based on a host or protocol. If a host or protocol is not specified, the rule matches any host or protocol.
`xpack.screenshotting.browser.autoDownload` | Depends on the `dist` parameter. | Flag to automatically download chromium distribution.
`xpack.screenshotting.browser.chromium.inspect` | Depends on the `dist` parameter. | Connects to the browser over a pipe instead of a WebSocket. See [puppeteer](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions) documentation.
`xpack.screenshotting.browser.chromium.disableSandbox` | Defaults to `false` for all operating systems except Debian and Red Hat Linux, which use `true`. | It is recommended that you research the feasibility of enabling unprivileged user namespaces. An exception is if you are running Kibana in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters. For more information, refer to [Chromium sandbox](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/sandboxing.md).
`xpack.screenshotting.browser.chromium.proxy.enabled` | `false` | Enables the proxy for Chromium to use.
`xpack.screenshotting.browser.chromium.proxy.server` | _none_ | The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported.
`xpack.screenshotting.browser.chromium.proxy.bypass` | `[]` | An array of hosts that should not go through the proxy server and should use a direct connection instead. Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601".
## How It Works
```mermaid
sequenceDiagram
participant User
participant Screenshotting
participant Browser
User ->> Screenshotting: API call
Screenshotting ->> Browser: Launch browser
activate Browser
Screenshotting ->> Browser: Create page
Screenshotting ->> Browser: Set parameters
Note over Screenshotting,Browser: timezone
Screenshotting ->> Browser: Open URL
Screenshotting ->> Browser: Set contextual data
Note over Screenshotting,Browser: custom context, screenshot mode flag
Browser ->> Screenshotting: Rendering
Screenshotting ->> Browser: Wait for visualizations
Note over Screenshotting,Browser: poll for a number of DOM nodes to match <br> the number of dashboard elements
Screenshotting ->> Browser: Wait for render completion
Note over Screenshotting,Browser: poll for selectors indicating rendering completion
Browser ->> Screenshotting: Page is ready
Screenshotting ->> Browser: Take screenshot
Browser ->> Screenshotting: Return PNG buffer
Screenshotting ->> User: Return screenshot
```
## Testing
### Integration
There is an example plugin that demonstrates integration with the screenshotting plugin. That plugin utilizes expression capturing.
### Chromium Downloads
To download all Chromium browsers for all platforms and architectures:
```
```bash
cd x-pack
npx gulp downloadChromium
```

View file

@ -46,7 +46,7 @@ export interface PdfScreenshotOptions extends CaptureOptions {
/**
* We default to the "print" layout if no ID is specified for the layout
*/
layout: PdfLayoutParams;
layout?: PdfLayoutParams;
}
export interface PdfScreenshotMetrics extends Partial<CaptureMetrics> {

View file

@ -24,7 +24,7 @@ export interface PngScreenshotOptions extends CaptureOptions {
*/
format?: 'png';
layout: PngLayoutParams;
layout?: PngLayoutParams;
}
/**

View file

@ -6,11 +6,12 @@
*/
import ipaddr from 'ipaddr.js';
import { sum } from 'lodash';
import { sum, defaultsDeep } from 'lodash';
import apm from 'elastic-apm-node';
import type { Transaction } from 'elastic-apm-node';
import { from, of, Observable } from 'rxjs';
import type { Optional } from '@kbn/utility-types';
import type { DeepPartial } from 'utility-types';
import {
catchError,
concatMap,
@ -49,7 +50,9 @@ import { Semaphore } from './semaphore';
export type { UrlOrUrlWithContext } from './observable';
export type { ScreenshotObservableResult } from './observable';
export interface CaptureOptions extends Optional<ScreenshotObservableOptions, 'urls'> {
export interface CaptureOptions
extends Optional<Omit<ScreenshotObservableOptions, 'timeouts'>, 'urls'>,
DeepPartial<Pick<ScreenshotObservableOptions, 'timeouts'>> {
/**
* Expression to render. Mutually exclusive with `urls`.
*/
@ -63,7 +66,7 @@ export interface CaptureOptions extends Optional<ScreenshotObservableOptions, 'u
/**
* Layout parameters.
*/
layout: LayoutParams;
layout?: LayoutParams;
/**
* Source Kibana request object from where the headers will be extracted.
@ -108,7 +111,7 @@ export class Screenshots {
private createLayout(transaction: Transaction | null, options: CaptureOptions): Layout {
const apmCreateLayout = transaction?.startSpan('create-layout', 'setup');
const layout = createLayout(options.layout);
const layout = createLayout(options.layout ?? {});
this.logger.debug(`Layout: width=${layout.width} height=${layout.height}`);
apmCreateLayout?.end();
@ -188,30 +191,52 @@ export class Screenshots {
return `${protocol}://${hostname}:${port}${this.http.basePath.serverBasePath}/app/${SCREENSHOTTING_APP_ID}`;
}
private getCaptureOptions({
expression,
input,
request,
...options
}: ScreenshotOptions): ScreenshotObservableOptions {
const headers = { ...(request?.headers ?? {}), ...(options.headers ?? {}) };
const urls = expression
? [
[
this.getScreenshottingAppUrl(),
{
[SCREENSHOTTING_EXPRESSION]: expression,
[SCREENSHOTTING_EXPRESSION_INPUT]: input,
},
] as UrlOrUrlWithContext,
]
: options.urls;
return defaultsDeep(
{
...options,
headers,
urls,
},
{
timeouts: {
openUrl: 60000,
waitForElements: 30000,
renderComplete: 30000,
loadDelay: 3000,
},
urls: [],
}
);
}
getScreenshots(options: PngScreenshotOptions): Observable<PngScreenshotResult>;
getScreenshots(options: PdfScreenshotOptions): Observable<PdfScreenshotResult>;
getScreenshots(options: ScreenshotOptions): Observable<ScreenshotResult>;
getScreenshots(options: ScreenshotOptions): Observable<ScreenshotResult> {
const transaction = apm.startTransaction('screenshot-pipeline', 'screenshotting');
const layout = this.createLayout(transaction, options);
const headers = { ...(options.request?.headers ?? {}), ...(options.headers ?? {}) };
const urls = options.expression
? [
[
this.getScreenshottingAppUrl(),
{
[SCREENSHOTTING_EXPRESSION]: options.expression,
[SCREENSHOTTING_EXPRESSION_INPUT]: options.input,
},
] as UrlOrUrlWithContext,
]
: options.urls ?? [];
const captureOptions = this.getCaptureOptions(options);
return this.captureScreenshots(layout, transaction, {
...options,
headers,
urls,
}).pipe(
return this.captureScreenshots(layout, transaction, captureOptions).pipe(
mergeMap((result) => {
switch (options.format) {
case 'pdf':

View file

@ -37,6 +37,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
require.resolve('./search_examples'),
require.resolve('./embedded_lens'),
require.resolve('./reporting_examples'),
require.resolve('./screenshotting'),
],
kbnTestServer: {

View file

@ -0,0 +1,87 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({
getService,
getPageObjects,
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const screenshots = getService('screenshots');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('Screenshotting Example', function () {
before(async () => {
this.tags('ciGroup13');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json');
await PageObjects.common.navigateToApp('screenshottingExample');
await testSubjects.setValue(
'expression',
`kibana
| kibana_context
| esaggs
index={indexPatternLoad id='logstash-*'}
aggs={aggCount id="1" enabled=true schema="metric"}
aggs={aggMax id="1" enabled=true schema="metric" field="bytes"}
aggs={aggTerms id="2" enabled=true schema="segment" field="response.raw" size=4 order="desc" orderBy="1"}
| metricVis metric={visdimension 0}
`
);
await testSubjects.click('run');
});
after(async () => {
await kibanaServer.importExport.unload(
'test/functional/fixtures/kbn_archiver/visualize.json'
);
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
});
it('should capture a screenshot ', async () => {
const image = await testSubjects.find('image');
const difference = await screenshots.compareAgainstBaseline(
'screenshotting_example_image',
updateBaselines,
image
);
expect(difference).to.be.lessThan(0.1);
});
it('should return memory metrics', async () => {
const memory = await testSubjects.find('memory');
const text = await memory.getVisibleText();
expect(text).to.match(/\d+\.\d+ MB/);
});
it('should return CPU metrics', async () => {
const memory = await testSubjects.find('cpu');
const text = await memory.getVisibleText();
expect(text).to.match(/\d+\.\d+%/);
});
it('should show an error message', async () => {
await testSubjects.setValue('expression', 'something');
await testSubjects.click('run');
const error = await testSubjects.find('error');
const text = await error.getVisibleText();
expect(text).to.contain('Function something could not be found.');
});
});
}