mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
0b2da10a7d
commit
3627b866a3
25 changed files with 732 additions and 38 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
87
dev_docs/tutorials/screenshotting.mdx
Normal file
87
dev_docs/tutorials/screenshotting.mdx
Normal 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>
|
|
@ -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" },
|
||||
|
|
13
x-pack/examples/screenshotting_example/README.md
Executable file
13
x-pack/examples/screenshotting_example/README.md
Executable 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
|
||||
```
|
16
x-pack/examples/screenshotting_example/common/index.ts
Normal file
16
x-pack/examples/screenshotting_example/common/index.ts
Normal 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';
|
19
x-pack/examples/screenshotting_example/kibana.json
Normal file
19
x-pack/examples/screenshotting_example/kibana.json
Normal 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"
|
||||
]
|
||||
}
|
137
x-pack/examples/screenshotting_example/public/app/app.tsx
Normal file
137
x-pack/examples/screenshotting_example/public/app/app.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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';
|
12
x-pack/examples/screenshotting_example/public/index.ts
Normal file
12
x-pack/examples/screenshotting_example/public/index.ts
Normal 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();
|
||||
}
|
50
x-pack/examples/screenshotting_example/public/plugin.tsx
Normal file
50
x-pack/examples/screenshotting_example/public/plugin.tsx
Normal 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() {}
|
||||
}
|
12
x-pack/examples/screenshotting_example/server/index.ts
Normal file
12
x-pack/examples/screenshotting_example/server/index.ts
Normal 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();
|
||||
}
|
52
x-pack/examples/screenshotting_example/server/plugin.ts
Normal file
52
x-pack/examples/screenshotting_example/server/plugin.ts
Normal 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() {}
|
||||
}
|
23
x-pack/examples/screenshotting_example/tsconfig.json
Normal file
23
x-pack/examples/screenshotting_example/tsconfig.json
Normal 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" }
|
||||
]
|
||||
}
|
|
@ -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.`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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]],
|
||||
});
|
||||
}),
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -24,7 +24,7 @@ export interface PngScreenshotOptions extends CaptureOptions {
|
|||
*/
|
||||
format?: 'png';
|
||||
|
||||
layout: PngLayoutParams;
|
||||
layout?: PngLayoutParams;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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: {
|
||||
|
|
87
x-pack/test/examples/screenshotting/index.ts
Normal file
87
x-pack/test/examples/screenshotting/index.ts
Normal 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.');
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue