mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* very wip * - Reached first iteration of reporting body value being saved with the report for **PDF** - Removed v2 of the reporting since it looks like we may be able to make a backwards compatible change on existing PDF/PNG exports * reintroduced pdfv2 export type, see https://github.com/elastic/kibana/issues/99890\#issuecomment-851527878 * fix a whol bunch of imports * mapped out a working version for pdf * refactor to tuples * added v2 pdf to export type registry * a lot of hackery to get reports generated in v2 * added png v2, png reports with locator state * wip: refactored for loading the saved object on the redirect app URL * major wip: initial stages of reporting redirect app, need to add a way to generate v2 reports! * added a way to generate a v2 pdf from the example reporting plugin * updated reporting example app to read and accept forwarded app state * added reporting locator and updated server-side route to not use Boom * removed reporting locator for now, first iteration of reports being generated using the reporting redirect app * version with PNG working * moved png/v2 -> png_v2 * moved printable_pdf/v2 -> printable_pdf_v2 * updated share public setup and start mocks * fix types after merging master * locator -> locatorParams AND added a new endpoint for getting locator params to client * fix type import * fix types * clean up bad imports * forceNow required on v2 payloads * reworked create job interface for PNG task payload and updated consumer code report example for forcenow * put locatorparams[] back onto the reportsource interface because on baseparams it conflicts with the different export type params * move getCustomLogo and generatePng to common for export types * additional import fixes * urls -> url * chore: fix and update types and fix jest import mocks * - refactored v2 behaviour to avoid client-side request for locator instead this value is injected pre-page-load so that the redirect app can use it - refactored the interface for the getScreenshot observable factory. specifically we now expect 'urlsOrUrlTuples' to be passed in. tested with new and old report types. * updated the reporting example app to use locator migration for v2 report types * added functionality for setting forceNow * added forceNow to job payload for v2 report types and fixed shared components for v2 * write the output of v2 reports to stream * fix types for forceNow * added tests for execute job * added comments, organized imports, removed selectors from report params * fix some type issues * feedback: removed duplicated PDF code, cleaned screenshot observable function and other minor tweaks * use variable (not destructured values) and remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com>
This commit is contained in:
parent
0ec9720a63
commit
a26f0048cd
73 changed files with 1553 additions and 95 deletions
|
@ -27,6 +27,7 @@ snapshots.js
|
||||||
/x-pack/plugins/canvas/shareable_runtime/build
|
/x-pack/plugins/canvas/shareable_runtime/build
|
||||||
/x-pack/plugins/canvas/storybook/build
|
/x-pack/plugins/canvas/storybook/build
|
||||||
/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/**
|
/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/**
|
||||||
|
/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/server/lib/pdf/assets/**
|
||||||
|
|
||||||
# package overrides
|
# package overrides
|
||||||
/packages/elastic-eslint-config-kibana
|
/packages/elastic-eslint-config-kibana
|
||||||
|
|
|
@ -19,7 +19,8 @@ export interface ScreenshotModePluginSetup {
|
||||||
isScreenshotMode: IsScreenshotMode;
|
isScreenshotMode: IsScreenshotMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current environment to screenshot mode. Intended to run in a browser-environment.
|
* Set the current environment to screenshot mode. Intended to run in a browser-environment, before any other scripts
|
||||||
|
* on the page have run to ensure that screenshot mode is detected as early as possible.
|
||||||
*/
|
*/
|
||||||
setScreenshotModeEnabled: () => void;
|
setScreenshotModeEnabled: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ const createSetupContract = (): Setup => {
|
||||||
registerUrlGenerator: jest.fn(),
|
registerUrlGenerator: jest.fn(),
|
||||||
},
|
},
|
||||||
url,
|
url,
|
||||||
|
navigate: jest.fn(),
|
||||||
};
|
};
|
||||||
return setupContract;
|
return setupContract;
|
||||||
};
|
};
|
||||||
|
@ -38,6 +39,7 @@ const createStartContract = (): Start => {
|
||||||
getUrlGenerator: jest.fn(),
|
getUrlGenerator: jest.fn(),
|
||||||
},
|
},
|
||||||
toggleShareContextMenu: jest.fn(),
|
toggleShareContextMenu: jest.fn(),
|
||||||
|
navigate: jest.fn(),
|
||||||
};
|
};
|
||||||
return startContract;
|
return startContract;
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
UrlGeneratorsStart,
|
UrlGeneratorsStart,
|
||||||
} from './url_generators/url_generator_service';
|
} from './url_generators/url_generator_service';
|
||||||
import { UrlService } from '../common/url_service';
|
import { UrlService } from '../common/url_service';
|
||||||
import { RedirectManager } from './url_service';
|
import { RedirectManager, RedirectOptions } from './url_service';
|
||||||
|
|
||||||
export interface ShareSetupDependencies {
|
export interface ShareSetupDependencies {
|
||||||
securityOss?: SecurityOssPluginSetup;
|
securityOss?: SecurityOssPluginSetup;
|
||||||
|
@ -42,6 +42,12 @@ export type SharePluginSetup = ShareMenuRegistrySetup & {
|
||||||
* Utilities to work with URL locators and short URLs.
|
* Utilities to work with URL locators and short URLs.
|
||||||
*/
|
*/
|
||||||
url: UrlService;
|
url: UrlService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts serialized values for extracting a locator, migrating state from a provided version against
|
||||||
|
* the locator, then using the locator to navigate.
|
||||||
|
*/
|
||||||
|
navigate(options: RedirectOptions): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -57,12 +63,20 @@ export type SharePluginStart = ShareMenuManagerStart & {
|
||||||
* Utilities to work with URL locators and short URLs.
|
* Utilities to work with URL locators and short URLs.
|
||||||
*/
|
*/
|
||||||
url: UrlService;
|
url: UrlService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts serialized values for extracting a locator, migrating state from a provided version against
|
||||||
|
* the locator, then using the locator to navigate.
|
||||||
|
*/
|
||||||
|
navigate(options: RedirectOptions): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
||||||
private readonly shareMenuRegistry = new ShareMenuRegistry();
|
private readonly shareMenuRegistry = new ShareMenuRegistry();
|
||||||
private readonly shareContextMenu = new ShareMenuManager();
|
private readonly shareContextMenu = new ShareMenuManager();
|
||||||
private readonly urlGeneratorsService = new UrlGeneratorsService();
|
private readonly urlGeneratorsService = new UrlGeneratorsService();
|
||||||
|
|
||||||
|
private redirectManager?: RedirectManager;
|
||||||
private url?: UrlService;
|
private url?: UrlService;
|
||||||
|
|
||||||
public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup {
|
public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup {
|
||||||
|
@ -87,15 +101,16 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirectManager = new RedirectManager({
|
this.redirectManager = new RedirectManager({
|
||||||
url: this.url,
|
url: this.url,
|
||||||
});
|
});
|
||||||
redirectManager.registerRedirectApp(core);
|
this.redirectManager.registerRedirectApp(core);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...this.shareMenuRegistry.setup(),
|
...this.shareMenuRegistry.setup(),
|
||||||
urlGenerators: this.urlGeneratorsService.setup(core),
|
urlGenerators: this.urlGeneratorsService.setup(core),
|
||||||
url: this.url,
|
url: this.url,
|
||||||
|
navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +123,7 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
||||||
),
|
),
|
||||||
urlGenerators: this.urlGeneratorsService.start(core),
|
urlGenerators: this.urlGeneratorsService.start(core),
|
||||||
url: this.url!,
|
url: this.url!,
|
||||||
|
navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,10 @@ export class RedirectManager {
|
||||||
|
|
||||||
public onMount(urlLocationSearch: string) {
|
public onMount(urlLocationSearch: string) {
|
||||||
const options = this.parseSearchParams(urlLocationSearch);
|
const options = this.parseSearchParams(urlLocationSearch);
|
||||||
|
this.navigate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public navigate(options: RedirectOptions) {
|
||||||
const locator = this.deps.url.locators.get(options.id);
|
const locator = this.deps.url.locators.get(options.id);
|
||||||
|
|
||||||
if (!locator) {
|
if (!locator) {
|
||||||
|
|
|
@ -7,3 +7,9 @@
|
||||||
|
|
||||||
export const PLUGIN_ID = 'reportingExample';
|
export const PLUGIN_ID = 'reportingExample';
|
||||||
export const PLUGIN_NAME = 'reportingExample';
|
export const PLUGIN_NAME = 'reportingExample';
|
||||||
|
|
||||||
|
export {
|
||||||
|
REPORTING_EXAMPLE_LOCATOR_ID,
|
||||||
|
ReportingExampleLocatorDefinition,
|
||||||
|
ReportingExampleLocatorParams,
|
||||||
|
} from './locator';
|
||||||
|
|
30
x-pack/examples/reporting_example/common/locator.ts
Normal file
30
x-pack/examples/reporting_example/common/locator.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* 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 { SerializableRecord } from '@kbn/utility-types';
|
||||||
|
import type { LocatorDefinition } from '../../../../src/plugins/share/public';
|
||||||
|
import { PLUGIN_ID } from '../common';
|
||||||
|
|
||||||
|
export const REPORTING_EXAMPLE_LOCATOR_ID = 'REPORTING_EXAMPLE_LOCATOR_ID';
|
||||||
|
|
||||||
|
export type ReportingExampleLocatorParams = SerializableRecord;
|
||||||
|
|
||||||
|
export class ReportingExampleLocatorDefinition implements LocatorDefinition<{}> {
|
||||||
|
public readonly id = REPORTING_EXAMPLE_LOCATOR_ID;
|
||||||
|
|
||||||
|
migrations = {
|
||||||
|
'1.0.0': (state: {}) => ({ ...state, migrated: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly getLocation = async (params: {}) => {
|
||||||
|
return {
|
||||||
|
app: PLUGIN_ID,
|
||||||
|
path: '/',
|
||||||
|
state: params,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -10,5 +10,5 @@
|
||||||
},
|
},
|
||||||
"description": "Example integration code for applications to feature reports.",
|
"description": "Example integration code for applications to feature reports.",
|
||||||
"optionalPlugins": [],
|
"optionalPlugins": [],
|
||||||
"requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode"]
|
"requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,23 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
|
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
|
||||||
import { ReportingExampleApp } from './components/app';
|
import { ReportingExampleApp } from './components/app';
|
||||||
import { SetupDeps, StartDeps } from './types';
|
import { SetupDeps, StartDeps, MyForwardableState } from './types';
|
||||||
|
|
||||||
export const renderApp = (
|
export const renderApp = (
|
||||||
coreStart: CoreStart,
|
coreStart: CoreStart,
|
||||||
deps: Omit<StartDeps & SetupDeps, 'developerExamples'>,
|
deps: Omit<StartDeps & SetupDeps, 'developerExamples'>,
|
||||||
{ appBasePath, element }: AppMountParameters // FIXME: appBasePath is deprecated
|
{ appBasePath, element }: AppMountParameters, // FIXME: appBasePath is deprecated
|
||||||
|
forwardedParams: MyForwardableState
|
||||||
) => {
|
) => {
|
||||||
ReactDOM.render(<ReportingExampleApp basename={appBasePath} {...coreStart} {...deps} />, element);
|
ReactDOM.render(
|
||||||
|
<ReportingExampleApp
|
||||||
|
basename={appBasePath}
|
||||||
|
{...coreStart}
|
||||||
|
{...deps}
|
||||||
|
forwardedParams={forwardedParams}
|
||||||
|
/>,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
|
||||||
return () => ReactDOM.unmountComponentAtNode(element);
|
return () => ReactDOM.unmountComponentAtNode(element);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,10 @@ import {
|
||||||
EuiPopover,
|
EuiPopover,
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
|
EuiCodeBlock,
|
||||||
|
EuiSpacer,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import moment from 'moment';
|
||||||
import { I18nProvider } from '@kbn/i18n/react';
|
import { I18nProvider } from '@kbn/i18n/react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
|
@ -29,11 +32,18 @@ import * as Rx from 'rxjs';
|
||||||
import { takeWhile } from 'rxjs/operators';
|
import { takeWhile } from 'rxjs/operators';
|
||||||
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public';
|
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public';
|
||||||
import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public';
|
import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public';
|
||||||
|
import type { JobParamsPDFV2 } from '../../../../plugins/reporting/server/export_types/printable_pdf_v2/types';
|
||||||
|
import type { JobParamsPNGV2 } from '../../../../plugins/reporting/server/export_types/png_v2/types';
|
||||||
|
|
||||||
|
import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common';
|
||||||
|
|
||||||
|
import { MyForwardableState } from '../types';
|
||||||
|
|
||||||
interface ReportingExampleAppProps {
|
interface ReportingExampleAppProps {
|
||||||
basename: string;
|
basename: string;
|
||||||
reporting: ReportingStart;
|
reporting: ReportingStart;
|
||||||
screenshotMode: ScreenshotModePluginSetup;
|
screenshotMode: ScreenshotModePluginSetup;
|
||||||
|
forwardedParams?: MyForwardableState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana'];
|
const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana'];
|
||||||
|
@ -42,8 +52,12 @@ export const ReportingExampleApp = ({
|
||||||
basename,
|
basename,
|
||||||
reporting,
|
reporting,
|
||||||
screenshotMode,
|
screenshotMode,
|
||||||
|
forwardedParams,
|
||||||
}: ReportingExampleAppProps) => {
|
}: ReportingExampleAppProps) => {
|
||||||
const { getDefaultLayoutSelectors } = reporting;
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('forwardedParams', forwardedParams);
|
||||||
|
}, [forwardedParams]);
|
||||||
|
|
||||||
// Context Menu
|
// Context Menu
|
||||||
const [isPopoverOpen, setPopover] = useState(false);
|
const [isPopoverOpen, setPopover] = useState(false);
|
||||||
|
@ -70,7 +84,6 @@ export const ReportingExampleApp = ({
|
||||||
return {
|
return {
|
||||||
layout: {
|
layout: {
|
||||||
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||||
selectors: getDefaultLayoutSelectors(),
|
|
||||||
},
|
},
|
||||||
relativeUrls: ['/app/reportingExample#/intended-visualization'],
|
relativeUrls: ['/app/reportingExample#/intended-visualization'],
|
||||||
objectType: 'develeloperExample',
|
objectType: 'develeloperExample',
|
||||||
|
@ -78,20 +91,65 @@ export const ReportingExampleApp = ({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPDFJobParamsDefaultV2 = (): JobParamsPDFV2 => {
|
||||||
|
return {
|
||||||
|
version: '8.0.0',
|
||||||
|
layout: {
|
||||||
|
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||||
|
},
|
||||||
|
locatorParams: [
|
||||||
|
{ id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } },
|
||||||
|
],
|
||||||
|
objectType: 'develeloperExample',
|
||||||
|
title: 'Reporting Developer Example',
|
||||||
|
browserTimezone: moment.tz.guess(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPNGJobParamsDefaultV2 = (): JobParamsPNGV2 => {
|
||||||
|
return {
|
||||||
|
version: '8.0.0',
|
||||||
|
layout: {
|
||||||
|
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||||
|
},
|
||||||
|
locatorParams: {
|
||||||
|
id: REPORTING_EXAMPLE_LOCATOR_ID,
|
||||||
|
version: '0.5.0',
|
||||||
|
params: { myTestState: {} },
|
||||||
|
},
|
||||||
|
objectType: 'develeloperExample',
|
||||||
|
title: 'Reporting Developer Example',
|
||||||
|
browserTimezone: moment.tz.guess(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const panels = [
|
const panels = [
|
||||||
{ id: 0, items: [{ name: 'PDF Reports', icon: 'document', panel: 1 }] },
|
{
|
||||||
|
id: 0,
|
||||||
|
items: [
|
||||||
|
{ name: 'PDF Reports', icon: 'document', panel: 1 },
|
||||||
|
{ name: 'PNG Reports', icon: 'document', panel: 7 },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
initialFocusedItemIndex: 1,
|
initialFocusedItemIndex: 1,
|
||||||
title: 'PDF Reports',
|
title: 'PDF Reports',
|
||||||
items: [
|
items: [
|
||||||
{ name: 'No Layout Option', icon: 'document', panel: 2 },
|
{ name: 'Default layout', icon: 'document', panel: 2 },
|
||||||
|
{ name: 'Default layout V2', icon: 'document', panel: 4 },
|
||||||
{ name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 },
|
{ name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
initialFocusedItemIndex: 0,
|
||||||
|
title: 'PNG Reports',
|
||||||
|
items: [{ name: 'Default layout V2', icon: 'document', panel: 5 }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'No Layout Option',
|
title: 'Default layout',
|
||||||
content: (
|
content: (
|
||||||
<reporting.components.ReportingPanelPDF
|
<reporting.components.ReportingPanelPDF
|
||||||
getJobParams={getPDFJobParamsDefault}
|
getJobParams={getPDFJobParamsDefault}
|
||||||
|
@ -110,6 +168,26 @@ export const ReportingExampleApp = ({
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Default layout V2',
|
||||||
|
content: (
|
||||||
|
<reporting.components.ReportingPanelPDFV2
|
||||||
|
getJobParams={getPDFJobParamsDefaultV2}
|
||||||
|
onClose={closePopover}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'Default layout V2',
|
||||||
|
content: (
|
||||||
|
<reporting.components.ReportingPanelPNGV2
|
||||||
|
getJobParams={getPNGJobParamsDefaultV2}
|
||||||
|
onClose={closePopover}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -124,9 +202,11 @@ export const ReportingExampleApp = ({
|
||||||
</EuiPageHeader>
|
</EuiPageHeader>
|
||||||
<EuiPageContent>
|
<EuiPageContent>
|
||||||
<EuiPageContentBody>
|
<EuiPageContentBody>
|
||||||
|
<EuiTitle>
|
||||||
|
<h2>Example of a Sharing menu using components from Reporting</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiSpacer />
|
||||||
<EuiText>
|
<EuiText>
|
||||||
<p>Example of a Sharing menu using components from Reporting</p>
|
|
||||||
|
|
||||||
<EuiPopover
|
<EuiPopover
|
||||||
id="contextMenuExample"
|
id="contextMenuExample"
|
||||||
button={<EuiButton onClick={onButtonClick}>Share</EuiButton>}
|
button={<EuiButton onClick={onButtonClick}>Share</EuiButton>}
|
||||||
|
@ -140,8 +220,29 @@ export const ReportingExampleApp = ({
|
||||||
|
|
||||||
<EuiHorizontalRule />
|
<EuiHorizontalRule />
|
||||||
|
|
||||||
<div data-shared-items-container data-shared-items-count="4">
|
<div data-shared-items-container data-shared-items-count="5">
|
||||||
<EuiFlexGroup gutterSize="l">
|
<EuiFlexGroup gutterSize="l">
|
||||||
|
<EuiFlexItem data-shared-item>
|
||||||
|
{forwardedParams ? (
|
||||||
|
<>
|
||||||
|
<EuiText>
|
||||||
|
<p>
|
||||||
|
<strong>Forwarded app state</strong>
|
||||||
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
<EuiCodeBlock>{JSON.stringify(forwardedParams)}</EuiCodeBlock>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EuiText>
|
||||||
|
<p>
|
||||||
|
<strong>No forwarded app state found</strong>
|
||||||
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
<EuiCodeBlock>{'{}'}</EuiCodeBlock>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</EuiFlexItem>
|
||||||
{logos.map((item, index) => (
|
{logos.map((item, index) => (
|
||||||
<EuiFlexItem key={index} data-shared-item>
|
<EuiFlexItem key={index} data-shared-item>
|
||||||
<EuiCard
|
<EuiCard
|
||||||
|
|
|
@ -12,11 +12,11 @@ import {
|
||||||
CoreStart,
|
CoreStart,
|
||||||
Plugin,
|
Plugin,
|
||||||
} from '../../../../src/core/public';
|
} from '../../../../src/core/public';
|
||||||
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
|
import { PLUGIN_ID, PLUGIN_NAME, ReportingExampleLocatorDefinition } from '../common';
|
||||||
import { SetupDeps, StartDeps } from './types';
|
import { SetupDeps, StartDeps, MyForwardableState } from './types';
|
||||||
|
|
||||||
export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
|
export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
|
||||||
public setup(core: CoreSetup, { developerExamples, screenshotMode }: SetupDeps): void {
|
public setup(core: CoreSetup, { developerExamples, screenshotMode, share }: SetupDeps): void {
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: PLUGIN_ID,
|
id: PLUGIN_ID,
|
||||||
title: PLUGIN_NAME,
|
title: PLUGIN_NAME,
|
||||||
|
@ -30,7 +30,12 @@ export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
|
||||||
unknown
|
unknown
|
||||||
];
|
];
|
||||||
// Render the application
|
// Render the application
|
||||||
return renderApp(coreStart, { ...depsStart, screenshotMode }, params);
|
return renderApp(
|
||||||
|
coreStart,
|
||||||
|
{ ...depsStart, screenshotMode, share },
|
||||||
|
params,
|
||||||
|
params.history.location.state as MyForwardableState
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,6 +45,8 @@ export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
|
||||||
title: 'Reporting integration',
|
title: 'Reporting integration',
|
||||||
description: 'Demonstrate how to put an Export button on a page and generate reports.',
|
description: 'Demonstrate how to put an Export button on a page and generate reports.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
share.url.locators.create(new ReportingExampleLocatorDefinition());
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {}
|
public start() {}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
|
import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
|
||||||
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public';
|
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public';
|
||||||
|
import { SharePluginSetup } from 'src/plugins/share/public';
|
||||||
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
|
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
|
||||||
import { ReportingStart } from '../../../plugins/reporting/public';
|
import { ReportingStart } from '../../../plugins/reporting/public';
|
||||||
|
|
||||||
|
@ -17,9 +18,12 @@ export interface PluginStart {}
|
||||||
|
|
||||||
export interface SetupDeps {
|
export interface SetupDeps {
|
||||||
developerExamples: DeveloperExamplesSetup;
|
developerExamples: DeveloperExamplesSetup;
|
||||||
|
share: SharePluginSetup;
|
||||||
screenshotMode: ScreenshotModePluginSetup;
|
screenshotMode: ScreenshotModePluginSetup;
|
||||||
}
|
}
|
||||||
export interface StartDeps {
|
export interface StartDeps {
|
||||||
navigation: NavigationPublicPluginStart;
|
navigation: NavigationPublicPluginStart;
|
||||||
reporting: ReportingStart;
|
reporting: ReportingStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MyForwardableState = Record<string, unknown>;
|
||||||
|
|
|
@ -61,10 +61,14 @@ export const CSV_REPORT_TYPE = 'CSV';
|
||||||
export const CSV_JOB_TYPE = 'csv_searchsource';
|
export const CSV_JOB_TYPE = 'csv_searchsource';
|
||||||
|
|
||||||
export const PDF_REPORT_TYPE = 'printablePdf';
|
export const PDF_REPORT_TYPE = 'printablePdf';
|
||||||
|
export const PDF_REPORT_TYPE_V2 = 'printablePdfV2';
|
||||||
export const PDF_JOB_TYPE = 'printable_pdf';
|
export const PDF_JOB_TYPE = 'printable_pdf';
|
||||||
|
export const PDF_JOB_TYPE_V2 = 'printable_pdf_v2';
|
||||||
|
|
||||||
export const PNG_REPORT_TYPE = 'PNG';
|
export const PNG_REPORT_TYPE = 'PNG';
|
||||||
|
export const PNG_REPORT_TYPE_V2 = 'pngV2';
|
||||||
export const PNG_JOB_TYPE = 'PNG';
|
export const PNG_JOB_TYPE = 'PNG';
|
||||||
|
export const PNG_JOB_TYPE_V2 = 'PNGV2';
|
||||||
|
|
||||||
export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate';
|
export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate';
|
||||||
|
|
||||||
|
@ -98,6 +102,20 @@ export const ILM_POLICY_NAME = 'kibana-reporting';
|
||||||
// Management UI route
|
// Management UI route
|
||||||
export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting';
|
export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting';
|
||||||
|
|
||||||
|
export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATOR_STORE_KEY__';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A way to get the client side route for the reporting redirect app.
|
||||||
|
*
|
||||||
|
* This route currently expects a job ID and a locator that to use from that job so that it can redirect to the
|
||||||
|
* correct page.
|
||||||
|
*
|
||||||
|
* TODO: Accommodate 'forceNow' value that some visualizations may rely on
|
||||||
|
*/
|
||||||
|
export const getRedirectAppPathHome = () => {
|
||||||
|
return '/app/management/insightsAndAlerting/reporting/r';
|
||||||
|
};
|
||||||
|
|
||||||
// Statuses
|
// Statuses
|
||||||
export enum JOB_STATUSES {
|
export enum JOB_STATUSES {
|
||||||
PENDING = 'pending',
|
PENDING = 'pending',
|
||||||
|
|
11
x-pack/plugins/reporting/common/job_utils.ts
Normal file
11
x-pack/plugins/reporting/common/job_utils.ts
Normal 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy
|
||||||
|
// export type entirely
|
||||||
|
export const isJobV2Params = ({ sharingData }: { sharingData: Record<string, unknown> }): boolean =>
|
||||||
|
Array.isArray(sharingData.locatorParams);
|
|
@ -5,6 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { SerializableRecord } from '@kbn/utility-types';
|
||||||
|
|
||||||
export interface PageSizeParams {
|
export interface PageSizeParams {
|
||||||
pageMarginTop: number;
|
pageMarginTop: number;
|
||||||
pageMarginBottom: number;
|
pageMarginBottom: number;
|
||||||
|
@ -64,6 +66,11 @@ export interface ReportSource {
|
||||||
created_by: string | false; // username or `false` if security is disabled. Used for ensuring users can only access the reports they've created.
|
created_by: string | false; // username or `false` if security is disabled. Used for ensuring users can only access the reports they've created.
|
||||||
payload: {
|
payload: {
|
||||||
headers: string; // encrypted headers
|
headers: string; // encrypted headers
|
||||||
|
/**
|
||||||
|
* PDF V2 reports will contain locators parameters (see {@link LocatorPublic}) that will be converted to {@link KibanaLocation}s when
|
||||||
|
* generating a report
|
||||||
|
*/
|
||||||
|
locatorParams?: LocatorParams[];
|
||||||
isDeprecated?: boolean; // set to true when the export type is being phased out
|
isDeprecated?: boolean; // set to true when the export type is being phased out
|
||||||
} & BaseParams;
|
} & BaseParams;
|
||||||
meta: { objectType: string; layout?: string }; // for telemetry
|
meta: { objectType: string; layout?: string }; // for telemetry
|
||||||
|
@ -169,8 +176,21 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink;
|
||||||
type ManagementLink = string;
|
type ManagementLink = string;
|
||||||
export type ManagementLinkFn = () => ManagementLink;
|
export type ManagementLinkFn = () => ManagementLink;
|
||||||
|
|
||||||
|
export interface LocatorParams<
|
||||||
|
P extends SerializableRecord = SerializableRecord & { forceNow?: string }
|
||||||
|
> {
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
params: P;
|
||||||
|
}
|
||||||
|
|
||||||
export type IlmPolicyMigrationStatus = 'policy-not-found' | 'indices-not-managed-by-policy' | 'ok';
|
export type IlmPolicyMigrationStatus = 'policy-not-found' | 'indices-not-managed-by-policy' | 'ok';
|
||||||
|
|
||||||
export interface IlmPolicyStatusResponse {
|
export interface IlmPolicyStatusResponse {
|
||||||
status: IlmPolicyMigrationStatus;
|
status: IlmPolicyMigrationStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Url = string;
|
||||||
|
type UrlLocatorTuple = [url: Url, locatorParams: LocatorParams];
|
||||||
|
|
||||||
|
export type UrlOrUrlLocatorTuple = Url | UrlLocatorTuple;
|
||||||
|
|
8
x-pack/plugins/reporting/public/constants.ts
Normal file
8
x-pack/plugins/reporting/public/constants.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* 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 const REACT_ROUTER_REDIRECT_APP_PATH = '/r';
|
|
@ -19,7 +19,7 @@ interface ContextValue {
|
||||||
|
|
||||||
const InternalApiClientContext = createContext<undefined | ContextValue>(undefined);
|
const InternalApiClientContext = createContext<undefined | ContextValue>(undefined);
|
||||||
|
|
||||||
export const InternalApiClientClientProvider: FunctionComponent<{
|
export const InternalApiClientProvider: FunctionComponent<{
|
||||||
apiClient: ReportingAPIClient;
|
apiClient: ReportingAPIClient;
|
||||||
}> = ({ apiClient, children }) => {
|
}> = ({ apiClient, children }) => {
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -9,4 +9,4 @@ export * from './reporting_api_client';
|
||||||
|
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
|
||||||
export { InternalApiClientClientProvider, useInternalApiClient } from './context';
|
export { InternalApiClientProvider, useInternalApiClient } from './context';
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { CoreSetup, CoreStart } from 'src/core/public';
|
import { CoreSetup, CoreStart } from 'src/core/public';
|
||||||
import { ILicense } from '../../../licensing/public';
|
import { ILicense } from '../../../licensing/public';
|
||||||
import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client';
|
import { ReportingAPIClient, InternalApiClientProvider } from '../lib/reporting_api_client';
|
||||||
import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context';
|
import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context';
|
||||||
import { ClientConfigType } from '../plugin';
|
import { ClientConfigType } from '../plugin';
|
||||||
import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports';
|
import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports';
|
||||||
|
@ -32,7 +32,7 @@ export async function mountManagementSection(
|
||||||
<KibanaContextProvider
|
<KibanaContextProvider
|
||||||
services={{ http: coreSetup.http, application: coreStart.application }}
|
services={{ http: coreSetup.http, application: coreStart.application }}
|
||||||
>
|
>
|
||||||
<InternalApiClientClientProvider apiClient={apiClient}>
|
<InternalApiClientProvider apiClient={apiClient}>
|
||||||
<IlmPolicyStatusContextProvider>
|
<IlmPolicyStatusContextProvider>
|
||||||
<ReportListing
|
<ReportListing
|
||||||
toasts={coreSetup.notifications.toasts}
|
toasts={coreSetup.notifications.toasts}
|
||||||
|
@ -43,7 +43,7 @@ export async function mountManagementSection(
|
||||||
urlService={urlService}
|
urlService={urlService}
|
||||||
/>
|
/>
|
||||||
</IlmPolicyStatusContextProvider>
|
</IlmPolicyStatusContextProvider>
|
||||||
</InternalApiClientClientProvider>
|
</InternalApiClientProvider>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
</I18nProvider>,
|
</I18nProvider>,
|
||||||
params.element
|
params.element
|
||||||
|
|
|
@ -21,7 +21,7 @@ import type { ILicense } from '../../../licensing/public';
|
||||||
import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types';
|
import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types';
|
||||||
import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context';
|
import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context';
|
||||||
import { Job } from '../lib/job';
|
import { Job } from '../lib/job';
|
||||||
import { InternalApiClientClientProvider, ReportingAPIClient } from '../lib/reporting_api_client';
|
import { InternalApiClientProvider, ReportingAPIClient } from '../lib/reporting_api_client';
|
||||||
import { KibanaContextProvider } from '../shared_imports';
|
import { KibanaContextProvider } from '../shared_imports';
|
||||||
import { ListingProps as Props, ReportListing } from '.';
|
import { ListingProps as Props, ReportListing } from '.';
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ describe('ReportListing', () => {
|
||||||
const createTestBed = registerTestBed(
|
const createTestBed = registerTestBed(
|
||||||
(props?: Partial<Props>) => (
|
(props?: Partial<Props>) => (
|
||||||
<KibanaContextProvider services={{ http: httpService, application: applicationService }}>
|
<KibanaContextProvider services={{ http: httpService, application: applicationService }}>
|
||||||
<InternalApiClientClientProvider apiClient={reportingAPIClient as ReportingAPIClient}>
|
<InternalApiClientProvider apiClient={reportingAPIClient as ReportingAPIClient}>
|
||||||
<IlmPolicyStatusContextProvider>
|
<IlmPolicyStatusContextProvider>
|
||||||
<ReportListing
|
<ReportListing
|
||||||
license$={license$}
|
license$={license$}
|
||||||
|
@ -96,7 +96,7 @@ describe('ReportListing', () => {
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</IlmPolicyStatusContextProvider>
|
</IlmPolicyStatusContextProvider>
|
||||||
</InternalApiClientClientProvider>
|
</InternalApiClientProvider>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
),
|
),
|
||||||
{ memoryRouter: { wrapComponent: false } }
|
{ memoryRouter: { wrapComponent: false } }
|
||||||
|
|
|
@ -42,6 +42,7 @@ import type {
|
||||||
} from './shared_imports';
|
} from './shared_imports';
|
||||||
import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
|
import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
|
||||||
import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
|
import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
|
||||||
|
import { isRedirectAppPath } from './utils';
|
||||||
|
|
||||||
export interface ClientConfigType {
|
export interface ClientConfigType {
|
||||||
poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } };
|
poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } };
|
||||||
|
@ -167,6 +168,15 @@ export class ReportingPublicPlugin
|
||||||
title: this.title,
|
title: this.title,
|
||||||
order: 1,
|
order: 1,
|
||||||
mount: async (params) => {
|
mount: async (params) => {
|
||||||
|
// The redirect app will be mounted if reporting is opened on a specific path. The redirect app expects a
|
||||||
|
// specific environment to be present so that it can navigate to a specific application. This is used by
|
||||||
|
// report generation to navigate to the correct place with full app state.
|
||||||
|
if (isRedirectAppPath(params.history.location.pathname)) {
|
||||||
|
const { mountRedirectApp } = await import('./redirect');
|
||||||
|
return mountRedirectApp({ ...params, share, apiClient });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise load the reporting management UI.
|
||||||
params.setBreadcrumbs([{ text: this.breadcrumbText }]);
|
params.setBreadcrumbs([{ text: this.breadcrumbText }]);
|
||||||
const [[start], { mountManagementSection }] = await Promise.all([
|
const [[start], { mountManagementSection }] = await Promise.all([
|
||||||
getStartServices(),
|
getStartServices(),
|
||||||
|
|
8
x-pack/plugins/reporting/public/redirect/index.ts
Normal file
8
x-pack/plugins/reporting/public/redirect/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* 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 { mountRedirectApp } from './mount_redirect_app';
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render, unmountComponentAtNode } from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import { EuiErrorBoundary } from '@elastic/eui';
|
||||||
|
|
||||||
|
import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports';
|
||||||
|
import type { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||||
|
|
||||||
|
import { RedirectApp } from './redirect_app';
|
||||||
|
|
||||||
|
interface MountParams extends ManagementAppMountParams {
|
||||||
|
apiClient: ReportingAPIClient;
|
||||||
|
share: SharePluginSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mountRedirectApp = ({ element, apiClient, history, share }: MountParams) => {
|
||||||
|
render(
|
||||||
|
<EuiErrorBoundary>
|
||||||
|
<RedirectApp apiClient={apiClient} history={history} share={share} />
|
||||||
|
</EuiErrorBoundary>,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unmountComponentAtNode(element);
|
||||||
|
};
|
||||||
|
};
|
74
x-pack/plugins/reporting/public/redirect/redirect_app.tsx
Normal file
74
x-pack/plugins/reporting/public/redirect/redirect_app.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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, { useEffect, useState } from 'react';
|
||||||
|
import type { FunctionComponent } from 'react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { EuiTitle, EuiCallOut, EuiCodeBlock } from '@elastic/eui';
|
||||||
|
|
||||||
|
import type { ScopedHistory } from 'src/core/public';
|
||||||
|
|
||||||
|
import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../common/constants';
|
||||||
|
import { LocatorParams } from '../../common/types';
|
||||||
|
|
||||||
|
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||||
|
import type { SharePluginSetup } from '../shared_imports';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
apiClient: ReportingAPIClient;
|
||||||
|
history: ScopedHistory;
|
||||||
|
share: SharePluginSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18nTexts = {
|
||||||
|
errorTitle: i18n.translate('xpack.reporting.redirectApp.errorTitle', {
|
||||||
|
defaultMessage: 'Redirect error',
|
||||||
|
}),
|
||||||
|
redirectingTitle: i18n.translate('xpack.reporting.redirectApp.redirectingMessage', {
|
||||||
|
defaultMessage: 'Redirecting...',
|
||||||
|
}),
|
||||||
|
consoleMessagePrefix: i18n.translate(
|
||||||
|
'xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Redirect page error:',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RedirectApp: FunctionComponent<Props> = ({ share }) => {
|
||||||
|
const [error, setError] = useState<undefined | Error>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const locatorParams = ((window as unknown) as Record<string, LocatorParams>)[
|
||||||
|
REPORTING_REDIRECT_LOCATOR_STORE_KEY
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!locatorParams) {
|
||||||
|
throw new Error('Could not find locator for report');
|
||||||
|
}
|
||||||
|
|
||||||
|
share.navigate(locatorParams);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(i18nTexts.consoleMessagePrefix, e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, [share]);
|
||||||
|
|
||||||
|
return error ? (
|
||||||
|
<EuiCallOut title={i18nTexts.errorTitle} color="danger">
|
||||||
|
<p>{error.message}</p>
|
||||||
|
{error.stack && <EuiCodeBlock>{error.stack}</EuiCodeBlock>}
|
||||||
|
</EuiCallOut>
|
||||||
|
) : (
|
||||||
|
<EuiTitle>
|
||||||
|
<h1>{i18nTexts.redirectingTitle}</h1>
|
||||||
|
</EuiTitle>
|
||||||
|
);
|
||||||
|
};
|
|
@ -24,6 +24,7 @@ export interface ExportPanelShareOpts {
|
||||||
export interface ReportingSharingData {
|
export interface ReportingSharingData {
|
||||||
title: string;
|
title: string;
|
||||||
layout: LayoutParams;
|
layout: LayoutParams;
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JobParamsProviderOptions {
|
export interface JobParamsProviderOptions {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ShareContext } from 'src/plugins/share/public';
|
import { ShareContext } from 'src/plugins/share/public';
|
||||||
import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
|
import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
|
||||||
|
import { isJobV2Params } from '../../common/job_utils';
|
||||||
import { checkLicense } from '../lib/license_check';
|
import { checkLicense } from '../lib/license_check';
|
||||||
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||||
import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
|
import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
|
||||||
|
@ -16,11 +17,11 @@ import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
|
||||||
const getJobParams = (
|
const getJobParams = (
|
||||||
apiClient: ReportingAPIClient,
|
apiClient: ReportingAPIClient,
|
||||||
opts: JobParamsProviderOptions,
|
opts: JobParamsProviderOptions,
|
||||||
type: 'pdf' | 'png'
|
type: 'png' | 'pngV2' | 'printablePdf' | 'printablePdfV2'
|
||||||
) => () => {
|
) => () => {
|
||||||
const {
|
const {
|
||||||
objectType,
|
objectType,
|
||||||
sharingData: { title, layout },
|
sharingData: { title, layout, locatorParams },
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const baseParams = {
|
const baseParams = {
|
||||||
|
@ -29,6 +30,14 @@ const getJobParams = (
|
||||||
title,
|
title,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (type === 'printablePdfV2') {
|
||||||
|
// multi locator for PDF V2
|
||||||
|
return { ...baseParams, locatorParams: [locatorParams] };
|
||||||
|
} else if (type === 'pngV2') {
|
||||||
|
// single locator for PNG V2
|
||||||
|
return { ...baseParams, locatorParams };
|
||||||
|
}
|
||||||
|
|
||||||
// Relative URL must have URL prefix (Spaces ID prefix), but not server basePath
|
// Relative URL must have URL prefix (Spaces ID prefix), but not server basePath
|
||||||
// Replace hashes with original RISON values.
|
// Replace hashes with original RISON values.
|
||||||
const relativeUrl = opts.shareableUrl.replace(
|
const relativeUrl = opts.shareableUrl.replace(
|
||||||
|
@ -36,7 +45,7 @@ const getJobParams = (
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type === 'pdf') {
|
if (type === 'printablePdf') {
|
||||||
// multi URL for PDF
|
// multi URL for PDF
|
||||||
return { ...baseParams, relativeUrls: [relativeUrl] };
|
return { ...baseParams, relativeUrls: [relativeUrl] };
|
||||||
}
|
}
|
||||||
|
@ -111,6 +120,16 @@ export const reportingScreenshotShareProvider = ({
|
||||||
defaultMessage: 'PNG Reports',
|
defaultMessage: 'PNG Reports',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const jobProviderOptions: JobParamsProviderOptions = {
|
||||||
|
shareableUrl,
|
||||||
|
objectType,
|
||||||
|
sharingData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isV2Job = isJobV2Params(jobProviderOptions);
|
||||||
|
|
||||||
|
const pngReportType = isV2Job ? 'pngV2' : 'png';
|
||||||
|
|
||||||
const panelPng = {
|
const panelPng = {
|
||||||
shareMenuItem: {
|
shareMenuItem: {
|
||||||
name: pngPanelTitle,
|
name: pngPanelTitle,
|
||||||
|
@ -128,10 +147,10 @@ export const reportingScreenshotShareProvider = ({
|
||||||
apiClient={apiClient}
|
apiClient={apiClient}
|
||||||
toasts={toasts}
|
toasts={toasts}
|
||||||
uiSettings={uiSettings}
|
uiSettings={uiSettings}
|
||||||
reportType="png"
|
reportType={pngReportType}
|
||||||
objectId={objectId}
|
objectId={objectId}
|
||||||
requiresSavedState={true}
|
requiresSavedState={true}
|
||||||
getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'png')}
|
getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)}
|
||||||
isDirty={isDirty}
|
isDirty={isDirty}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -143,6 +162,8 @@ export const reportingScreenshotShareProvider = ({
|
||||||
defaultMessage: 'PDF Reports',
|
defaultMessage: 'PDF Reports',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pdfReportType = isV2Job ? 'printablePdfV2' : 'printablePdf';
|
||||||
|
|
||||||
const panelPdf = {
|
const panelPdf = {
|
||||||
shareMenuItem: {
|
shareMenuItem: {
|
||||||
name: pdfPanelTitle,
|
name: pdfPanelTitle,
|
||||||
|
@ -160,11 +181,11 @@ export const reportingScreenshotShareProvider = ({
|
||||||
apiClient={apiClient}
|
apiClient={apiClient}
|
||||||
toasts={toasts}
|
toasts={toasts}
|
||||||
uiSettings={uiSettings}
|
uiSettings={uiSettings}
|
||||||
reportType="printablePdf"
|
reportType={pdfReportType}
|
||||||
objectId={objectId}
|
objectId={objectId}
|
||||||
requiresSavedState={true}
|
requiresSavedState={true}
|
||||||
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
|
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
|
||||||
getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'pdf')}
|
getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)}
|
||||||
isDirty={isDirty}
|
isDirty={isDirty}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,7 +21,13 @@ import React, { Component, ReactElement } from 'react';
|
||||||
import { ToastsSetup, IUiSettingsClient } from 'src/core/public';
|
import { ToastsSetup, IUiSettingsClient } from 'src/core/public';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
|
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
|
||||||
import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants';
|
import {
|
||||||
|
CSV_REPORT_TYPE,
|
||||||
|
PDF_REPORT_TYPE,
|
||||||
|
PDF_REPORT_TYPE_V2,
|
||||||
|
PNG_REPORT_TYPE,
|
||||||
|
PNG_REPORT_TYPE_V2,
|
||||||
|
} from '../../common/constants';
|
||||||
import { BaseParams } from '../../common/types';
|
import { BaseParams } from '../../common/types';
|
||||||
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||||
|
|
||||||
|
@ -200,10 +206,12 @@ class ReportingPanelContentUi extends Component<Props, State> {
|
||||||
private prettyPrintReportingType = () => {
|
private prettyPrintReportingType = () => {
|
||||||
switch (this.props.reportType) {
|
switch (this.props.reportType) {
|
||||||
case PDF_REPORT_TYPE:
|
case PDF_REPORT_TYPE:
|
||||||
|
case PDF_REPORT_TYPE_V2:
|
||||||
return 'PDF';
|
return 'PDF';
|
||||||
case 'csv_searchsource':
|
case 'csv_searchsource':
|
||||||
return CSV_REPORT_TYPE;
|
return CSV_REPORT_TYPE;
|
||||||
case 'png':
|
case 'png':
|
||||||
|
case PNG_REPORT_TYPE_V2:
|
||||||
return PNG_REPORT_TYPE;
|
return PNG_REPORT_TYPE;
|
||||||
default:
|
default:
|
||||||
return this.props.reportType;
|
return this.props.reportType;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import { CoreSetup } from 'kibana/public';
|
import { CoreSetup } from 'kibana/public';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReportingAPIClient } from '../';
|
import { ReportingAPIClient } from '../';
|
||||||
import { PDF_REPORT_TYPE } from '../../common/constants';
|
import { PDF_REPORT_TYPE, PDF_REPORT_TYPE_V2, PNG_REPORT_TYPE_V2 } from '../../common/constants';
|
||||||
import type { Props as PanelPropsScreenCapture } from '../share_context_menu/screen_capture_panel_content';
|
import type { Props as PanelPropsScreenCapture } from '../share_context_menu/screen_capture_panel_content';
|
||||||
import { ScreenCapturePanelContent } from '../share_context_menu/screen_capture_panel_content_lazy';
|
import { ScreenCapturePanelContent } from '../share_context_menu/screen_capture_panel_content_lazy';
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ interface IncludeOnCloseFn {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropsPDF = Pick<PanelPropsScreenCapture, 'getJobParams' | 'layoutOption'> & IncludeOnCloseFn;
|
type Props = Pick<PanelPropsScreenCapture, 'getJobParams' | 'layoutOption'> & IncludeOnCloseFn;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* As of 7.14, the only shared component is a PDF report that is suited for Canvas integration.
|
* As of 7.14, the only shared component is a PDF report that is suited for Canvas integration.
|
||||||
|
@ -25,7 +25,7 @@ type PropsPDF = Pick<PanelPropsScreenCapture, 'getJobParams' | 'layoutOption'> &
|
||||||
*/
|
*/
|
||||||
export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) {
|
export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) {
|
||||||
return {
|
return {
|
||||||
ReportingPanelPDF(props: PropsPDF) {
|
ReportingPanelPDF(props: Props) {
|
||||||
return (
|
return (
|
||||||
<ScreenCapturePanelContent
|
<ScreenCapturePanelContent
|
||||||
layoutOption={props.layoutOption}
|
layoutOption={props.layoutOption}
|
||||||
|
@ -38,5 +38,31 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
ReportingPanelPDFV2(props: Props) {
|
||||||
|
return (
|
||||||
|
<ScreenCapturePanelContent
|
||||||
|
layoutOption={props.layoutOption}
|
||||||
|
requiresSavedState={false}
|
||||||
|
reportType={PDF_REPORT_TYPE_V2}
|
||||||
|
apiClient={apiClient}
|
||||||
|
toasts={core.notifications.toasts}
|
||||||
|
uiSettings={core.uiSettings}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ReportingPanelPNGV2(props: Props) {
|
||||||
|
return (
|
||||||
|
<ScreenCapturePanelContent
|
||||||
|
layoutOption={props.layoutOption}
|
||||||
|
requiresSavedState={false}
|
||||||
|
reportType={PNG_REPORT_TYPE_V2}
|
||||||
|
apiClient={apiClient}
|
||||||
|
toasts={core.notifications.toasts}
|
||||||
|
uiSettings={core.uiSettings}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
12
x-pack/plugins/reporting/public/utils.ts
Normal file
12
x-pack/plugins/reporting/public/utils.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 { REACT_ROUTER_REDIRECT_APP_PATH } from './constants';
|
||||||
|
|
||||||
|
export const isRedirectAppPath = (pathname: string) => {
|
||||||
|
return pathname.startsWith(REACT_ROUTER_REDIRECT_APP_PATH);
|
||||||
|
};
|
|
@ -10,6 +10,8 @@ import { map, truncate } from 'lodash';
|
||||||
import open from 'opn';
|
import open from 'opn';
|
||||||
import puppeteer, { ElementHandle, EvaluateFn, SerializableOrJSHandle } from 'puppeteer';
|
import puppeteer, { ElementHandle, EvaluateFn, SerializableOrJSHandle } from 'puppeteer';
|
||||||
import { parse as parseUrl } from 'url';
|
import { parse as parseUrl } from 'url';
|
||||||
|
import type { LocatorParams } from '../../../../common/types';
|
||||||
|
import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../../../common/constants';
|
||||||
import { getDisallowedOutgoingUrlError } from '../';
|
import { getDisallowedOutgoingUrlError } from '../';
|
||||||
import { ReportingCore } from '../../..';
|
import { ReportingCore } from '../../..';
|
||||||
import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server';
|
import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server';
|
||||||
|
@ -94,10 +96,12 @@ export class HeadlessChromiumDriver {
|
||||||
conditionalHeaders,
|
conditionalHeaders,
|
||||||
waitForSelector: pageLoadSelector,
|
waitForSelector: pageLoadSelector,
|
||||||
timeout,
|
timeout,
|
||||||
|
locator,
|
||||||
}: {
|
}: {
|
||||||
conditionalHeaders: ConditionalHeaders;
|
conditionalHeaders: ConditionalHeaders;
|
||||||
waitForSelector: string;
|
waitForSelector: string;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
locator?: LocatorParams;
|
||||||
},
|
},
|
||||||
logger: LevelLogger
|
logger: LevelLogger
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -106,8 +110,27 @@ export class HeadlessChromiumDriver {
|
||||||
// Reset intercepted request count
|
// Reset intercepted request count
|
||||||
this.interceptedCount = 0;
|
this.interceptedCount = 0;
|
||||||
|
|
||||||
const enableScreenshotMode = this.core.getEnableScreenshotMode();
|
/**
|
||||||
await this.page.evaluateOnNewDocument(enableScreenshotMode);
|
* Integrate with the screenshot mode plugin contract by calling this function before any other
|
||||||
|
* scripts have run on the browser page.
|
||||||
|
*/
|
||||||
|
await this.page.evaluateOnNewDocument(this.core.getEnableScreenshotMode());
|
||||||
|
|
||||||
|
if (locator) {
|
||||||
|
await this.page.evaluateOnNewDocument(
|
||||||
|
(key: string, value: unknown) => {
|
||||||
|
Object.defineProperty(window, key, {
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
enumerable: true,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
REPORTING_REDIRECT_LOCATOR_STORE_KEY,
|
||||||
|
locator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.page.setRequestInterception(true);
|
await this.page.setRequestInterception(true);
|
||||||
|
|
||||||
this.registerListeners(conditionalHeaders, logger);
|
this.registerListeners(conditionalHeaders, logger);
|
||||||
|
|
|
@ -8,11 +8,12 @@
|
||||||
import apm from 'elastic-apm-node';
|
import apm from 'elastic-apm-node';
|
||||||
import * as Rx from 'rxjs';
|
import * as Rx from 'rxjs';
|
||||||
import { finalize, map, tap } from 'rxjs/operators';
|
import { finalize, map, tap } from 'rxjs/operators';
|
||||||
import { ReportingCore } from '../../../';
|
import { ReportingCore } from '../../';
|
||||||
import { LevelLogger } from '../../../lib';
|
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||||
import { LayoutParams, PreserveLayout } from '../../../lib/layouts';
|
import { LevelLogger } from '../../lib';
|
||||||
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
import { LayoutParams, PreserveLayout } from '../../lib/layouts';
|
||||||
import { ConditionalHeaders } from '../../common';
|
import { getScreenshots$, ScreenshotResults } from '../../lib/screenshots';
|
||||||
|
import { ConditionalHeaders } from '../common';
|
||||||
|
|
||||||
function getBase64DecodedSize(value: string) {
|
function getBase64DecodedSize(value: string) {
|
||||||
// @see https://en.wikipedia.org/wiki/Base64#Output_padding
|
// @see https://en.wikipedia.org/wiki/Base64#Output_padding
|
||||||
|
@ -30,7 +31,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
||||||
|
|
||||||
return function generatePngObservable(
|
return function generatePngObservable(
|
||||||
logger: LevelLogger,
|
logger: LevelLogger,
|
||||||
url: string,
|
urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple,
|
||||||
browserTimezone: string | undefined,
|
browserTimezone: string | undefined,
|
||||||
conditionalHeaders: ConditionalHeaders,
|
conditionalHeaders: ConditionalHeaders,
|
||||||
layoutParams: LayoutParams
|
layoutParams: LayoutParams
|
||||||
|
@ -47,7 +48,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
||||||
let apmBuffer: typeof apm.currentSpan;
|
let apmBuffer: typeof apm.currentSpan;
|
||||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls: [url],
|
urlsOrUrlLocatorTuples: [urlOrUrlLocatorTuple],
|
||||||
conditionalHeaders,
|
conditionalHeaders,
|
||||||
layout,
|
layout,
|
||||||
browserTimezone,
|
browserTimezone,
|
|
@ -5,14 +5,14 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReportingCore } from '../../../';
|
import { ReportingCore } from '../..';
|
||||||
import {
|
import {
|
||||||
createMockConfig,
|
createMockConfig,
|
||||||
createMockConfigSchema,
|
createMockConfigSchema,
|
||||||
createMockLevelLogger,
|
createMockLevelLogger,
|
||||||
createMockReportingCore,
|
createMockReportingCore,
|
||||||
} from '../../../test_helpers';
|
} from '../../test_helpers';
|
||||||
import { getConditionalHeaders } from '../../common';
|
import { getConditionalHeaders } from '.';
|
||||||
import { getCustomLogo } from './get_custom_logo';
|
import { getCustomLogo } from './get_custom_logo';
|
||||||
|
|
||||||
let mockReportingPlugin: ReportingCore;
|
let mockReportingPlugin: ReportingCore;
|
|
@ -5,10 +5,10 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReportingCore } from '../../../';
|
import { ReportingCore } from '../../';
|
||||||
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants';
|
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants';
|
||||||
import { LevelLogger } from '../../../lib';
|
import { LevelLogger } from '../../lib';
|
||||||
import { ConditionalHeaders } from '../../common';
|
import { ConditionalHeaders } from '../common';
|
||||||
|
|
||||||
export const getCustomLogo = async (
|
export const getCustomLogo = async (
|
||||||
reporting: ReportingCore,
|
reporting: ReportingCore,
|
|
@ -10,6 +10,9 @@ export { getConditionalHeaders } from './get_conditional_headers';
|
||||||
export { getFullUrls } from './get_full_urls';
|
export { getFullUrls } from './get_full_urls';
|
||||||
export { omitBlockedHeaders } from './omit_blocked_headers';
|
export { omitBlockedHeaders } from './omit_blocked_headers';
|
||||||
export { validateUrls } from './validate_urls';
|
export { validateUrls } from './validate_urls';
|
||||||
|
export { generatePngObservableFactory } from './generate_png';
|
||||||
|
export { getCustomLogo } from './get_custom_logo';
|
||||||
|
export { setForceNow } from './set_force_now';
|
||||||
|
|
||||||
export interface TimeRangeParams {
|
export interface TimeRangeParams {
|
||||||
min?: Date | string | number | null;
|
min?: Date | string | number | null;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
StyleDictionary,
|
StyleDictionary,
|
||||||
TDocumentDefinitions,
|
TDocumentDefinitions,
|
||||||
} from 'pdfmake/interfaces';
|
} from 'pdfmake/interfaces';
|
||||||
import { LayoutInstance } from '../../../../lib/layouts';
|
import { LayoutInstance } from '../../../lib/layouts';
|
||||||
import { REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
import { REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||||
import { getFont } from './get_font';
|
import { getFont } from './get_font';
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PreserveLayout, PrintLayout } from '../../../../lib/layouts';
|
import { PreserveLayout, PrintLayout } from '../../../lib/layouts';
|
||||||
import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers';
|
import { createMockConfig, createMockConfigSchema } from '../../../test_helpers';
|
||||||
import { PdfMaker } from './';
|
import { PdfMaker } from './';
|
||||||
|
|
||||||
const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`;
|
const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`;
|
|
@ -12,12 +12,12 @@ import _ from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Printer from 'pdfmake';
|
import Printer from 'pdfmake';
|
||||||
import { Content, ContentImage, ContentText } from 'pdfmake/interfaces';
|
import { Content, ContentImage, ContentText } from 'pdfmake/interfaces';
|
||||||
import { LayoutInstance } from '../../../../lib/layouts';
|
import { LayoutInstance } from '../../../lib/layouts';
|
||||||
import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||||
import { getFont } from './get_font';
|
import { getFont } from './get_font';
|
||||||
import { getTemplate } from './get_template';
|
import { getTemplate } from './get_template';
|
||||||
|
|
||||||
const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets');
|
const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets');
|
||||||
const tableBorderWidth = 1;
|
const tableBorderWidth = 1;
|
||||||
|
|
||||||
export class PdfMaker {
|
export class PdfMaker {
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 { LocatorParams } from '../../../common/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add `forceNow` to {@link LocatorParams['params']} to enable clients to set the time appropriately when
|
||||||
|
* reporting navigates to the page in Chromium.
|
||||||
|
*/
|
||||||
|
export const setForceNow = (forceNow: string) => (locator: LocatorParams): LocatorParams => {
|
||||||
|
return {
|
||||||
|
...locator,
|
||||||
|
params: {
|
||||||
|
...locator.params,
|
||||||
|
forceNow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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 { parse as urlParse, UrlWithStringQuery } from 'url';
|
||||||
|
import { ReportingConfig } from '../../../';
|
||||||
|
import { getAbsoluteUrlFactory } from '../get_absolute_url';
|
||||||
|
import { validateUrls } from '../validate_urls';
|
||||||
|
|
||||||
|
export function getFullUrls(config: ReportingConfig, relativeUrls: string[]) {
|
||||||
|
const [basePath, protocol, hostname, port] = [
|
||||||
|
config.kbnConfig.get('server', 'basePath'),
|
||||||
|
config.get('kibanaServer', 'protocol'),
|
||||||
|
config.get('kibanaServer', 'hostname'),
|
||||||
|
config.get('kibanaServer', 'port'),
|
||||||
|
] as string[];
|
||||||
|
const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port });
|
||||||
|
|
||||||
|
validateUrls(relativeUrls);
|
||||||
|
|
||||||
|
const urls = relativeUrls.map((relativeUrl) => {
|
||||||
|
const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl);
|
||||||
|
return getAbsoluteUrl({
|
||||||
|
path: parsedRelative.pathname === null ? undefined : parsedRelative.pathname,
|
||||||
|
hash: parsedRelative.hash === null ? undefined : parsedRelative.hash,
|
||||||
|
search: parsedRelative.search === null ? undefined : parsedRelative.search,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
|
@ -15,11 +15,11 @@ import {
|
||||||
createMockConfigSchema,
|
createMockConfigSchema,
|
||||||
createMockReportingCore,
|
createMockReportingCore,
|
||||||
} from '../../../test_helpers';
|
} from '../../../test_helpers';
|
||||||
import { generatePngObservableFactory } from '../lib/generate_png';
|
import { generatePngObservableFactory } from '../../common';
|
||||||
import { TaskPayloadPNG } from '../types';
|
import { TaskPayloadPNG } from '../types';
|
||||||
import { runTaskFnFactory } from './';
|
import { runTaskFnFactory } from './';
|
||||||
|
|
||||||
jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
jest.mock('../../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
let mockReporting: ReportingCore;
|
let mockReporting: ReportingCore;
|
||||||
|
|
|
@ -16,8 +16,8 @@ import {
|
||||||
getConditionalHeaders,
|
getConditionalHeaders,
|
||||||
getFullUrls,
|
getFullUrls,
|
||||||
omitBlockedHeaders,
|
omitBlockedHeaders,
|
||||||
|
generatePngObservableFactory,
|
||||||
} from '../../common';
|
} from '../../common';
|
||||||
import { generatePngObservableFactory } from '../lib/generate_png';
|
|
||||||
import { TaskPayloadPNG } from '../types';
|
import { TaskPayloadPNG } from '../types';
|
||||||
|
|
||||||
export const runTaskFnFactory: RunTaskFnFactory<
|
export const runTaskFnFactory: RunTaskFnFactory<
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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 { cryptoFactory } from '../../lib';
|
||||||
|
import { CreateJobFn, CreateJobFnFactory } from '../../types';
|
||||||
|
import { JobParamsPNGV2, TaskPayloadPNGV2 } from './types';
|
||||||
|
|
||||||
|
export const createJobFnFactory: CreateJobFnFactory<
|
||||||
|
CreateJobFn<JobParamsPNGV2, TaskPayloadPNGV2>
|
||||||
|
> = function createJobFactoryFn(reporting, logger) {
|
||||||
|
const config = reporting.getConfig();
|
||||||
|
const crypto = cryptoFactory(config.get('encryptionKey'));
|
||||||
|
|
||||||
|
return async function createJob({ locatorParams, ...jobParams }, context, req) {
|
||||||
|
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...jobParams,
|
||||||
|
headers: serializedEncryptedHeaders,
|
||||||
|
spaceId: reporting.getSpaceId(req, logger),
|
||||||
|
locatorParams: [locatorParams],
|
||||||
|
forceNow: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* 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 * as Rx from 'rxjs';
|
||||||
|
import { Writable } from 'stream';
|
||||||
|
import { ReportingCore } from '../../';
|
||||||
|
import { CancellationToken } from '../../../common';
|
||||||
|
import { LocatorParams } from '../../../common/types';
|
||||||
|
import { cryptoFactory, LevelLogger } from '../../lib';
|
||||||
|
import {
|
||||||
|
createMockConfig,
|
||||||
|
createMockConfigSchema,
|
||||||
|
createMockReportingCore,
|
||||||
|
} from '../../test_helpers';
|
||||||
|
import { generatePngObservableFactory } from '../common';
|
||||||
|
import { runTaskFnFactory } from './execute_job';
|
||||||
|
import { TaskPayloadPNGV2 } from './types';
|
||||||
|
|
||||||
|
jest.mock('../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
||||||
|
|
||||||
|
let content: string;
|
||||||
|
let mockReporting: ReportingCore;
|
||||||
|
let stream: jest.Mocked<Writable>;
|
||||||
|
|
||||||
|
const cancellationToken = ({
|
||||||
|
on: jest.fn(),
|
||||||
|
} as unknown) as CancellationToken;
|
||||||
|
|
||||||
|
const mockLoggerFactory = {
|
||||||
|
get: jest.fn().mockImplementation(() => ({
|
||||||
|
error: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const getMockLogger = () => new LevelLogger(mockLoggerFactory);
|
||||||
|
|
||||||
|
const mockEncryptionKey = 'abcabcsecuresecret';
|
||||||
|
const encryptHeaders = async (headers: Record<string, string>) => {
|
||||||
|
const crypto = cryptoFactory(mockEncryptionKey);
|
||||||
|
return await crypto.encrypt(headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBasePayload = (baseObj: unknown) => baseObj as TaskPayloadPNGV2;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
content = '';
|
||||||
|
stream = ({ write: jest.fn((chunk) => (content += chunk)) } as unknown) as typeof stream;
|
||||||
|
|
||||||
|
const mockReportingConfig = createMockConfigSchema({
|
||||||
|
index: '.reporting-2018.10.10',
|
||||||
|
encryptionKey: mockEncryptionKey,
|
||||||
|
queue: {
|
||||||
|
indexInterval: 'daily',
|
||||||
|
timeout: Infinity,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockReporting = await createMockReportingCore(mockReportingConfig);
|
||||||
|
mockReporting.setConfig(createMockConfig(mockReportingConfig));
|
||||||
|
|
||||||
|
(generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset());
|
||||||
|
|
||||||
|
test(`passes browserTimezone to generatePng`, async () => {
|
||||||
|
const encryptedHeaders = await encryptHeaders({});
|
||||||
|
const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock;
|
||||||
|
generatePngObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||||
|
|
||||||
|
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||||
|
const browserTimezone = 'UTC';
|
||||||
|
await runTask(
|
||||||
|
'pngJobId',
|
||||||
|
getBasePayload({
|
||||||
|
forceNow: 'test',
|
||||||
|
locatorParams: [{ version: 'test', id: 'test', params: {} }] as LocatorParams[],
|
||||||
|
browserTimezone,
|
||||||
|
headers: encryptedHeaders,
|
||||||
|
}),
|
||||||
|
cancellationToken,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
LevelLogger {
|
||||||
|
"_logger": Object {
|
||||||
|
"get": [MockFunction],
|
||||||
|
},
|
||||||
|
"_tags": Array [
|
||||||
|
"PNGV2",
|
||||||
|
"execute",
|
||||||
|
"pngJobId",
|
||||||
|
],
|
||||||
|
"warning": [Function],
|
||||||
|
},
|
||||||
|
Array [
|
||||||
|
"localhost:80undefined/app/management/insightsAndAlerting/reporting/r",
|
||||||
|
Object {
|
||||||
|
"id": "test",
|
||||||
|
"params": Object {
|
||||||
|
"forceNow": "test",
|
||||||
|
},
|
||||||
|
"version": "test",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"UTC",
|
||||||
|
Object {
|
||||||
|
"conditions": Object {
|
||||||
|
"basePath": undefined,
|
||||||
|
"hostname": "localhost",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": undefined,
|
||||||
|
},
|
||||||
|
"headers": Object {},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`returns content_type of application/png`, async () => {
|
||||||
|
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||||
|
const encryptedHeaders = await encryptHeaders({});
|
||||||
|
|
||||||
|
const generatePngObservable = await generatePngObservableFactory(mockReporting);
|
||||||
|
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of('foo'));
|
||||||
|
|
||||||
|
const { content_type: contentType } = await runTask(
|
||||||
|
'pngJobId',
|
||||||
|
getBasePayload({
|
||||||
|
locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[],
|
||||||
|
headers: encryptedHeaders,
|
||||||
|
}),
|
||||||
|
cancellationToken,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
expect(contentType).toBe('image/png');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`returns content of generatePng getBuffer base64 encoded`, async () => {
|
||||||
|
const testContent = 'raw string from get_screenhots';
|
||||||
|
const generatePngObservable = await generatePngObservableFactory(mockReporting);
|
||||||
|
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ base64: testContent }));
|
||||||
|
|
||||||
|
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||||
|
const encryptedHeaders = await encryptHeaders({});
|
||||||
|
await runTask(
|
||||||
|
'pngJobId',
|
||||||
|
getBasePayload({
|
||||||
|
locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[],
|
||||||
|
headers: encryptedHeaders,
|
||||||
|
}),
|
||||||
|
cancellationToken,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(content).toEqual(testContent);
|
||||||
|
});
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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 apm from 'elastic-apm-node';
|
||||||
|
import * as Rx from 'rxjs';
|
||||||
|
import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
|
||||||
|
import { PNG_JOB_TYPE_V2, getRedirectAppPathHome } from '../../../common/constants';
|
||||||
|
import { TaskRunResult } from '../../lib/tasks';
|
||||||
|
import { RunTaskFn, RunTaskFnFactory } from '../../types';
|
||||||
|
import {
|
||||||
|
decryptJobHeaders,
|
||||||
|
getConditionalHeaders,
|
||||||
|
omitBlockedHeaders,
|
||||||
|
generatePngObservableFactory,
|
||||||
|
setForceNow,
|
||||||
|
} from '../common';
|
||||||
|
import { getFullUrls } from '../common/v2/get_full_urls';
|
||||||
|
import { TaskPayloadPNGV2 } from './types';
|
||||||
|
|
||||||
|
export const runTaskFnFactory: RunTaskFnFactory<
|
||||||
|
RunTaskFn<TaskPayloadPNGV2>
|
||||||
|
> = function executeJobFactoryFn(reporting, parentLogger) {
|
||||||
|
const config = reporting.getConfig();
|
||||||
|
const encryptionKey = config.get('encryptionKey');
|
||||||
|
|
||||||
|
return async function runTask(jobId, job, cancellationToken, stream) {
|
||||||
|
const apmTrans = apm.startTransaction('reporting execute_job pngV2', 'reporting');
|
||||||
|
const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup');
|
||||||
|
let apmGeneratePng: { end: () => void } | null | undefined;
|
||||||
|
|
||||||
|
const generatePngObservable = await generatePngObservableFactory(reporting);
|
||||||
|
const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]);
|
||||||
|
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
|
||||||
|
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
|
||||||
|
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
|
||||||
|
map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)),
|
||||||
|
mergeMap((conditionalHeaders) => {
|
||||||
|
const relativeUrl = getRedirectAppPathHome();
|
||||||
|
const [url] = getFullUrls(config, [relativeUrl]);
|
||||||
|
const [locatorParams] = job.locatorParams.map(setForceNow(job.forceNow));
|
||||||
|
|
||||||
|
apmGetAssets?.end();
|
||||||
|
|
||||||
|
apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute');
|
||||||
|
return generatePngObservable(
|
||||||
|
jobLogger,
|
||||||
|
[url, locatorParams],
|
||||||
|
job.browserTimezone,
|
||||||
|
conditionalHeaders,
|
||||||
|
job.layout
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
tap(({ base64 }) => stream.write(base64)),
|
||||||
|
map(({ base64, warnings }) => ({
|
||||||
|
content_type: 'image/png',
|
||||||
|
content: base64,
|
||||||
|
size: (base64 && base64.length) || 0,
|
||||||
|
warnings,
|
||||||
|
})),
|
||||||
|
catchError((err) => {
|
||||||
|
jobLogger.error(err);
|
||||||
|
return Rx.throwError(err);
|
||||||
|
}),
|
||||||
|
finalize(() => apmGeneratePng?.end())
|
||||||
|
);
|
||||||
|
|
||||||
|
const stop$ = Rx.fromEventPattern(cancellationToken.on);
|
||||||
|
return process$.pipe(takeUntil(stop$)).toPromise();
|
||||||
|
};
|
||||||
|
};
|
39
x-pack/plugins/reporting/server/export_types/png_v2/index.ts
Normal file
39
x-pack/plugins/reporting/server/export_types/png_v2/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
LICENSE_TYPE_ENTERPRISE,
|
||||||
|
LICENSE_TYPE_GOLD,
|
||||||
|
LICENSE_TYPE_PLATINUM,
|
||||||
|
LICENSE_TYPE_STANDARD,
|
||||||
|
LICENSE_TYPE_TRIAL,
|
||||||
|
PNG_JOB_TYPE_V2 as jobType,
|
||||||
|
} from '../../../common/constants';
|
||||||
|
import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types';
|
||||||
|
import { createJobFnFactory } from './create_job';
|
||||||
|
import { runTaskFnFactory } from './execute_job';
|
||||||
|
import { metadata } from './metadata';
|
||||||
|
import { JobParamsPNGV2, TaskPayloadPNGV2 } from './types';
|
||||||
|
|
||||||
|
export const getExportType = (): ExportTypeDefinition<
|
||||||
|
CreateJobFn<JobParamsPNGV2>,
|
||||||
|
RunTaskFn<TaskPayloadPNGV2>
|
||||||
|
> => ({
|
||||||
|
...metadata,
|
||||||
|
jobType,
|
||||||
|
jobContentEncoding: 'base64',
|
||||||
|
jobContentExtension: 'PNG',
|
||||||
|
createJobFnFactory,
|
||||||
|
runTaskFnFactory,
|
||||||
|
validLicenses: [
|
||||||
|
LICENSE_TYPE_TRIAL,
|
||||||
|
LICENSE_TYPE_STANDARD,
|
||||||
|
LICENSE_TYPE_GOLD,
|
||||||
|
LICENSE_TYPE_PLATINUM,
|
||||||
|
LICENSE_TYPE_ENTERPRISE,
|
||||||
|
],
|
||||||
|
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PNG_REPORT_TYPE_V2 } from '../../../common/constants';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
id: PNG_REPORT_TYPE_V2,
|
||||||
|
name: 'PNG',
|
||||||
|
};
|
29
x-pack/plugins/reporting/server/export_types/png_v2/types.d.ts
vendored
Normal file
29
x-pack/plugins/reporting/server/export_types/png_v2/types.d.ts
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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 { LocatorParams } from '../../../common/types';
|
||||||
|
import type { LayoutParams } from '../../lib/layouts';
|
||||||
|
import type { BaseParams, BasePayload } from '../../types';
|
||||||
|
|
||||||
|
// Job params: structure of incoming user request data
|
||||||
|
export interface JobParamsPNGV2 extends BaseParams {
|
||||||
|
layout: LayoutParams;
|
||||||
|
/**
|
||||||
|
* This value is used to re-create the same visual state as when the report was requested as well as navigate to the correct page.
|
||||||
|
*/
|
||||||
|
locatorParams: LocatorParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job payload: structure of stored job data provided by create_job
|
||||||
|
export interface TaskPayloadPNGV2 extends BasePayload {
|
||||||
|
layout: LayoutParams;
|
||||||
|
forceNow: string;
|
||||||
|
/**
|
||||||
|
* Even though we only ever handle one locator for a PNG, we store it as an array for consistency with how PDFs are stored
|
||||||
|
*/
|
||||||
|
locatorParams: LocatorParams[];
|
||||||
|
}
|
|
@ -16,9 +16,9 @@ import {
|
||||||
getConditionalHeaders,
|
getConditionalHeaders,
|
||||||
getFullUrls,
|
getFullUrls,
|
||||||
omitBlockedHeaders,
|
omitBlockedHeaders,
|
||||||
|
getCustomLogo,
|
||||||
} from '../../common';
|
} from '../../common';
|
||||||
import { generatePdfObservableFactory } from '../lib/generate_pdf';
|
import { generatePdfObservableFactory } from '../lib/generate_pdf';
|
||||||
import { getCustomLogo } from '../lib/get_custom_logo';
|
|
||||||
import { TaskPayloadPDF } from '../types';
|
import { TaskPayloadPDF } from '../types';
|
||||||
|
|
||||||
export const runTaskFnFactory: RunTaskFnFactory<
|
export const runTaskFnFactory: RunTaskFnFactory<
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { LevelLogger } from '../../../lib';
|
||||||
import { createLayout, LayoutParams } from '../../../lib/layouts';
|
import { createLayout, LayoutParams } from '../../../lib/layouts';
|
||||||
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
||||||
import { ConditionalHeaders } from '../../common';
|
import { ConditionalHeaders } from '../../common';
|
||||||
import { PdfMaker } from './pdf';
|
import { PdfMaker } from '../../common/pdf';
|
||||||
import { getTracker } from './tracker';
|
import { getTracker } from './tracker';
|
||||||
|
|
||||||
const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
||||||
|
@ -50,7 +50,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
||||||
tracker.startScreenshots();
|
tracker.startScreenshots();
|
||||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls,
|
urlsOrUrlLocatorTuples: urls,
|
||||||
conditionalHeaders,
|
conditionalHeaders,
|
||||||
layout,
|
layout,
|
||||||
browserTimezone,
|
browserTimezone,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { BaseParams, BasePayload } from '../../types';
|
||||||
interface BaseParamsPDF {
|
interface BaseParamsPDF {
|
||||||
layout: LayoutParams;
|
layout: LayoutParams;
|
||||||
forceNow?: string;
|
forceNow?: string;
|
||||||
|
// TODO: Add comment explaining this field
|
||||||
relativeUrls: string[];
|
relativeUrls: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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 { cryptoFactory } from '../../lib';
|
||||||
|
import { CreateJobFn, CreateJobFnFactory } from '../../types';
|
||||||
|
import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types';
|
||||||
|
|
||||||
|
export const createJobFnFactory: CreateJobFnFactory<
|
||||||
|
CreateJobFn<JobParamsPDFV2, TaskPayloadPDFV2>
|
||||||
|
> = function createJobFactoryFn(reporting, logger) {
|
||||||
|
const config = reporting.getConfig();
|
||||||
|
const crypto = cryptoFactory(config.get('encryptionKey'));
|
||||||
|
|
||||||
|
return async function createJob(jobParams, context, req) {
|
||||||
|
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...jobParams,
|
||||||
|
headers: serializedEncryptedHeaders,
|
||||||
|
spaceId: reporting.getSpaceId(req, logger),
|
||||||
|
forceNow: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
jest.mock('./lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() }));
|
||||||
|
|
||||||
|
import * as Rx from 'rxjs';
|
||||||
|
import { Writable } from 'stream';
|
||||||
|
import { ReportingCore } from '../../';
|
||||||
|
import { CancellationToken } from '../../../common';
|
||||||
|
import { LocatorParams } from '../../../common/types';
|
||||||
|
import { cryptoFactory, LevelLogger } from '../../lib';
|
||||||
|
import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers';
|
||||||
|
import { runTaskFnFactory } from './execute_job';
|
||||||
|
import { generatePdfObservableFactory } from './lib/generate_pdf';
|
||||||
|
import { TaskPayloadPDFV2 } from './types';
|
||||||
|
|
||||||
|
let content: string;
|
||||||
|
let mockReporting: ReportingCore;
|
||||||
|
let stream: jest.Mocked<Writable>;
|
||||||
|
|
||||||
|
const cancellationToken = ({
|
||||||
|
on: jest.fn(),
|
||||||
|
} as unknown) as CancellationToken;
|
||||||
|
|
||||||
|
const mockLoggerFactory = {
|
||||||
|
get: jest.fn().mockImplementation(() => ({
|
||||||
|
error: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const getMockLogger = () => new LevelLogger(mockLoggerFactory);
|
||||||
|
|
||||||
|
const mockEncryptionKey = 'testencryptionkey';
|
||||||
|
const encryptHeaders = async (headers: Record<string, string>) => {
|
||||||
|
const crypto = cryptoFactory(mockEncryptionKey);
|
||||||
|
return await crypto.encrypt(headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBasePayload = (baseObj: any) =>
|
||||||
|
({
|
||||||
|
params: { forceNow: 'test' },
|
||||||
|
...baseObj,
|
||||||
|
} as TaskPayloadPDFV2);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
content = '';
|
||||||
|
stream = ({ write: jest.fn((chunk) => (content += chunk)) } as unknown) as typeof stream;
|
||||||
|
|
||||||
|
const reportingConfig = {
|
||||||
|
'server.basePath': '/sbp',
|
||||||
|
index: '.reports-test',
|
||||||
|
encryptionKey: mockEncryptionKey,
|
||||||
|
'kibanaServer.hostname': 'localhost',
|
||||||
|
'kibanaServer.port': 5601,
|
||||||
|
'kibanaServer.protocol': 'http',
|
||||||
|
};
|
||||||
|
const mockSchema = createMockConfigSchema(reportingConfig);
|
||||||
|
mockReporting = await createMockReportingCore(mockSchema);
|
||||||
|
|
||||||
|
(generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset());
|
||||||
|
|
||||||
|
test(`passes browserTimezone to generatePdf`, async () => {
|
||||||
|
const encryptedHeaders = await encryptHeaders({});
|
||||||
|
const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock;
|
||||||
|
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||||
|
|
||||||
|
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
|
||||||
|
const browserTimezone = 'UTC';
|
||||||
|
await runTask(
|
||||||
|
'pdfJobId',
|
||||||
|
getBasePayload({
|
||||||
|
forceNow: 'test',
|
||||||
|
title: 'PDF Params Timezone Test',
|
||||||
|
locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[],
|
||||||
|
browserTimezone,
|
||||||
|
headers: encryptedHeaders,
|
||||||
|
}),
|
||||||
|
cancellationToken,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
|
||||||
|
const tzParam = generatePdfObservable.mock.calls[0][4];
|
||||||
|
expect(tzParam).toBe('UTC');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`returns content_type of application/pdf`, async () => {
|
||||||
|
const logger = getMockLogger();
|
||||||
|
const runTask = runTaskFnFactory(mockReporting, logger);
|
||||||
|
const encryptedHeaders = await encryptHeaders({});
|
||||||
|
|
||||||
|
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
|
||||||
|
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from('')));
|
||||||
|
|
||||||
|
const { content_type: contentType } = await runTask(
|
||||||
|
'pdfJobId',
|
||||||
|
getBasePayload({ locatorParams: [], headers: encryptedHeaders }),
|
||||||
|
cancellationToken,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
expect(contentType).toBe('application/pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`returns content of generatePdf getBuffer base64 encoded`, async () => {
|
||||||
|
const testContent = 'test content';
|
||||||
|
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
|
||||||
|
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
|
||||||
|
|
||||||
|
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
|
||||||
|
const encryptedHeaders = await encryptHeaders({});
|
||||||
|
await runTask(
|
||||||
|
'pdfJobId',
|
||||||
|
getBasePayload({ locatorParams: [], headers: encryptedHeaders }),
|
||||||
|
cancellationToken,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(content).toEqual(Buffer.from(testContent).toString('base64'));
|
||||||
|
});
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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 apm from 'elastic-apm-node';
|
||||||
|
import * as Rx from 'rxjs';
|
||||||
|
import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators';
|
||||||
|
import { PDF_JOB_TYPE_V2 } from '../../../common/constants';
|
||||||
|
import { TaskRunResult } from '../../lib/tasks';
|
||||||
|
import { RunTaskFn, RunTaskFnFactory } from '../../types';
|
||||||
|
import {
|
||||||
|
decryptJobHeaders,
|
||||||
|
getConditionalHeaders,
|
||||||
|
omitBlockedHeaders,
|
||||||
|
getCustomLogo,
|
||||||
|
setForceNow,
|
||||||
|
} from '../common';
|
||||||
|
import { generatePdfObservableFactory } from './lib/generate_pdf';
|
||||||
|
import { TaskPayloadPDFV2 } from './types';
|
||||||
|
|
||||||
|
export const runTaskFnFactory: RunTaskFnFactory<
|
||||||
|
RunTaskFn<TaskPayloadPDFV2>
|
||||||
|
> = function executeJobFactoryFn(reporting, parentLogger) {
|
||||||
|
const config = reporting.getConfig();
|
||||||
|
const encryptionKey = config.get('encryptionKey');
|
||||||
|
|
||||||
|
return async function runTask(jobId, job, cancellationToken, stream) {
|
||||||
|
const jobLogger = parentLogger.clone([PDF_JOB_TYPE_V2, 'execute-job', jobId]);
|
||||||
|
const apmTrans = apm.startTransaction('reporting execute_job pdf_v2', 'reporting');
|
||||||
|
const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup');
|
||||||
|
let apmGeneratePdf: { end: () => void } | null | undefined;
|
||||||
|
|
||||||
|
const generatePdfObservable = await generatePdfObservableFactory(reporting);
|
||||||
|
|
||||||
|
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
|
||||||
|
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
|
||||||
|
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
|
||||||
|
map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)),
|
||||||
|
mergeMap((conditionalHeaders) =>
|
||||||
|
getCustomLogo(reporting, conditionalHeaders, job.spaceId, jobLogger)
|
||||||
|
),
|
||||||
|
mergeMap(({ logo, conditionalHeaders }) => {
|
||||||
|
const { browserTimezone, layout, title, locatorParams } = job;
|
||||||
|
if (apmGetAssets) apmGetAssets.end();
|
||||||
|
|
||||||
|
apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute');
|
||||||
|
return generatePdfObservable(
|
||||||
|
jobLogger,
|
||||||
|
jobId,
|
||||||
|
title,
|
||||||
|
locatorParams.map(setForceNow(job.forceNow)),
|
||||||
|
browserTimezone,
|
||||||
|
conditionalHeaders,
|
||||||
|
layout,
|
||||||
|
logo
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
map(({ buffer, warnings }) => {
|
||||||
|
if (apmGeneratePdf) apmGeneratePdf.end();
|
||||||
|
|
||||||
|
const apmEncode = apmTrans?.startSpan('encode_pdf', 'output');
|
||||||
|
const content = buffer?.toString('base64') || null;
|
||||||
|
apmEncode?.end();
|
||||||
|
|
||||||
|
stream.write(content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content_type: 'application/pdf',
|
||||||
|
content,
|
||||||
|
size: buffer?.byteLength || 0,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
jobLogger.error(err);
|
||||||
|
return Rx.throwError(err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const stop$ = Rx.fromEventPattern(cancellationToken.on);
|
||||||
|
|
||||||
|
if (apmTrans) apmTrans.end();
|
||||||
|
return process$.pipe(takeUntil(stop$)).toPromise();
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
LICENSE_TYPE_ENTERPRISE,
|
||||||
|
LICENSE_TYPE_GOLD,
|
||||||
|
LICENSE_TYPE_PLATINUM,
|
||||||
|
LICENSE_TYPE_STANDARD,
|
||||||
|
LICENSE_TYPE_TRIAL,
|
||||||
|
PDF_JOB_TYPE_V2 as jobType,
|
||||||
|
} from '../../../common/constants';
|
||||||
|
import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types';
|
||||||
|
import { createJobFnFactory } from './create_job';
|
||||||
|
import { runTaskFnFactory } from './execute_job';
|
||||||
|
import { metadata } from './metadata';
|
||||||
|
import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types';
|
||||||
|
|
||||||
|
export const getExportType = (): ExportTypeDefinition<
|
||||||
|
CreateJobFn<JobParamsPDFV2>,
|
||||||
|
RunTaskFn<TaskPayloadPDFV2>
|
||||||
|
> => ({
|
||||||
|
...metadata,
|
||||||
|
jobType,
|
||||||
|
jobContentEncoding: 'base64',
|
||||||
|
jobContentExtension: 'pdf',
|
||||||
|
createJobFnFactory,
|
||||||
|
runTaskFnFactory,
|
||||||
|
validLicenses: [
|
||||||
|
LICENSE_TYPE_TRIAL,
|
||||||
|
LICENSE_TYPE_STANDARD,
|
||||||
|
LICENSE_TYPE_GOLD,
|
||||||
|
LICENSE_TYPE_PLATINUM,
|
||||||
|
LICENSE_TYPE_ENTERPRISE,
|
||||||
|
],
|
||||||
|
});
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* 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 { groupBy, zip } from 'lodash';
|
||||||
|
import * as Rx from 'rxjs';
|
||||||
|
import { mergeMap } from 'rxjs/operators';
|
||||||
|
import { ReportingCore } from '../../../';
|
||||||
|
import { getRedirectAppPathHome } from '../../../../common/constants';
|
||||||
|
import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../../common/types';
|
||||||
|
import { LevelLogger } from '../../../lib';
|
||||||
|
import { createLayout, LayoutParams } from '../../../lib/layouts';
|
||||||
|
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
||||||
|
import { ConditionalHeaders } from '../../common';
|
||||||
|
import { PdfMaker } from '../../common/pdf';
|
||||||
|
import { getFullUrls } from '../../common/v2/get_full_urls';
|
||||||
|
import { getTracker } from './tracker';
|
||||||
|
|
||||||
|
const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
||||||
|
const grouped = groupBy(urlScreenshots.map((u) => u.timeRange));
|
||||||
|
const values = Object.values(grouped);
|
||||||
|
if (values.length === 1) {
|
||||||
|
return values[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
||||||
|
const config = reporting.getConfig();
|
||||||
|
const captureConfig = config.get('capture');
|
||||||
|
const { browserDriverFactory } = await reporting.getPluginStartDeps();
|
||||||
|
|
||||||
|
return function generatePdfObservable(
|
||||||
|
logger: LevelLogger,
|
||||||
|
jobId: string,
|
||||||
|
title: string,
|
||||||
|
locatorParams: LocatorParams[],
|
||||||
|
browserTimezone: string | undefined,
|
||||||
|
conditionalHeaders: ConditionalHeaders,
|
||||||
|
layoutParams: LayoutParams,
|
||||||
|
logo?: string
|
||||||
|
): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> {
|
||||||
|
const tracker = getTracker();
|
||||||
|
tracker.startLayout();
|
||||||
|
|
||||||
|
const layout = createLayout(captureConfig, layoutParams);
|
||||||
|
logger.debug(`Layout: width=${layout.width} height=${layout.height}`);
|
||||||
|
tracker.endLayout();
|
||||||
|
|
||||||
|
tracker.startScreenshots();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each locator we get the relative URL to the redirect app
|
||||||
|
*/
|
||||||
|
const relativeUrls = locatorParams.map(() => getRedirectAppPathHome());
|
||||||
|
const urls = getFullUrls(reporting.getConfig(), relativeUrls);
|
||||||
|
|
||||||
|
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||||
|
logger,
|
||||||
|
urlsOrUrlLocatorTuples: zip(urls, locatorParams) as UrlOrUrlLocatorTuple[],
|
||||||
|
conditionalHeaders,
|
||||||
|
layout,
|
||||||
|
browserTimezone,
|
||||||
|
}).pipe(
|
||||||
|
mergeMap(async (results: ScreenshotResults[]) => {
|
||||||
|
tracker.endScreenshots();
|
||||||
|
|
||||||
|
tracker.startSetup();
|
||||||
|
const pdfOutput = new PdfMaker(layout, logo);
|
||||||
|
if (title) {
|
||||||
|
const timeRange = getTimeRange(results);
|
||||||
|
title += timeRange ? ` - ${timeRange}` : '';
|
||||||
|
pdfOutput.setTitle(title);
|
||||||
|
}
|
||||||
|
tracker.endSetup();
|
||||||
|
|
||||||
|
results.forEach((r) => {
|
||||||
|
r.screenshots.forEach((screenshot) => {
|
||||||
|
logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.base64EncodedData?.length || 0}`); // prettier-ignore
|
||||||
|
tracker.startAddImage();
|
||||||
|
tracker.endAddImage();
|
||||||
|
pdfOutput.addImage(screenshot.base64EncodedData, {
|
||||||
|
title: screenshot.title,
|
||||||
|
description: screenshot.description,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer: Buffer | null = null;
|
||||||
|
try {
|
||||||
|
tracker.startCompile();
|
||||||
|
logger.debug(`Compiling PDF using "${layout.id}" layout...`);
|
||||||
|
pdfOutput.generate();
|
||||||
|
tracker.endCompile();
|
||||||
|
|
||||||
|
tracker.startGetBuffer();
|
||||||
|
logger.debug(`Generating PDF Buffer...`);
|
||||||
|
buffer = await pdfOutput.getBuffer();
|
||||||
|
|
||||||
|
const byteLength = buffer?.byteLength ?? 0;
|
||||||
|
logger.debug(`PDF buffer byte length: ${byteLength}`);
|
||||||
|
tracker.setByteLength(byteLength);
|
||||||
|
|
||||||
|
tracker.endGetBuffer();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Could not generate the PDF buffer!`);
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.end();
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
warnings: results.reduce((found, current) => {
|
||||||
|
if (current.error) {
|
||||||
|
found.push(current.error.message);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}, [] as string[]),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return screenshots$;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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 apm from 'elastic-apm-node';
|
||||||
|
|
||||||
|
interface PdfTracker {
|
||||||
|
setByteLength: (byteLength: number) => void;
|
||||||
|
startLayout: () => void;
|
||||||
|
endLayout: () => void;
|
||||||
|
startScreenshots: () => void;
|
||||||
|
endScreenshots: () => void;
|
||||||
|
startSetup: () => void;
|
||||||
|
endSetup: () => void;
|
||||||
|
startAddImage: () => void;
|
||||||
|
endAddImage: () => void;
|
||||||
|
startCompile: () => void;
|
||||||
|
endCompile: () => void;
|
||||||
|
startGetBuffer: () => void;
|
||||||
|
endGetBuffer: () => void;
|
||||||
|
end: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPANTYPE_SETUP = 'setup';
|
||||||
|
const SPANTYPE_OUTPUT = 'output';
|
||||||
|
|
||||||
|
interface ApmSpan {
|
||||||
|
end: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTracker(): PdfTracker {
|
||||||
|
const apmTrans = apm.startTransaction('reporting generate_pdf', 'reporting');
|
||||||
|
|
||||||
|
let apmLayout: ApmSpan | null = null;
|
||||||
|
let apmScreenshots: ApmSpan | null = null;
|
||||||
|
let apmSetup: ApmSpan | null = null;
|
||||||
|
let apmAddImage: ApmSpan | null = null;
|
||||||
|
let apmCompilePdf: ApmSpan | null = null;
|
||||||
|
let apmGetBuffer: ApmSpan | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startLayout() {
|
||||||
|
apmLayout = apmTrans?.startSpan('create_layout', SPANTYPE_SETUP) || null;
|
||||||
|
},
|
||||||
|
endLayout() {
|
||||||
|
if (apmLayout) apmLayout.end();
|
||||||
|
},
|
||||||
|
startScreenshots() {
|
||||||
|
apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', SPANTYPE_SETUP) || null;
|
||||||
|
},
|
||||||
|
endScreenshots() {
|
||||||
|
if (apmScreenshots) apmScreenshots.end();
|
||||||
|
},
|
||||||
|
startSetup() {
|
||||||
|
apmSetup = apmTrans?.startSpan('setup_pdf', SPANTYPE_SETUP) || null;
|
||||||
|
},
|
||||||
|
endSetup() {
|
||||||
|
if (apmSetup) apmSetup.end();
|
||||||
|
},
|
||||||
|
startAddImage() {
|
||||||
|
apmAddImage = apmTrans?.startSpan('add_pdf_image', SPANTYPE_OUTPUT) || null;
|
||||||
|
},
|
||||||
|
endAddImage() {
|
||||||
|
if (apmAddImage) apmAddImage.end();
|
||||||
|
},
|
||||||
|
startCompile() {
|
||||||
|
apmCompilePdf = apmTrans?.startSpan('compile_pdf', SPANTYPE_OUTPUT) || null;
|
||||||
|
},
|
||||||
|
endCompile() {
|
||||||
|
if (apmCompilePdf) apmCompilePdf.end();
|
||||||
|
},
|
||||||
|
startGetBuffer() {
|
||||||
|
apmGetBuffer = apmTrans?.startSpan('get_buffer', SPANTYPE_OUTPUT) || null;
|
||||||
|
},
|
||||||
|
endGetBuffer() {
|
||||||
|
if (apmGetBuffer) apmGetBuffer.end();
|
||||||
|
},
|
||||||
|
setByteLength(byteLength: number) {
|
||||||
|
apmTrans?.setLabel('byte_length', byteLength, false);
|
||||||
|
},
|
||||||
|
end() {
|
||||||
|
if (apmTrans) apmTrans.end();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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 { forEach, isArray } from 'lodash';
|
||||||
|
import { url } from '../../../../../../../src/plugins/kibana_utils/server';
|
||||||
|
|
||||||
|
function toKeyValue(obj) {
|
||||||
|
const parts = [];
|
||||||
|
forEach(obj, function (value, key) {
|
||||||
|
if (isArray(value)) {
|
||||||
|
forEach(value, function (arrayValue) {
|
||||||
|
const keyStr = url.encodeUriQuery(key, true);
|
||||||
|
const valStr = arrayValue === true ? '' : '=' + url.encodeUriQuery(arrayValue, true);
|
||||||
|
parts.push(keyStr + valStr);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const keyStr = url.encodeUriQuery(key, true);
|
||||||
|
const valStr = value === true ? '' : '=' + url.encodeUriQuery(value, true);
|
||||||
|
parts.push(keyStr + valStr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parts.length ? parts.join('&') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uriEncode = {
|
||||||
|
stringify: toKeyValue,
|
||||||
|
string: url.encodeUriQuery,
|
||||||
|
};
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
id: 'printablePdfV2',
|
||||||
|
name: 'PDF',
|
||||||
|
};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LocatorParams } from '../../../common/types';
|
||||||
|
import { LayoutParams } from '../../lib/layouts';
|
||||||
|
import { BaseParams, BasePayload } from '../../types';
|
||||||
|
|
||||||
|
interface BaseParamsPDFV2 {
|
||||||
|
layout: LayoutParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This value is used to re-create the same visual state as when the report was requested as well as navigate to the correct page.
|
||||||
|
*/
|
||||||
|
locatorParams: LocatorParams[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job params: structure of incoming user request data, after being parsed from RISON
|
||||||
|
export type JobParamsPDFV2 = BaseParamsPDFV2 & BaseParams;
|
||||||
|
|
||||||
|
// Job payload: structure of stored job data provided by create_job
|
||||||
|
export interface TaskPayloadPDFV2 extends BasePayload, BaseParamsPDFV2 {
|
||||||
|
layout: LayoutParams;
|
||||||
|
/**
|
||||||
|
* The value of forceNow is injected server-side every time a given report is generated.
|
||||||
|
*/
|
||||||
|
forceNow: string;
|
||||||
|
}
|
|
@ -21,5 +21,4 @@ export {
|
||||||
ReportingSetupDeps as PluginSetup,
|
ReportingSetupDeps as PluginSetup,
|
||||||
ReportingStartDeps as PluginStart,
|
ReportingStartDeps as PluginStart,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export { ReportingPlugin as Plugin };
|
export { ReportingPlugin as Plugin };
|
||||||
|
|
|
@ -10,7 +10,10 @@ import { getExportType as getTypeCsvDeprecated } from '../export_types/csv';
|
||||||
import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_searchsource_immediate';
|
import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_searchsource_immediate';
|
||||||
import { getExportType as getTypeCsv } from '../export_types/csv_searchsource';
|
import { getExportType as getTypeCsv } from '../export_types/csv_searchsource';
|
||||||
import { getExportType as getTypePng } from '../export_types/png';
|
import { getExportType as getTypePng } from '../export_types/png';
|
||||||
|
import { getExportType as getTypePngV2 } from '../export_types/png_v2';
|
||||||
import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf';
|
import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf';
|
||||||
|
import { getExportType as getTypePrintablePdfV2 } from '../export_types/printable_pdf_v2';
|
||||||
|
|
||||||
import { CreateJobFn, ExportTypeDefinition } from '../types';
|
import { CreateJobFn, ExportTypeDefinition } from '../types';
|
||||||
|
|
||||||
type GetCallbackFn = (item: ExportTypeDefinition) => boolean;
|
type GetCallbackFn = (item: ExportTypeDefinition) => boolean;
|
||||||
|
@ -88,7 +91,9 @@ export function getExportTypesRegistry(): ExportTypesRegistry {
|
||||||
getTypeCsvDeprecated,
|
getTypeCsvDeprecated,
|
||||||
getTypeCsvFromSavedObject,
|
getTypeCsvFromSavedObject,
|
||||||
getTypePng,
|
getTypePng,
|
||||||
|
getTypePngV2,
|
||||||
getTypePrintablePdf,
|
getTypePrintablePdf,
|
||||||
|
getTypePrintablePdfV2,
|
||||||
];
|
];
|
||||||
getTypeFns.forEach((getType) => {
|
getTypeFns.forEach((getType) => {
|
||||||
registry.register(getType());
|
registry.register(getType());
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LevelLogger } from '../';
|
import { LevelLogger } from '../';
|
||||||
|
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||||
import { ConditionalHeaders } from '../../export_types/common';
|
import { ConditionalHeaders } from '../../export_types/common';
|
||||||
import { LayoutInstance } from '../layouts';
|
import { LayoutInstance } from '../layouts';
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ export { getScreenshots$ } from './observable';
|
||||||
|
|
||||||
export interface ScreenshotObservableOpts {
|
export interface ScreenshotObservableOpts {
|
||||||
logger: LevelLogger;
|
logger: LevelLogger;
|
||||||
urls: string[];
|
urlsOrUrlLocatorTuples: UrlOrUrlLocatorTuple[];
|
||||||
conditionalHeaders: ConditionalHeaders;
|
conditionalHeaders: ConditionalHeaders;
|
||||||
layout: LayoutInstance;
|
layout: LayoutInstance;
|
||||||
browserTimezone?: string;
|
browserTimezone?: string;
|
||||||
|
|
|
@ -69,7 +69,7 @@ describe('Screenshot Observable Pipeline', () => {
|
||||||
it('pipelines a single url into screenshot and timeRange', async () => {
|
it('pipelines a single url into screenshot and timeRange', async () => {
|
||||||
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls: ['/welcome/home/start/index.htm'],
|
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.htm'],
|
||||||
conditionalHeaders: {} as ConditionalHeaders,
|
conditionalHeaders: {} as ConditionalHeaders,
|
||||||
layout: mockLayout,
|
layout: mockLayout,
|
||||||
browserTimezone: 'UTC',
|
browserTimezone: 'UTC',
|
||||||
|
@ -129,7 +129,10 @@ describe('Screenshot Observable Pipeline', () => {
|
||||||
// test
|
// test
|
||||||
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'],
|
urlsOrUrlLocatorTuples: [
|
||||||
|
'/welcome/home/start/index2.htm',
|
||||||
|
'/welcome/home/start/index.php3?page=./home.php',
|
||||||
|
],
|
||||||
conditionalHeaders: {} as ConditionalHeaders,
|
conditionalHeaders: {} as ConditionalHeaders,
|
||||||
layout: mockLayout,
|
layout: mockLayout,
|
||||||
browserTimezone: 'UTC',
|
browserTimezone: 'UTC',
|
||||||
|
@ -228,7 +231,7 @@ describe('Screenshot Observable Pipeline', () => {
|
||||||
const getScreenshot = async () => {
|
const getScreenshot = async () => {
|
||||||
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls: [
|
urlsOrUrlLocatorTuples: [
|
||||||
'/welcome/home/start/index2.htm',
|
'/welcome/home/start/index2.htm',
|
||||||
'/welcome/home/start/index.php3?page=./home.php3',
|
'/welcome/home/start/index.php3?page=./home.php3',
|
||||||
],
|
],
|
||||||
|
@ -322,7 +325,7 @@ describe('Screenshot Observable Pipeline', () => {
|
||||||
const getScreenshot = async () => {
|
const getScreenshot = async () => {
|
||||||
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
|
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||||
conditionalHeaders: {} as ConditionalHeaders,
|
conditionalHeaders: {} as ConditionalHeaders,
|
||||||
layout: mockLayout,
|
layout: mockLayout,
|
||||||
browserTimezone: 'UTC',
|
browserTimezone: 'UTC',
|
||||||
|
@ -352,7 +355,7 @@ describe('Screenshot Observable Pipeline', () => {
|
||||||
|
|
||||||
const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||||
logger,
|
logger,
|
||||||
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
|
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||||
conditionalHeaders: {} as ConditionalHeaders,
|
conditionalHeaders: {} as ConditionalHeaders,
|
||||||
layout: mockLayout,
|
layout: mockLayout,
|
||||||
browserTimezone: 'UTC',
|
browserTimezone: 'UTC',
|
||||||
|
|
|
@ -34,7 +34,13 @@ interface ScreenSetupData {
|
||||||
export function getScreenshots$(
|
export function getScreenshots$(
|
||||||
captureConfig: CaptureConfig,
|
captureConfig: CaptureConfig,
|
||||||
browserDriverFactory: HeadlessChromiumDriverFactory,
|
browserDriverFactory: HeadlessChromiumDriverFactory,
|
||||||
{ logger, urls, conditionalHeaders, layout, browserTimezone }: ScreenshotObservableOpts
|
{
|
||||||
|
logger,
|
||||||
|
urlsOrUrlLocatorTuples,
|
||||||
|
conditionalHeaders,
|
||||||
|
layout,
|
||||||
|
browserTimezone,
|
||||||
|
}: ScreenshotObservableOpts
|
||||||
): Rx.Observable<ScreenshotResults[]> {
|
): Rx.Observable<ScreenshotResults[]> {
|
||||||
const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting');
|
const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting');
|
||||||
|
|
||||||
|
@ -49,8 +55,8 @@ export function getScreenshots$(
|
||||||
apmCreatePage?.end();
|
apmCreatePage?.end();
|
||||||
exit$.subscribe({ error: () => apmTrans?.end() });
|
exit$.subscribe({ error: () => apmTrans?.end() });
|
||||||
|
|
||||||
return Rx.from(urls).pipe(
|
return Rx.from(urlsOrUrlLocatorTuples).pipe(
|
||||||
concatMap((url, index) => {
|
concatMap((urlOrUrlLocatorTuple, index) => {
|
||||||
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
|
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
|
||||||
mergeMap(() => {
|
mergeMap(() => {
|
||||||
// If we're moving to another page in the app, we'll want to wait for the app to tell us
|
// If we're moving to another page in the app, we'll want to wait for the app to tell us
|
||||||
|
@ -62,7 +68,7 @@ export function getScreenshots$(
|
||||||
return openUrl(
|
return openUrl(
|
||||||
captureConfig,
|
captureConfig,
|
||||||
driver,
|
driver,
|
||||||
url,
|
urlOrUrlLocatorTuple,
|
||||||
pageLoadSelector,
|
pageLoadSelector,
|
||||||
conditionalHeaders,
|
conditionalHeaders,
|
||||||
logger
|
logger
|
||||||
|
@ -129,7 +135,7 @@ export function getScreenshots$(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
take(urls.length),
|
take(urlsOrUrlLocatorTuples.length),
|
||||||
toArray()
|
toArray()
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||||
import { LevelLogger, startTrace } from '../';
|
import { LevelLogger, startTrace } from '../';
|
||||||
import { durationToNumber } from '../../../common/schema_utils';
|
import { durationToNumber } from '../../../common/schema_utils';
|
||||||
import { HeadlessChromiumDriver } from '../../browsers';
|
import { HeadlessChromiumDriver } from '../../browsers';
|
||||||
|
@ -15,19 +16,24 @@ import { CaptureConfig } from '../../types';
|
||||||
export const openUrl = async (
|
export const openUrl = async (
|
||||||
captureConfig: CaptureConfig,
|
captureConfig: CaptureConfig,
|
||||||
browser: HeadlessChromiumDriver,
|
browser: HeadlessChromiumDriver,
|
||||||
url: string,
|
urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple,
|
||||||
pageLoadSelector: string,
|
waitForSelector: string,
|
||||||
conditionalHeaders: ConditionalHeaders,
|
conditionalHeaders: ConditionalHeaders,
|
||||||
logger: LevelLogger
|
logger: LevelLogger
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const endTrace = startTrace('open_url', 'wait');
|
const endTrace = startTrace('open_url', 'wait');
|
||||||
|
let url: string;
|
||||||
|
let locator: undefined | LocatorParams;
|
||||||
|
|
||||||
|
if (typeof urlOrUrlLocatorTuple === 'string') {
|
||||||
|
url = urlOrUrlLocatorTuple;
|
||||||
|
} else {
|
||||||
|
[url, locator] = urlOrUrlLocatorTuple;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const timeout = durationToNumber(captureConfig.timeouts.openUrl);
|
const timeout = durationToNumber(captureConfig.timeouts.openUrl);
|
||||||
await browser.open(
|
await browser.open(url, { conditionalHeaders, waitForSelector, timeout, locator }, logger);
|
||||||
url,
|
|
||||||
{ conditionalHeaders, waitForSelector: pageLoadSelector, timeout },
|
|
||||||
logger
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
@ -33,11 +33,14 @@ export class ReportingPlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
public setup(core: CoreSetup, plugins: ReportingSetupDeps) {
|
public setup(core: CoreSetup, plugins: ReportingSetupDeps) {
|
||||||
|
const { http } = core;
|
||||||
|
const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins;
|
||||||
|
|
||||||
const reportingCore = new ReportingCore(this.logger, this.initContext);
|
const reportingCore = new ReportingCore(this.logger, this.initContext);
|
||||||
|
|
||||||
// prevent throwing errors in route handlers about async deps not being initialized
|
// prevent throwing errors in route handlers about async deps not being initialized
|
||||||
// @ts-expect-error null is not assignable to object. use a boolean property to ensure reporting API is enabled.
|
// @ts-expect-error null is not assignable to object. use a boolean property to ensure reporting API is enabled.
|
||||||
core.http.registerRouteHandlerContext(PLUGIN_ID, () => {
|
http.registerRouteHandlerContext(PLUGIN_ID, () => {
|
||||||
if (reportingCore.pluginIsStarted()) {
|
if (reportingCore.pluginIsStarted()) {
|
||||||
return reportingCore.getContract();
|
return reportingCore.getContract();
|
||||||
} else {
|
} else {
|
||||||
|
@ -46,9 +49,6 @@ export class ReportingPlugin
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { http } = core;
|
|
||||||
const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins;
|
|
||||||
|
|
||||||
const router = http.createRouter<ReportingRequestHandlerContext>();
|
const router = http.createRouter<ReportingRequestHandlerContext>();
|
||||||
const basePath = http.basePath;
|
const basePath = http.basePath;
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,9 @@ import {
|
||||||
import { registerDiagnoseScreenshot } from './screenshot';
|
import { registerDiagnoseScreenshot } from './screenshot';
|
||||||
import type { ReportingRequestHandlerContext } from '../../types';
|
import type { ReportingRequestHandlerContext } from '../../types';
|
||||||
|
|
||||||
jest.mock('../../export_types/png/lib/generate_png');
|
jest.mock('../../export_types/common/generate_png');
|
||||||
|
|
||||||
import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
|
import { generatePngObservableFactory } from '../../export_types/common';
|
||||||
|
|
||||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,8 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { ReportingCore } from '../..';
|
import { ReportingCore } from '../..';
|
||||||
import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server';
|
import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server';
|
||||||
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
||||||
import { omitBlockedHeaders } from '../../export_types/common';
|
import { omitBlockedHeaders, generatePngObservableFactory } from '../../export_types/common';
|
||||||
import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url';
|
import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url';
|
||||||
import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
|
|
||||||
import { LevelLogger as Logger } from '../../lib';
|
import { LevelLogger as Logger } from '../../lib';
|
||||||
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
|
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
|
||||||
import { DiagnosticResponse } from './';
|
import { DiagnosticResponse } from './';
|
||||||
|
|
|
@ -112,7 +112,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
|
||||||
const result = await jobsQuery.get(user, docId);
|
const result = await jobsQuery.get(user, docId);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw Boom.notFound();
|
return res.notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { jobtype: jobType } = result;
|
const { jobtype: jobType } = result;
|
||||||
|
|
|
@ -30,11 +30,11 @@ import { ReportTaskParams } from './lib/tasks';
|
||||||
export interface ReportingSetupDeps {
|
export interface ReportingSetupDeps {
|
||||||
licensing: LicensingPluginSetup;
|
licensing: LicensingPluginSetup;
|
||||||
features: FeaturesPluginSetup;
|
features: FeaturesPluginSetup;
|
||||||
|
screenshotMode: ScreenshotModePluginSetup;
|
||||||
security?: SecurityPluginSetup;
|
security?: SecurityPluginSetup;
|
||||||
spaces?: SpacesPluginSetup;
|
spaces?: SpacesPluginSetup;
|
||||||
taskManager: TaskManagerSetupContract;
|
taskManager: TaskManagerSetupContract;
|
||||||
usageCollection?: UsageCollectionSetup;
|
usageCollection?: UsageCollectionSetup;
|
||||||
screenshotMode: ScreenshotModePluginSetup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReportingStartDeps {
|
export interface ReportingStartDeps {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue