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/storybook/build
|
||||
/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
|
||||
/packages/elastic-eslint-config-kibana
|
||||
|
|
|
@ -19,7 +19,8 @@ export interface ScreenshotModePluginSetup {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ const createSetupContract = (): Setup => {
|
|||
registerUrlGenerator: jest.fn(),
|
||||
},
|
||||
url,
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
@ -38,6 +39,7 @@ const createStartContract = (): Start => {
|
|||
getUrlGenerator: jest.fn(),
|
||||
},
|
||||
toggleShareContextMenu: jest.fn(),
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
UrlGeneratorsStart,
|
||||
} from './url_generators/url_generator_service';
|
||||
import { UrlService } from '../common/url_service';
|
||||
import { RedirectManager } from './url_service';
|
||||
import { RedirectManager, RedirectOptions } from './url_service';
|
||||
|
||||
export interface ShareSetupDependencies {
|
||||
securityOss?: SecurityOssPluginSetup;
|
||||
|
@ -42,6 +42,12 @@ export type SharePluginSetup = ShareMenuRegistrySetup & {
|
|||
* Utilities to work with URL locators and short URLs.
|
||||
*/
|
||||
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 */
|
||||
|
@ -57,12 +63,20 @@ export type SharePluginStart = ShareMenuManagerStart & {
|
|||
* Utilities to work with URL locators and short URLs.
|
||||
*/
|
||||
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> {
|
||||
private readonly shareMenuRegistry = new ShareMenuRegistry();
|
||||
private readonly shareContextMenu = new ShareMenuManager();
|
||||
private readonly urlGeneratorsService = new UrlGeneratorsService();
|
||||
|
||||
private redirectManager?: RedirectManager;
|
||||
private url?: UrlService;
|
||||
|
||||
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,
|
||||
});
|
||||
redirectManager.registerRedirectApp(core);
|
||||
this.redirectManager.registerRedirectApp(core);
|
||||
|
||||
return {
|
||||
...this.shareMenuRegistry.setup(),
|
||||
urlGenerators: this.urlGeneratorsService.setup(core),
|
||||
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),
|
||||
url: this.url!,
|
||||
navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,10 @@ export class RedirectManager {
|
|||
|
||||
public onMount(urlLocationSearch: string) {
|
||||
const options = this.parseSearchParams(urlLocationSearch);
|
||||
this.navigate(options);
|
||||
}
|
||||
|
||||
public navigate(options: RedirectOptions) {
|
||||
const locator = this.deps.url.locators.get(options.id);
|
||||
|
||||
if (!locator) {
|
||||
|
|
|
@ -7,3 +7,9 @@
|
|||
|
||||
export const PLUGIN_ID = '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.",
|
||||
"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 { AppMountParameters, CoreStart } from '../../../../src/core/public';
|
||||
import { ReportingExampleApp } from './components/app';
|
||||
import { SetupDeps, StartDeps } from './types';
|
||||
import { SetupDeps, StartDeps, MyForwardableState } from './types';
|
||||
|
||||
export const renderApp = (
|
||||
coreStart: CoreStart,
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,10 @@ import {
|
|||
EuiPopover,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiCodeBlock,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
@ -29,11 +32,18 @@ import * as Rx from 'rxjs';
|
|||
import { takeWhile } from 'rxjs/operators';
|
||||
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/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 {
|
||||
basename: string;
|
||||
reporting: ReportingStart;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
forwardedParams?: MyForwardableState;
|
||||
}
|
||||
|
||||
const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana'];
|
||||
|
@ -42,8 +52,12 @@ export const ReportingExampleApp = ({
|
|||
basename,
|
||||
reporting,
|
||||
screenshotMode,
|
||||
forwardedParams,
|
||||
}: ReportingExampleAppProps) => {
|
||||
const { getDefaultLayoutSelectors } = reporting;
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('forwardedParams', forwardedParams);
|
||||
}, [forwardedParams]);
|
||||
|
||||
// Context Menu
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
@ -70,7 +84,6 @@ export const ReportingExampleApp = ({
|
|||
return {
|
||||
layout: {
|
||||
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||
selectors: getDefaultLayoutSelectors(),
|
||||
},
|
||||
relativeUrls: ['/app/reportingExample#/intended-visualization'],
|
||||
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 = [
|
||||
{ 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,
|
||||
initialFocusedItemIndex: 1,
|
||||
title: 'PDF Reports',
|
||||
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 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
initialFocusedItemIndex: 0,
|
||||
title: 'PNG Reports',
|
||||
items: [{ name: 'Default layout V2', icon: 'document', panel: 5 }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'No Layout Option',
|
||||
title: 'Default layout',
|
||||
content: (
|
||||
<reporting.components.ReportingPanelPDF
|
||||
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 (
|
||||
|
@ -124,9 +202,11 @@ export const ReportingExampleApp = ({
|
|||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>
|
||||
<EuiTitle>
|
||||
<h2>Example of a Sharing menu using components from Reporting</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiText>
|
||||
<p>Example of a Sharing menu using components from Reporting</p>
|
||||
|
||||
<EuiPopover
|
||||
id="contextMenuExample"
|
||||
button={<EuiButton onClick={onButtonClick}>Share</EuiButton>}
|
||||
|
@ -140,8 +220,29 @@ export const ReportingExampleApp = ({
|
|||
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<div data-shared-items-container data-shared-items-count="4">
|
||||
<div data-shared-items-container data-shared-items-count="5">
|
||||
<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) => (
|
||||
<EuiFlexItem key={index} data-shared-item>
|
||||
<EuiCard
|
||||
|
|
|
@ -12,11 +12,11 @@ import {
|
|||
CoreStart,
|
||||
Plugin,
|
||||
} from '../../../../src/core/public';
|
||||
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
|
||||
import { SetupDeps, StartDeps } from './types';
|
||||
import { PLUGIN_ID, PLUGIN_NAME, ReportingExampleLocatorDefinition } from '../common';
|
||||
import { SetupDeps, StartDeps, MyForwardableState } from './types';
|
||||
|
||||
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({
|
||||
id: PLUGIN_ID,
|
||||
title: PLUGIN_NAME,
|
||||
|
@ -30,7 +30,12 @@ export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
|
|||
unknown
|
||||
];
|
||||
// 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',
|
||||
description: 'Demonstrate how to put an Export button on a page and generate reports.',
|
||||
});
|
||||
|
||||
share.url.locators.create(new ReportingExampleLocatorDefinition());
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
|
||||
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public';
|
||||
import { SharePluginSetup } from 'src/plugins/share/public';
|
||||
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
|
||||
import { ReportingStart } from '../../../plugins/reporting/public';
|
||||
|
||||
|
@ -17,9 +18,12 @@ export interface PluginStart {}
|
|||
|
||||
export interface SetupDeps {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
share: SharePluginSetup;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
}
|
||||
export interface StartDeps {
|
||||
navigation: NavigationPublicPluginStart;
|
||||
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 PDF_REPORT_TYPE = 'printablePdf';
|
||||
export const PDF_REPORT_TYPE_V2 = 'printablePdfV2';
|
||||
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_V2 = 'pngV2';
|
||||
export const PNG_JOB_TYPE = 'PNG';
|
||||
export const PNG_JOB_TYPE_V2 = 'PNGV2';
|
||||
|
||||
export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate';
|
||||
|
||||
|
@ -98,6 +102,20 @@ export const ILM_POLICY_NAME = 'kibana-reporting';
|
|||
// Management UI route
|
||||
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
|
||||
export enum JOB_STATUSES {
|
||||
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.
|
||||
*/
|
||||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
export interface PageSizeParams {
|
||||
pageMarginTop: 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.
|
||||
payload: {
|
||||
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
|
||||
} & BaseParams;
|
||||
meta: { objectType: string; layout?: string }; // for telemetry
|
||||
|
@ -169,8 +176,21 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink;
|
|||
type ManagementLink = string;
|
||||
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 interface IlmPolicyStatusResponse {
|
||||
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);
|
||||
|
||||
export const InternalApiClientClientProvider: FunctionComponent<{
|
||||
export const InternalApiClientProvider: FunctionComponent<{
|
||||
apiClient: ReportingAPIClient;
|
||||
}> = ({ apiClient, children }) => {
|
||||
const {
|
||||
|
|
|
@ -9,4 +9,4 @@ export * from './reporting_api_client';
|
|||
|
||||
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 { CoreSetup, CoreStart } from 'src/core/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 { ClientConfigType } from '../plugin';
|
||||
import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports';
|
||||
|
@ -32,7 +32,7 @@ export async function mountManagementSection(
|
|||
<KibanaContextProvider
|
||||
services={{ http: coreSetup.http, application: coreStart.application }}
|
||||
>
|
||||
<InternalApiClientClientProvider apiClient={apiClient}>
|
||||
<InternalApiClientProvider apiClient={apiClient}>
|
||||
<IlmPolicyStatusContextProvider>
|
||||
<ReportListing
|
||||
toasts={coreSetup.notifications.toasts}
|
||||
|
@ -43,7 +43,7 @@ export async function mountManagementSection(
|
|||
urlService={urlService}
|
||||
/>
|
||||
</IlmPolicyStatusContextProvider>
|
||||
</InternalApiClientClientProvider>
|
||||
</InternalApiClientProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>,
|
||||
params.element
|
||||
|
|
|
@ -21,7 +21,7 @@ import type { ILicense } from '../../../licensing/public';
|
|||
import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types';
|
||||
import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context';
|
||||
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 { ListingProps as Props, ReportListing } from '.';
|
||||
|
||||
|
@ -84,7 +84,7 @@ describe('ReportListing', () => {
|
|||
const createTestBed = registerTestBed(
|
||||
(props?: Partial<Props>) => (
|
||||
<KibanaContextProvider services={{ http: httpService, application: applicationService }}>
|
||||
<InternalApiClientClientProvider apiClient={reportingAPIClient as ReportingAPIClient}>
|
||||
<InternalApiClientProvider apiClient={reportingAPIClient as ReportingAPIClient}>
|
||||
<IlmPolicyStatusContextProvider>
|
||||
<ReportListing
|
||||
license$={license$}
|
||||
|
@ -96,7 +96,7 @@ describe('ReportListing', () => {
|
|||
{...props}
|
||||
/>
|
||||
</IlmPolicyStatusContextProvider>
|
||||
</InternalApiClientClientProvider>
|
||||
</InternalApiClientProvider>
|
||||
</KibanaContextProvider>
|
||||
),
|
||||
{ memoryRouter: { wrapComponent: false } }
|
||||
|
|
|
@ -42,6 +42,7 @@ import type {
|
|||
} from './shared_imports';
|
||||
import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
|
||||
import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
|
||||
import { isRedirectAppPath } from './utils';
|
||||
|
||||
export interface ClientConfigType {
|
||||
poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } };
|
||||
|
@ -167,6 +168,15 @@ export class ReportingPublicPlugin
|
|||
title: this.title,
|
||||
order: 1,
|
||||
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 }]);
|
||||
const [[start], { mountManagementSection }] = await Promise.all([
|
||||
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 {
|
||||
title: string;
|
||||
layout: LayoutParams;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface JobParamsProviderOptions {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { ShareContext } from 'src/plugins/share/public';
|
||||
import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.';
|
||||
import { isJobV2Params } from '../../common/job_utils';
|
||||
import { checkLicense } from '../lib/license_check';
|
||||
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
|
||||
|
@ -16,11 +17,11 @@ import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy';
|
|||
const getJobParams = (
|
||||
apiClient: ReportingAPIClient,
|
||||
opts: JobParamsProviderOptions,
|
||||
type: 'pdf' | 'png'
|
||||
type: 'png' | 'pngV2' | 'printablePdf' | 'printablePdfV2'
|
||||
) => () => {
|
||||
const {
|
||||
objectType,
|
||||
sharingData: { title, layout },
|
||||
sharingData: { title, layout, locatorParams },
|
||||
} = opts;
|
||||
|
||||
const baseParams = {
|
||||
|
@ -29,6 +30,14 @@ const getJobParams = (
|
|||
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
|
||||
// Replace hashes with original RISON values.
|
||||
const relativeUrl = opts.shareableUrl.replace(
|
||||
|
@ -36,7 +45,7 @@ const getJobParams = (
|
|||
''
|
||||
);
|
||||
|
||||
if (type === 'pdf') {
|
||||
if (type === 'printablePdf') {
|
||||
// multi URL for PDF
|
||||
return { ...baseParams, relativeUrls: [relativeUrl] };
|
||||
}
|
||||
|
@ -111,6 +120,16 @@ export const reportingScreenshotShareProvider = ({
|
|||
defaultMessage: 'PNG Reports',
|
||||
});
|
||||
|
||||
const jobProviderOptions: JobParamsProviderOptions = {
|
||||
shareableUrl,
|
||||
objectType,
|
||||
sharingData,
|
||||
};
|
||||
|
||||
const isV2Job = isJobV2Params(jobProviderOptions);
|
||||
|
||||
const pngReportType = isV2Job ? 'pngV2' : 'png';
|
||||
|
||||
const panelPng = {
|
||||
shareMenuItem: {
|
||||
name: pngPanelTitle,
|
||||
|
@ -128,10 +147,10 @@ export const reportingScreenshotShareProvider = ({
|
|||
apiClient={apiClient}
|
||||
toasts={toasts}
|
||||
uiSettings={uiSettings}
|
||||
reportType="png"
|
||||
reportType={pngReportType}
|
||||
objectId={objectId}
|
||||
requiresSavedState={true}
|
||||
getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'png')}
|
||||
getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
@ -143,6 +162,8 @@ export const reportingScreenshotShareProvider = ({
|
|||
defaultMessage: 'PDF Reports',
|
||||
});
|
||||
|
||||
const pdfReportType = isV2Job ? 'printablePdfV2' : 'printablePdf';
|
||||
|
||||
const panelPdf = {
|
||||
shareMenuItem: {
|
||||
name: pdfPanelTitle,
|
||||
|
@ -160,11 +181,11 @@ export const reportingScreenshotShareProvider = ({
|
|||
apiClient={apiClient}
|
||||
toasts={toasts}
|
||||
uiSettings={uiSettings}
|
||||
reportType="printablePdf"
|
||||
reportType={pdfReportType}
|
||||
objectId={objectId}
|
||||
requiresSavedState={true}
|
||||
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
|
||||
getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'pdf')}
|
||||
getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
|
|
@ -21,7 +21,13 @@ import React, { Component, ReactElement } from 'react';
|
|||
import { ToastsSetup, IUiSettingsClient } from 'src/core/public';
|
||||
import url from 'url';
|
||||
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 { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
|
||||
|
@ -200,10 +206,12 @@ class ReportingPanelContentUi extends Component<Props, State> {
|
|||
private prettyPrintReportingType = () => {
|
||||
switch (this.props.reportType) {
|
||||
case PDF_REPORT_TYPE:
|
||||
case PDF_REPORT_TYPE_V2:
|
||||
return 'PDF';
|
||||
case 'csv_searchsource':
|
||||
return CSV_REPORT_TYPE;
|
||||
case 'png':
|
||||
case PNG_REPORT_TYPE_V2:
|
||||
return PNG_REPORT_TYPE;
|
||||
default:
|
||||
return this.props.reportType;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { CoreSetup } from 'kibana/public';
|
||||
import React from 'react';
|
||||
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 { ScreenCapturePanelContent } from '../share_context_menu/screen_capture_panel_content_lazy';
|
||||
|
||||
|
@ -16,7 +16,7 @@ interface IncludeOnCloseFn {
|
|||
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.
|
||||
|
@ -25,7 +25,7 @@ type PropsPDF = Pick<PanelPropsScreenCapture, 'getJobParams' | 'layoutOption'> &
|
|||
*/
|
||||
export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) {
|
||||
return {
|
||||
ReportingPanelPDF(props: PropsPDF) {
|
||||
ReportingPanelPDF(props: Props) {
|
||||
return (
|
||||
<ScreenCapturePanelContent
|
||||
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 puppeteer, { ElementHandle, EvaluateFn, SerializableOrJSHandle } from 'puppeteer';
|
||||
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 { ReportingCore } from '../../..';
|
||||
import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server';
|
||||
|
@ -94,10 +96,12 @@ export class HeadlessChromiumDriver {
|
|||
conditionalHeaders,
|
||||
waitForSelector: pageLoadSelector,
|
||||
timeout,
|
||||
locator,
|
||||
}: {
|
||||
conditionalHeaders: ConditionalHeaders;
|
||||
waitForSelector: string;
|
||||
timeout: number;
|
||||
locator?: LocatorParams;
|
||||
},
|
||||
logger: LevelLogger
|
||||
): Promise<void> {
|
||||
|
@ -106,8 +110,27 @@ export class HeadlessChromiumDriver {
|
|||
// Reset intercepted request count
|
||||
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);
|
||||
|
||||
this.registerListeners(conditionalHeaders, logger);
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import apm from 'elastic-apm-node';
|
||||
import * as Rx from 'rxjs';
|
||||
import { finalize, map, tap } from 'rxjs/operators';
|
||||
import { ReportingCore } from '../../../';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { LayoutParams, PreserveLayout } from '../../../lib/layouts';
|
||||
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
||||
import { ConditionalHeaders } from '../../common';
|
||||
import { ReportingCore } from '../../';
|
||||
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||
import { LevelLogger } from '../../lib';
|
||||
import { LayoutParams, PreserveLayout } from '../../lib/layouts';
|
||||
import { getScreenshots$, ScreenshotResults } from '../../lib/screenshots';
|
||||
import { ConditionalHeaders } from '../common';
|
||||
|
||||
function getBase64DecodedSize(value: string) {
|
||||
// @see https://en.wikipedia.org/wiki/Base64#Output_padding
|
||||
|
@ -30,7 +31,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
|||
|
||||
return function generatePngObservable(
|
||||
logger: LevelLogger,
|
||||
url: string,
|
||||
urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple,
|
||||
browserTimezone: string | undefined,
|
||||
conditionalHeaders: ConditionalHeaders,
|
||||
layoutParams: LayoutParams
|
||||
|
@ -47,7 +48,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
|||
let apmBuffer: typeof apm.currentSpan;
|
||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||
logger,
|
||||
urls: [url],
|
||||
urlsOrUrlLocatorTuples: [urlOrUrlLocatorTuple],
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone,
|
|
@ -5,14 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReportingCore } from '../../../';
|
||||
import { ReportingCore } from '../..';
|
||||
import {
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../../test_helpers';
|
||||
import { getConditionalHeaders } from '../../common';
|
||||
} from '../../test_helpers';
|
||||
import { getConditionalHeaders } from '.';
|
||||
import { getCustomLogo } from './get_custom_logo';
|
||||
|
||||
let mockReportingPlugin: ReportingCore;
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReportingCore } from '../../../';
|
||||
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { ConditionalHeaders } from '../../common';
|
||||
import { ReportingCore } from '../../';
|
||||
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants';
|
||||
import { LevelLogger } from '../../lib';
|
||||
import { ConditionalHeaders } from '../common';
|
||||
|
||||
export const getCustomLogo = async (
|
||||
reporting: ReportingCore,
|
|
@ -10,6 +10,9 @@ export { getConditionalHeaders } from './get_conditional_headers';
|
|||
export { getFullUrls } from './get_full_urls';
|
||||
export { omitBlockedHeaders } from './omit_blocked_headers';
|
||||
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 {
|
||||
min?: Date | string | number | null;
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
StyleDictionary,
|
||||
TDocumentDefinitions,
|
||||
} from 'pdfmake/interfaces';
|
||||
import { LayoutInstance } from '../../../../lib/layouts';
|
||||
import { LayoutInstance } from '../../../lib/layouts';
|
||||
import { REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||
import { getFont } from './get_font';
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PreserveLayout, PrintLayout } from '../../../../lib/layouts';
|
||||
import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers';
|
||||
import { PreserveLayout, PrintLayout } from '../../../lib/layouts';
|
||||
import { createMockConfig, createMockConfigSchema } from '../../../test_helpers';
|
||||
import { PdfMaker } from './';
|
||||
|
||||
const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`;
|
|
@ -12,12 +12,12 @@ import _ from 'lodash';
|
|||
import path from 'path';
|
||||
import Printer from 'pdfmake';
|
||||
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 { getFont } from './get_font';
|
||||
import { getTemplate } from './get_template';
|
||||
|
||||
const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets');
|
||||
const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets');
|
||||
const tableBorderWidth = 1;
|
||||
|
||||
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,
|
||||
createMockReportingCore,
|
||||
} from '../../../test_helpers';
|
||||
import { generatePngObservableFactory } from '../lib/generate_png';
|
||||
import { generatePngObservableFactory } from '../../common';
|
||||
import { TaskPayloadPNG } from '../types';
|
||||
import { runTaskFnFactory } from './';
|
||||
|
||||
jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
||||
jest.mock('../../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
||||
|
||||
let content: string;
|
||||
let mockReporting: ReportingCore;
|
||||
|
|
|
@ -16,8 +16,8 @@ import {
|
|||
getConditionalHeaders,
|
||||
getFullUrls,
|
||||
omitBlockedHeaders,
|
||||
generatePngObservableFactory,
|
||||
} from '../../common';
|
||||
import { generatePngObservableFactory } from '../lib/generate_png';
|
||||
import { TaskPayloadPNG } from '../types';
|
||||
|
||||
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,
|
||||
getFullUrls,
|
||||
omitBlockedHeaders,
|
||||
getCustomLogo,
|
||||
} from '../../common';
|
||||
import { generatePdfObservableFactory } from '../lib/generate_pdf';
|
||||
import { getCustomLogo } from '../lib/get_custom_logo';
|
||||
import { TaskPayloadPDF } from '../types';
|
||||
|
||||
export const runTaskFnFactory: RunTaskFnFactory<
|
||||
|
|
|
@ -13,7 +13,7 @@ import { LevelLogger } from '../../../lib';
|
|||
import { createLayout, LayoutParams } from '../../../lib/layouts';
|
||||
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
||||
import { ConditionalHeaders } from '../../common';
|
||||
import { PdfMaker } from './pdf';
|
||||
import { PdfMaker } from '../../common/pdf';
|
||||
import { getTracker } from './tracker';
|
||||
|
||||
const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
||||
|
@ -50,7 +50,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
|||
tracker.startScreenshots();
|
||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||
logger,
|
||||
urls,
|
||||
urlsOrUrlLocatorTuples: urls,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { BaseParams, BasePayload } from '../../types';
|
|||
interface BaseParamsPDF {
|
||||
layout: LayoutParams;
|
||||
forceNow?: string;
|
||||
// TODO: Add comment explaining this field
|
||||
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,
|
||||
ReportingStartDeps as PluginStart,
|
||||
} from './types';
|
||||
|
||||
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 getTypeCsv } from '../export_types/csv_searchsource';
|
||||
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 getTypePrintablePdfV2 } from '../export_types/printable_pdf_v2';
|
||||
|
||||
import { CreateJobFn, ExportTypeDefinition } from '../types';
|
||||
|
||||
type GetCallbackFn = (item: ExportTypeDefinition) => boolean;
|
||||
|
@ -88,7 +91,9 @@ export function getExportTypesRegistry(): ExportTypesRegistry {
|
|||
getTypeCsvDeprecated,
|
||||
getTypeCsvFromSavedObject,
|
||||
getTypePng,
|
||||
getTypePngV2,
|
||||
getTypePrintablePdf,
|
||||
getTypePrintablePdfV2,
|
||||
];
|
||||
getTypeFns.forEach((getType) => {
|
||||
registry.register(getType());
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { LevelLogger } from '../';
|
||||
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||
import { ConditionalHeaders } from '../../export_types/common';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
|
||||
|
@ -13,7 +14,7 @@ export { getScreenshots$ } from './observable';
|
|||
|
||||
export interface ScreenshotObservableOpts {
|
||||
logger: LevelLogger;
|
||||
urls: string[];
|
||||
urlsOrUrlLocatorTuples: UrlOrUrlLocatorTuple[];
|
||||
conditionalHeaders: ConditionalHeaders;
|
||||
layout: LayoutInstance;
|
||||
browserTimezone?: string;
|
||||
|
|
|
@ -69,7 +69,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
it('pipelines a single url into screenshot and timeRange', async () => {
|
||||
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urls: ['/welcome/home/start/index.htm'],
|
||||
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.htm'],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
|
@ -129,7 +129,10 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
// test
|
||||
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
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,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
|
@ -228,7 +231,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
const getScreenshot = async () => {
|
||||
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urls: [
|
||||
urlsOrUrlLocatorTuples: [
|
||||
'/welcome/home/start/index2.htm',
|
||||
'/welcome/home/start/index.php3?page=./home.php3',
|
||||
],
|
||||
|
@ -322,7 +325,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
const getScreenshot = async () => {
|
||||
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
|
@ -352,7 +355,7 @@ describe('Screenshot Observable Pipeline', () => {
|
|||
|
||||
const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
|
|
|
@ -34,7 +34,13 @@ interface ScreenSetupData {
|
|||
export function getScreenshots$(
|
||||
captureConfig: CaptureConfig,
|
||||
browserDriverFactory: HeadlessChromiumDriverFactory,
|
||||
{ logger, urls, conditionalHeaders, layout, browserTimezone }: ScreenshotObservableOpts
|
||||
{
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone,
|
||||
}: ScreenshotObservableOpts
|
||||
): Rx.Observable<ScreenshotResults[]> {
|
||||
const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting');
|
||||
|
||||
|
@ -49,8 +55,8 @@ export function getScreenshots$(
|
|||
apmCreatePage?.end();
|
||||
exit$.subscribe({ error: () => apmTrans?.end() });
|
||||
|
||||
return Rx.from(urls).pipe(
|
||||
concatMap((url, index) => {
|
||||
return Rx.from(urlsOrUrlLocatorTuples).pipe(
|
||||
concatMap((urlOrUrlLocatorTuple, index) => {
|
||||
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
|
||||
mergeMap(() => {
|
||||
// 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(
|
||||
captureConfig,
|
||||
driver,
|
||||
url,
|
||||
urlOrUrlLocatorTuple,
|
||||
pageLoadSelector,
|
||||
conditionalHeaders,
|
||||
logger
|
||||
|
@ -129,7 +135,7 @@ export function getScreenshots$(
|
|||
)
|
||||
);
|
||||
}),
|
||||
take(urls.length),
|
||||
take(urlsOrUrlLocatorTuples.length),
|
||||
toArray()
|
||||
);
|
||||
}),
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||
import { LevelLogger, startTrace } from '../';
|
||||
import { durationToNumber } from '../../../common/schema_utils';
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
|
@ -15,19 +16,24 @@ import { CaptureConfig } from '../../types';
|
|||
export const openUrl = async (
|
||||
captureConfig: CaptureConfig,
|
||||
browser: HeadlessChromiumDriver,
|
||||
url: string,
|
||||
pageLoadSelector: string,
|
||||
urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple,
|
||||
waitForSelector: string,
|
||||
conditionalHeaders: ConditionalHeaders,
|
||||
logger: LevelLogger
|
||||
): Promise<void> => {
|
||||
const endTrace = startTrace('open_url', 'wait');
|
||||
let url: string;
|
||||
let locator: undefined | LocatorParams;
|
||||
|
||||
if (typeof urlOrUrlLocatorTuple === 'string') {
|
||||
url = urlOrUrlLocatorTuple;
|
||||
} else {
|
||||
[url, locator] = urlOrUrlLocatorTuple;
|
||||
}
|
||||
|
||||
try {
|
||||
const timeout = durationToNumber(captureConfig.timeouts.openUrl);
|
||||
await browser.open(
|
||||
url,
|
||||
{ conditionalHeaders, waitForSelector: pageLoadSelector, timeout },
|
||||
logger
|
||||
);
|
||||
await browser.open(url, { conditionalHeaders, waitForSelector, timeout, locator }, logger);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
throw new Error(
|
||||
|
|
|
@ -33,11 +33,14 @@ export class ReportingPlugin
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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.
|
||||
core.http.registerRouteHandlerContext(PLUGIN_ID, () => {
|
||||
http.registerRouteHandlerContext(PLUGIN_ID, () => {
|
||||
if (reportingCore.pluginIsStarted()) {
|
||||
return reportingCore.getContract();
|
||||
} 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 basePath = http.basePath;
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ import {
|
|||
import { registerDiagnoseScreenshot } from './screenshot';
|
||||
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>>;
|
||||
|
||||
|
|
|
@ -9,9 +9,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ReportingCore } from '../..';
|
||||
import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server';
|
||||
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 { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
|
||||
import { DiagnosticResponse } from './';
|
||||
|
|
|
@ -112,7 +112,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
|
|||
const result = await jobsQuery.get(user, docId);
|
||||
|
||||
if (!result) {
|
||||
throw Boom.notFound();
|
||||
return res.notFound();
|
||||
}
|
||||
|
||||
const { jobtype: jobType } = result;
|
||||
|
|
|
@ -30,11 +30,11 @@ import { ReportTaskParams } from './lib/tasks';
|
|||
export interface ReportingSetupDeps {
|
||||
licensing: LicensingPluginSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
}
|
||||
|
||||
export interface ReportingStartDeps {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue