mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Reporting/PDF] Layout option for generating full-page Canvas reports (#84959)
* [Reporting/PDF] Custom layout option for Canvas * fix snapshots * --wip-- [skip ci] * check pdf data * add test * functional tests work * add fixme comment * read strings from pdf for test * Update reports.ts * function name / comment improvment * Add Canvas toggle to choose pdf layout type * Fix Canvas pdf panel storybook test * Update style for new Canvas report type switch * Update canvas share menu snapshot * Fix tests for validating Canvas PDF using inline snapshots Run test server with: node scripts/functional_tests_server.js --config x-pack/test/functional/config.js Run test suite with: node scripts/functional_test_runner.js --config x-pack/test/functional/config.js --grep 'Canvas PDF Report' * Fix i18n and typo * Add a test for removing borders * Fix i18n * Update snapshot Co-authored-by: Timothy Sullivan <tsullivan@elastic.co> Co-authored-by: Tim Sullivan <tsullivan@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fbe48221ae
commit
cda3627a79
25 changed files with 3354 additions and 206 deletions
|
@ -1420,6 +1420,18 @@ export const ComponentStrings = {
|
|||
workpadName,
|
||||
},
|
||||
}),
|
||||
getPDFFullPageLayoutHelpText: () =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.FullPageLayoutHelpText', {
|
||||
defaultMessage: 'Remove borders and footer logo',
|
||||
}),
|
||||
getPDFFullPageLayoutLabel: () =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.FullPageLayoutLabel', {
|
||||
defaultMessage: 'Full page layout',
|
||||
}),
|
||||
getPDFPanelAdvancedOptionsLabel: () =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelAdvancedOptionsLabel', {
|
||||
defaultMessage: 'Advanced options',
|
||||
}),
|
||||
getPDFPanelCopyAriaLabel: () =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel', {
|
||||
defaultMessage:
|
||||
|
@ -1462,6 +1474,10 @@ export const ComponentStrings = {
|
|||
PDF,
|
||||
},
|
||||
}),
|
||||
getPDFPanelOptionsLabel: () =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelOptionsLabel', {
|
||||
defaultMessage: 'Options',
|
||||
}),
|
||||
getShareableZipErrorTitle: (workpadName: string) =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', {
|
||||
defaultMessage:
|
||||
|
|
|
@ -17,8 +17,78 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
|
|||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
<h6
|
||||
className="euiTitle euiTitle--xxsmall"
|
||||
>
|
||||
Options
|
||||
</h6>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
<div
|
||||
className="euiFormRow"
|
||||
id="generated-id-row"
|
||||
>
|
||||
<div
|
||||
className="euiFormRow__fieldWrapper"
|
||||
>
|
||||
<div
|
||||
className="euiSwitch"
|
||||
>
|
||||
<button
|
||||
aria-checked={false}
|
||||
aria-describedby="generated-id-help"
|
||||
aria-labelledby="generated-id"
|
||||
className="euiSwitch__button"
|
||||
data-test-subj="reportModeToggle"
|
||||
id="generated-id"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="euiSwitch__body"
|
||||
>
|
||||
<span
|
||||
className="euiSwitch__thumb"
|
||||
/>
|
||||
<span
|
||||
className="euiSwitch__track"
|
||||
>
|
||||
<span
|
||||
className="euiSwitch__icon"
|
||||
data-euiicon-type="cross"
|
||||
size="m"
|
||||
/>
|
||||
<span
|
||||
className="euiSwitch__icon euiSwitch__icon--checked"
|
||||
data-euiicon-type="check"
|
||||
size="m"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
className="euiSwitch__label"
|
||||
id="generated-id"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Full page layout
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="euiFormHelpText euiFormRow__text"
|
||||
id="generated-id-help"
|
||||
>
|
||||
Remove borders and footer logo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="euiButton euiButton--primary euiButton--small euiButton--fill"
|
||||
data-test-subj="generateReportButton"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
|
@ -38,52 +108,50 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
|
|||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
<div
|
||||
className="euiText euiText--small"
|
||||
>
|
||||
<p>
|
||||
Alternatively, copy this POST URL to call generation from outside Kibana or from Watcher.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
<div
|
||||
className="canvasClipboard"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<button
|
||||
aria-label="Alternatively, you can generate a PDF from a script or with Watcher by using this URL. Press Enter to copy the URL to clipboard."
|
||||
className="euiButton euiButton--primary euiButton--small"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": undefined,
|
||||
"width": "100%",
|
||||
}
|
||||
<hr
|
||||
className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall"
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": "-16px",
|
||||
"marginRight": "-16px",
|
||||
"width": "auto",
|
||||
}
|
||||
type="button"
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="euiAccordion"
|
||||
>
|
||||
<div
|
||||
className="euiAccordion__triggerWrapper"
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent euiButton__content"
|
||||
<button
|
||||
aria-controls="advanced-options"
|
||||
aria-expanded={false}
|
||||
className="euiAccordion__button"
|
||||
id="generated-id"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent__icon"
|
||||
data-euiicon-type="copy"
|
||||
size="m"
|
||||
/>
|
||||
<span
|
||||
className="euiButton__text"
|
||||
className="euiAccordion__iconWrapper"
|
||||
>
|
||||
Copy POST URL
|
||||
<span
|
||||
className="euiAccordion__icon"
|
||||
data-euiicon-type="arrowRight"
|
||||
size="m"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
className="euiIEFlexWrapFix"
|
||||
>
|
||||
Advanced options
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="euiAccordion__childWrapper"
|
||||
id="advanced-options"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu default 1`] = `
|
|||
<button
|
||||
aria-label="Share this workpad"
|
||||
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
|
||||
data-test-subj="shareTopNavButton"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
|
|
|
@ -25,6 +25,10 @@ storiesOf('components/WorkpadHeader/ShareMenu/PDFPanel', module)
|
|||
})
|
||||
.add('default', () => (
|
||||
<div className="euiPanel">
|
||||
<PDFPanel pdfURL="pdfUrl" onCopy={action('onCopy')} onExport={action('onExport')} />
|
||||
<PDFPanel
|
||||
getPdfURL={() => 'PDF URL String'}
|
||||
onCopy={action('onCopy')}
|
||||
onExport={action('onExport')}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
|
|
@ -4,18 +4,28 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { Clipboard } from '../../clipboard';
|
||||
import { LayoutType } from './utils';
|
||||
|
||||
import { ComponentStrings } from '../../../../i18n/components';
|
||||
const { WorkpadHeaderShareMenu: strings } = ComponentStrings;
|
||||
|
||||
interface Props {
|
||||
/** The URL that will invoke PDF Report generation. */
|
||||
pdfURL: string;
|
||||
/** Retrieve URL that will invoke PDF Report generation. */
|
||||
getPdfURL: (layout: LayoutType) => string;
|
||||
/** Handler to invoke when the PDF is exported */
|
||||
onExport: () => void;
|
||||
onExport: (layout: LayoutType) => void;
|
||||
/** Handler to invoke when the URL is copied to the clipboard. */
|
||||
onCopy: () => void;
|
||||
}
|
||||
|
@ -23,29 +33,65 @@ interface Props {
|
|||
/**
|
||||
* A panel displayed in the Export Menu with options in which to generate PDF Reports.
|
||||
*/
|
||||
export const PDFPanel = ({ pdfURL, onExport, onCopy }: Props) => (
|
||||
<div className="canvasShareMenu__panelContent">
|
||||
<EuiText size="s">
|
||||
<p>{strings.getPDFPanelGenerateDescription()}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiButton fill onClick={onExport} size="s" style={{ width: '100%' }}>
|
||||
{strings.getPDFPanelGenerateButtonLabel()}
|
||||
</EuiButton>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<p>{strings.getPDFPanelCopyDescription()}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<Clipboard content={pdfURL} onCopy={onCopy}>
|
||||
export const PDFPanel = ({ getPdfURL, onExport, onCopy }: Props) => {
|
||||
const [reportLayout, setReportLayout] = useState<LayoutType>('preserve_layout');
|
||||
|
||||
return (
|
||||
<div className="canvasShareMenu__panelContent">
|
||||
<EuiText size="s">
|
||||
<p>{strings.getPDFPanelGenerateDescription()}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTitle size="xxs">
|
||||
<h6>{strings.getPDFPanelOptionsLabel()}</h6>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow helpText={strings.getPDFFullPageLayoutHelpText()}>
|
||||
<EuiSwitch
|
||||
label={strings.getPDFFullPageLayoutLabel()}
|
||||
checked={reportLayout === 'canvas'}
|
||||
onChange={() =>
|
||||
reportLayout === 'canvas'
|
||||
? setReportLayout('preserve_layout')
|
||||
: setReportLayout('canvas')
|
||||
}
|
||||
data-test-subj="reportModeToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiButton
|
||||
iconType="copy"
|
||||
fill
|
||||
onClick={() => onExport(reportLayout)}
|
||||
size="s"
|
||||
style={{ width: '100%' }}
|
||||
aria-label={strings.getPDFPanelCopyAriaLabel()}
|
||||
data-test-subj="generateReportButton"
|
||||
>
|
||||
{strings.getPDFPanelCopyButtonLabel()}
|
||||
{strings.getPDFPanelGenerateButtonLabel()}
|
||||
</EuiButton>
|
||||
</Clipboard>
|
||||
</div>
|
||||
);
|
||||
<EuiHorizontalRule
|
||||
margin="s"
|
||||
style={{ width: 'auto', marginLeft: '-16px', marginRight: '-16px' }}
|
||||
/>
|
||||
<EuiAccordion
|
||||
id="advanced-options"
|
||||
buttonContent={strings.getPDFPanelAdvancedOptionsLabel()}
|
||||
paddingSize="none"
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<p>{strings.getPDFPanelCopyDescription()}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<Clipboard content={getPdfURL(reportLayout)} onCopy={onCopy}>
|
||||
<EuiButton
|
||||
iconType="copy"
|
||||
size="s"
|
||||
style={{ width: '100%' }}
|
||||
aria-label={strings.getPDFPanelCopyAriaLabel()}
|
||||
>
|
||||
{strings.getPDFPanelCopyButtonLabel()}
|
||||
</EuiButton>
|
||||
</Clipboard>
|
||||
</EuiAccordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
|
|||
import { Popover, ClosePopoverFn } from '../../popover';
|
||||
import { PDFPanel } from './pdf_panel';
|
||||
import { ShareWebsiteFlyout } from './flyout';
|
||||
import { LayoutType } from './utils';
|
||||
|
||||
const { WorkpadHeaderShareMenu: strings } = ComponentStrings;
|
||||
|
||||
|
@ -21,9 +22,9 @@ type ExportUrlTypes = 'pdf';
|
|||
type CloseTypes = 'share';
|
||||
|
||||
export type OnCopyFn = (type: CopyTypes) => void;
|
||||
export type OnExportFn = (type: ExportTypes) => void;
|
||||
export type OnExportFn = (type: ExportTypes, layout?: LayoutType) => void;
|
||||
export type OnCloseFn = (type: CloseTypes) => void;
|
||||
export type GetExportUrlFn = (type: ExportUrlTypes) => string;
|
||||
export type GetExportUrlFn = (type: ExportUrlTypes, layout: LayoutType) => string;
|
||||
|
||||
export interface Props {
|
||||
/** Handler to invoke when an export URL is copied to the clipboard. */
|
||||
|
@ -47,9 +48,9 @@ export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExpor
|
|||
const getPDFPanel = (closePopover: ClosePopoverFn) => {
|
||||
return (
|
||||
<PDFPanel
|
||||
pdfURL={getExportUrl('pdf')}
|
||||
onExport={() => {
|
||||
onExport('pdf');
|
||||
getPdfURL={(layoutType: LayoutType) => getExportUrl('pdf', layoutType)}
|
||||
onExport={(layoutType) => {
|
||||
onExport('pdf', layoutType);
|
||||
closePopover();
|
||||
}}
|
||||
onCopy={() => {
|
||||
|
@ -79,6 +80,7 @@ export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExpor
|
|||
title: strings.getShareDownloadPDFTitle(),
|
||||
content: getPDFPanel(closePopover),
|
||||
},
|
||||
'data-test-subj': 'sharePanel-PDFReports',
|
||||
},
|
||||
{
|
||||
name: strings.getShareWebsiteTitle(),
|
||||
|
@ -92,7 +94,12 @@ export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExpor
|
|||
});
|
||||
|
||||
const shareControl = (togglePopover: React.MouseEventHandler<any>) => (
|
||||
<EuiButtonEmpty size="xs" aria-label={strings.getShareWorkpadMessage()} onClick={togglePopover}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
aria-label={strings.getShareWorkpadMessage()}
|
||||
onClick={togglePopover}
|
||||
data-test-subj="shareTopNavButton"
|
||||
>
|
||||
{strings.getShareMenuButtonLabel()}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
|
|
@ -45,10 +45,11 @@ export const ShareMenu = compose<ComponentProps, {}>(
|
|||
withServices,
|
||||
withProps(
|
||||
({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({
|
||||
getExportUrl: (type) => {
|
||||
getExportUrl: (type, layout) => {
|
||||
if (type === 'pdf') {
|
||||
const pdfUrl = getPdfUrl(
|
||||
workpad,
|
||||
layout,
|
||||
{ pageCount },
|
||||
services.platform.getBasePathInterface()
|
||||
);
|
||||
|
@ -69,10 +70,15 @@ export const ShareMenu = compose<ComponentProps, {}>(
|
|||
throw new Error(strings.getUnknownExportErrorMessage(type));
|
||||
}
|
||||
},
|
||||
onExport: (type) => {
|
||||
onExport: (type, layout) => {
|
||||
switch (type) {
|
||||
case 'pdf':
|
||||
return createPdf(workpad, { pageCount }, services.platform.getBasePathInterface())
|
||||
return createPdf(
|
||||
workpad,
|
||||
layout || 'preserve_layout',
|
||||
{ pageCount },
|
||||
services.platform.getBasePathInterface()
|
||||
)
|
||||
.then(({ data }: { data: { job: { id: string } } }) => {
|
||||
services.notify.info(strings.getExportPDFMessage(), {
|
||||
title: strings.getExportPDFTitle(workpad.name),
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
jest.mock('../../../../common/lib/fetch');
|
||||
|
||||
import { getPdfUrl, createPdf } from './utils';
|
||||
import { getPdfUrl, createPdf, LayoutType } from './utils';
|
||||
import { workpads } from '../../../../__tests__/fixtures/workpads';
|
||||
import { fetch } from '../../../../common/lib/fetch';
|
||||
import { IBasePath } from 'kibana/public';
|
||||
|
@ -18,25 +18,31 @@ const basePath = ({
|
|||
} as unknown) as IBasePath;
|
||||
const workpad = workpads[0];
|
||||
|
||||
test('getPdfUrl returns the correct url', () => {
|
||||
const url = getPdfUrl(workpad, { pageCount: 2 }, basePath);
|
||||
test('getPdfUrl returns the correct url for canvas layout', () => {
|
||||
['canvas', 'preserve_layout'].forEach((layout) => {
|
||||
const url = getPdfUrl(workpad, layout as LayoutType, { pageCount: 2 }, basePath);
|
||||
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"basepath/s/spacey//api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FPhoenix,layout:(dimensions:(height:0,width:0),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F1,%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F2),title:'base%20workpad')"`
|
||||
);
|
||||
expect(url).toMatchInlineSnapshot(
|
||||
`"basepath/s/spacey//api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(dimensions:(height:0,width:0),id:${layout}),objectType:'canvas%20workpad',relativeUrls:!(%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F1,%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F2),title:'base%20workpad')"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('createPdf posts to create the pdf', () => {
|
||||
createPdf(workpad, { pageCount: 2 }, basePath);
|
||||
test('createPdf posts to create the pdf with canvas layout', () => {
|
||||
['canvas', 'preserve_layout'].forEach((layout, index) => {
|
||||
createPdf(workpad, layout as LayoutType, { pageCount: 2 }, basePath);
|
||||
|
||||
expect(fetch.post).toBeCalled();
|
||||
expect(fetch.post).toBeCalled();
|
||||
|
||||
const args = (fetch.post as jest.MockedFunction<typeof fetch.post>).mock.calls[0];
|
||||
const args = (fetch.post as jest.MockedFunction<typeof fetch.post>).mock.calls[index];
|
||||
|
||||
expect(args[0]).toMatchInlineSnapshot(`"basepath/s/spacey//api/reporting/generate/printablePdf"`);
|
||||
expect(args[1]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"jobParams": "(browserTimezone:America/Phoenix,layout:(dimensions:(height:0,width:0),id:preserve_layout),objectType:'canvas workpad',relativeUrls:!(/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/1,/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/2),title:'base workpad')",
|
||||
}
|
||||
`);
|
||||
expect(args[0]).toMatchInlineSnapshot(
|
||||
`"basepath/s/spacey//api/reporting/generate/printablePdf"`
|
||||
);
|
||||
expect(args[1]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"jobParams": "(browserTimezone:America/New_York,layout:(dimensions:(height:0,width:0),id:${layout}),objectType:'canvas workpad',relativeUrls:!(/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/1,/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/2),title:'base workpad')",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,18 +6,18 @@
|
|||
|
||||
import rison from 'rison-node';
|
||||
import { IBasePath } from 'kibana/public';
|
||||
import moment from 'moment-timezone';
|
||||
import { fetch } from '../../../../common/lib/fetch';
|
||||
import { CanvasWorkpad } from '../../../../types';
|
||||
import { url } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
// type of the desired pdf output (print or preserve_layout)
|
||||
const PDF_LAYOUT_TYPE = 'preserve_layout';
|
||||
|
||||
interface PageCount {
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
type Arguments = [CanvasWorkpad, PageCount, IBasePath];
|
||||
export type LayoutType = 'canvas' | 'preserve_layout';
|
||||
|
||||
type Arguments = [CanvasWorkpad, LayoutType, PageCount, IBasePath];
|
||||
|
||||
interface PdfUrlData {
|
||||
createPdfUri: string;
|
||||
|
@ -26,6 +26,7 @@ interface PdfUrlData {
|
|||
|
||||
function getPdfUrlParts(
|
||||
{ id, name: title, width, height }: CanvasWorkpad,
|
||||
layoutType: LayoutType,
|
||||
{ pageCount }: PageCount,
|
||||
basePath: IBasePath
|
||||
): PdfUrlData {
|
||||
|
@ -50,10 +51,10 @@ function getPdfUrlParts(
|
|||
}
|
||||
|
||||
const jobParams = {
|
||||
browserTimezone: 'America/Phoenix', // TODO: get browser timezone, or Kibana setting?
|
||||
browserTimezone: moment.tz.guess(),
|
||||
layout: {
|
||||
dimensions: { width, height },
|
||||
id: PDF_LAYOUT_TYPE,
|
||||
id: layoutType,
|
||||
},
|
||||
objectType: 'canvas workpad',
|
||||
relativeUrls: workpadUrls,
|
||||
|
|
|
@ -80,7 +80,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
|||
let buffer: Buffer | null = null;
|
||||
try {
|
||||
tracker.startCompile();
|
||||
logger.debug(`Compiling PDF...`);
|
||||
logger.debug(`Compiling PDF using "${layout.id}" layout...`);
|
||||
pdfOutput.generate();
|
||||
tracker.endCompile();
|
||||
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
|
||||
import { BufferOptions } from 'pdfmake/interfaces';
|
||||
|
||||
export const REPORTING_TABLE_LAYOUT = 'noBorder';
|
||||
|
||||
export function getDocOptions(tableBorderWidth: number): BufferOptions {
|
||||
return {
|
||||
tableLayouts: {
|
||||
noBorder: {
|
||||
[REPORTING_TABLE_LAYOUT]: {
|
||||
// format is function (i, node) { ... };
|
||||
hLineWidth: () => 0,
|
||||
vLineWidth: () => 0,
|
||||
|
@ -18,17 +20,6 @@ export function getDocOptions(tableBorderWidth: number): BufferOptions {
|
|||
paddingTop: () => 0,
|
||||
paddingBottom: () => 0,
|
||||
},
|
||||
simpleBorder: {
|
||||
// format is function (i, node) { ... };
|
||||
hLineWidth: () => tableBorderWidth,
|
||||
vLineWidth: () => tableBorderWidth,
|
||||
hLineColor: () => 'silver',
|
||||
vLineColor: () => 'silver',
|
||||
paddingLeft: () => 0,
|
||||
paddingRight: () => 0,
|
||||
paddingTop: () => 0,
|
||||
paddingBottom: () => 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,8 +6,14 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import path from 'path';
|
||||
import { TDocumentDefinitions } from 'pdfmake/interfaces';
|
||||
import {
|
||||
ContentText,
|
||||
DynamicContent,
|
||||
StyleDictionary,
|
||||
TDocumentDefinitions,
|
||||
} from 'pdfmake/interfaces';
|
||||
import { LayoutInstance } from '../../../../lib/layouts';
|
||||
import { REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||
import { getFont } from './get_font';
|
||||
|
||||
export function getTemplate(
|
||||
|
@ -29,6 +35,79 @@ export function getTemplate(
|
|||
const subheadingMarginBottom = 5;
|
||||
const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom;
|
||||
|
||||
const getStyle = (): StyleDictionary => ({
|
||||
heading: {
|
||||
alignment: 'left',
|
||||
fontSize: headingFontSize,
|
||||
bold: true,
|
||||
margin: [headingMarginTop, 0, headingMarginBottom, 0],
|
||||
},
|
||||
subheading: {
|
||||
alignment: 'left',
|
||||
fontSize: subheadingFontSize,
|
||||
italics: true,
|
||||
margin: [0, 0, subheadingMarginBottom, 20],
|
||||
},
|
||||
warning: {
|
||||
color: '#f39c12', // same as @brand-warning in Kibana colors.less
|
||||
},
|
||||
});
|
||||
const getHeader = (): ContentText => ({
|
||||
margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0],
|
||||
text: title,
|
||||
font: getFont(title),
|
||||
style: {
|
||||
color: '#aaa',
|
||||
},
|
||||
fontSize: 10,
|
||||
alignment: 'center',
|
||||
});
|
||||
const getFooter = (): DynamicContent => (currentPage: number, pageCount: number) => {
|
||||
const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); // Default Elastic Logo
|
||||
return {
|
||||
margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0],
|
||||
layout: REPORTING_TABLE_LAYOUT,
|
||||
table: {
|
||||
widths: [100, '*', 100],
|
||||
body: [
|
||||
[
|
||||
{
|
||||
fit: [100, 35],
|
||||
image: logo || logoPath,
|
||||
},
|
||||
{
|
||||
alignment: 'center',
|
||||
text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', {
|
||||
defaultMessage: 'Page {currentPage} of {pageCount}',
|
||||
values: { currentPage: currentPage.toString(), pageCount },
|
||||
}),
|
||||
style: {
|
||||
color: '#aaa',
|
||||
},
|
||||
},
|
||||
'',
|
||||
],
|
||||
[
|
||||
logo
|
||||
? {
|
||||
text: i18n.translate('xpack.reporting.exportTypes.printablePdf.logoDescription', {
|
||||
defaultMessage: 'Powered by Elastic',
|
||||
}),
|
||||
fontSize: 10,
|
||||
style: {
|
||||
color: '#aaa',
|
||||
},
|
||||
margin: [0, 2, 0, 0],
|
||||
}
|
||||
: '',
|
||||
'',
|
||||
'',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// define page size
|
||||
pageOrientation: layout.getPdfPageOrientation(),
|
||||
|
@ -40,87 +119,14 @@ export function getTemplate(
|
|||
headingHeight,
|
||||
subheadingHeight,
|
||||
}),
|
||||
pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom],
|
||||
pageMargins: layout.useReportingBranding
|
||||
? [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom]
|
||||
: [0, 0, 0, 0],
|
||||
|
||||
header() {
|
||||
return {
|
||||
margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0],
|
||||
text: title,
|
||||
font: getFont(title),
|
||||
style: {
|
||||
color: '#aaa',
|
||||
},
|
||||
fontSize: 10,
|
||||
alignment: 'center',
|
||||
};
|
||||
},
|
||||
header: layout.hasHeader ? getHeader() : undefined,
|
||||
footer: layout.hasFooter ? getFooter() : undefined,
|
||||
|
||||
footer(currentPage: number, pageCount: number) {
|
||||
const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); // Default Elastic Logo
|
||||
return {
|
||||
margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0],
|
||||
layout: 'noBorder',
|
||||
table: {
|
||||
widths: [100, '*', 100],
|
||||
body: [
|
||||
[
|
||||
{
|
||||
fit: [100, 35],
|
||||
image: logo || logoPath,
|
||||
},
|
||||
{
|
||||
alignment: 'center',
|
||||
text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', {
|
||||
defaultMessage: 'Page {currentPage} of {pageCount}',
|
||||
values: { currentPage: currentPage.toString(), pageCount },
|
||||
}),
|
||||
style: {
|
||||
color: '#aaa',
|
||||
},
|
||||
},
|
||||
'',
|
||||
],
|
||||
[
|
||||
logo
|
||||
? {
|
||||
text: i18n.translate(
|
||||
'xpack.reporting.exportTypes.printablePdf.logoDescription',
|
||||
{
|
||||
defaultMessage: 'Powered by Elastic',
|
||||
}
|
||||
),
|
||||
fontSize: 10,
|
||||
style: {
|
||||
color: '#aaa',
|
||||
},
|
||||
margin: [0, 2, 0, 0],
|
||||
}
|
||||
: '',
|
||||
'',
|
||||
'',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
styles: {
|
||||
heading: {
|
||||
alignment: 'left',
|
||||
fontSize: headingFontSize,
|
||||
bold: true,
|
||||
margin: [headingMarginTop, 0, headingMarginBottom, 0],
|
||||
},
|
||||
subheading: {
|
||||
alignment: 'left',
|
||||
fontSize: subheadingFontSize,
|
||||
italics: true,
|
||||
margin: [0, 0, subheadingMarginBottom, 20],
|
||||
},
|
||||
warning: {
|
||||
color: '#f39c12', // same as @brand-warning in Kibana colors.less
|
||||
},
|
||||
},
|
||||
styles: layout.useReportingBranding ? getStyle() : undefined,
|
||||
|
||||
defaultStyle: {
|
||||
fontSize: 12,
|
||||
|
|
|
@ -10,9 +10,9 @@ import concat from 'concat-stream';
|
|||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import Printer from 'pdfmake';
|
||||
import { Content, ContentText } from 'pdfmake/interfaces';
|
||||
import { Content, ContentImage, ContentText } from 'pdfmake/interfaces';
|
||||
import { LayoutInstance } from '../../../../lib/layouts';
|
||||
import { getDocOptions } from './get_doc_options';
|
||||
import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||
import { getFont } from './get_font';
|
||||
import { getTemplate } from './get_template';
|
||||
|
||||
|
@ -67,7 +67,7 @@ export class PdfMaker {
|
|||
this._content.push(contents);
|
||||
}
|
||||
|
||||
addImage(base64EncodedData: string, { title = '', description = '' }) {
|
||||
addBrandedImage(img: ContentImage, { title = '', description = '' }) {
|
||||
const contents: Content[] = [];
|
||||
|
||||
if (title && title.length > 0) {
|
||||
|
@ -88,19 +88,11 @@ export class PdfMaker {
|
|||
});
|
||||
}
|
||||
|
||||
const size = this._layout.getPdfImageSize();
|
||||
const img = {
|
||||
image: `data:image/png;base64,${base64EncodedData}`,
|
||||
alignment: 'center',
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
};
|
||||
|
||||
const wrappedImg = {
|
||||
table: {
|
||||
body: [[img]],
|
||||
},
|
||||
layout: 'noBorder',
|
||||
layout: REPORTING_TABLE_LAYOUT,
|
||||
};
|
||||
|
||||
contents.push(wrappedImg);
|
||||
|
@ -108,6 +100,22 @@ export class PdfMaker {
|
|||
this._addContents(contents);
|
||||
}
|
||||
|
||||
addImage(base64EncodedData: string, opts = { title: '', description: '' }) {
|
||||
const size = this._layout.getPdfImageSize();
|
||||
const img = {
|
||||
image: `data:image/png;base64,${base64EncodedData}`,
|
||||
alignment: 'center' as 'center',
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
};
|
||||
|
||||
if (this._layout.useReportingBranding) {
|
||||
return this.addBrandedImage(img, opts);
|
||||
}
|
||||
|
||||
this._addContents([img]);
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this._title = title;
|
||||
}
|
||||
|
|
86
x-pack/plugins/reporting/server/lib/layouts/canvas_layout.ts
Normal file
86
x-pack/plugins/reporting/server/lib/layouts/canvas_layout.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
getDefaultLayoutSelectors,
|
||||
LayoutInstance,
|
||||
LayoutSelectorDictionary,
|
||||
LayoutTypes,
|
||||
PageSizeParams,
|
||||
Size,
|
||||
} from './';
|
||||
import { Layout } from './layout';
|
||||
|
||||
// FIXME - should use zoom from capture config
|
||||
const ZOOM: number = 2;
|
||||
|
||||
/*
|
||||
* This class provides a Layout definition. The PdfMaker class uses this to
|
||||
* define a document layout that includes no margins or branding or added logos.
|
||||
* The single image that was captured should be the only structural part of the
|
||||
* PDF document definition
|
||||
*/
|
||||
export class CanvasLayout extends Layout implements LayoutInstance {
|
||||
public readonly selectors: LayoutSelectorDictionary = getDefaultLayoutSelectors();
|
||||
public readonly groupCount = 1;
|
||||
public readonly height: number;
|
||||
public readonly width: number;
|
||||
private readonly scaledHeight: number;
|
||||
private readonly scaledWidth: number;
|
||||
|
||||
public hasHeader: boolean = false;
|
||||
public hasFooter: boolean = false;
|
||||
public useReportingBranding: boolean = false;
|
||||
|
||||
constructor(size: Size) {
|
||||
super(LayoutTypes.CANVAS);
|
||||
this.height = size.height;
|
||||
this.width = size.width;
|
||||
this.scaledHeight = size.height * ZOOM;
|
||||
this.scaledWidth = size.width * ZOOM;
|
||||
}
|
||||
|
||||
public getPdfPageOrientation() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getCssOverridesPath() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getBrowserViewport() {
|
||||
return {
|
||||
height: this.scaledHeight,
|
||||
width: this.scaledWidth,
|
||||
};
|
||||
}
|
||||
|
||||
public getBrowserZoom() {
|
||||
return ZOOM;
|
||||
}
|
||||
|
||||
public getViewport() {
|
||||
return {
|
||||
height: this.scaledHeight,
|
||||
width: this.scaledWidth,
|
||||
zoom: ZOOM,
|
||||
};
|
||||
}
|
||||
|
||||
public getPdfImageSize() {
|
||||
return {
|
||||
height: this.height,
|
||||
width: this.width,
|
||||
};
|
||||
}
|
||||
|
||||
public getPdfPageSize(pageSizeParams: PageSizeParams): Size {
|
||||
return {
|
||||
height: this.height,
|
||||
width: this.width,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ReportingConfig } from '../..';
|
||||
import { createMockConfig, createMockConfigSchema } from '../../test_helpers';
|
||||
import { createLayout, LayoutParams, PreserveLayout } from './';
|
||||
import { CanvasLayout } from './canvas_layout';
|
||||
|
||||
describe('Create Layout', () => {
|
||||
let config: ReportingConfig;
|
||||
beforeEach(() => {
|
||||
config = createMockConfig(createMockConfigSchema());
|
||||
});
|
||||
|
||||
it('creates preserve layout instance', () => {
|
||||
const { id, height, width } = new PreserveLayout({ width: 16, height: 16 });
|
||||
const preserveParams: LayoutParams = { id, dimensions: { height, width } };
|
||||
const layout = createLayout(config.get('capture'), preserveParams);
|
||||
expect(layout).toMatchInlineSnapshot(`
|
||||
PreserveLayout {
|
||||
"groupCount": 1,
|
||||
"hasFooter": true,
|
||||
"hasHeader": true,
|
||||
"height": 16,
|
||||
"id": "preserve_layout",
|
||||
"scaledHeight": 32,
|
||||
"scaledWidth": 32,
|
||||
"selectors": Object {
|
||||
"itemsCountAttribute": "data-shared-items-count",
|
||||
"renderComplete": "[data-shared-item]",
|
||||
"screenshot": "[data-shared-items-container]",
|
||||
"timefilterDurationAttribute": "data-shared-timefilter-duration",
|
||||
},
|
||||
"useReportingBranding": true,
|
||||
"width": 16,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the print layout', () => {
|
||||
const print = createLayout(config.get('capture'));
|
||||
const printParams: LayoutParams = {
|
||||
id: print.id,
|
||||
};
|
||||
const layout = createLayout(config.get('capture'), printParams);
|
||||
expect(layout).toMatchInlineSnapshot(`
|
||||
PrintLayout {
|
||||
"captureConfig": Object {
|
||||
"browser": Object {
|
||||
"chromium": Object {
|
||||
"disableSandbox": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"groupCount": 2,
|
||||
"hasFooter": true,
|
||||
"hasHeader": true,
|
||||
"id": "print",
|
||||
"selectors": Object {
|
||||
"itemsCountAttribute": "data-shared-items-count",
|
||||
"renderComplete": "[data-shared-item]",
|
||||
"screenshot": "[data-shared-item]",
|
||||
"timefilterDurationAttribute": "data-shared-timefilter-duration",
|
||||
},
|
||||
"useReportingBranding": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the canvas layout', () => {
|
||||
const { id, height, width } = new CanvasLayout({ width: 18, height: 18 });
|
||||
const canvasParams: LayoutParams = { id, dimensions: { height, width } };
|
||||
const layout = createLayout(config.get('capture'), canvasParams);
|
||||
expect(layout).toMatchInlineSnapshot(`
|
||||
CanvasLayout {
|
||||
"groupCount": 1,
|
||||
"hasFooter": false,
|
||||
"hasHeader": false,
|
||||
"height": 18,
|
||||
"id": "canvas",
|
||||
"scaledHeight": 36,
|
||||
"scaledWidth": 36,
|
||||
"selectors": Object {
|
||||
"itemsCountAttribute": "data-shared-items-count",
|
||||
"renderComplete": "[data-shared-item]",
|
||||
"screenshot": "[data-shared-items-container]",
|
||||
"timefilterDurationAttribute": "data-shared-timefilter-duration",
|
||||
},
|
||||
"useReportingBranding": false,
|
||||
"width": 18,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
import { LAYOUT_TYPES } from '../../../common/constants';
|
||||
import { CaptureConfig } from '../../types';
|
||||
import { LayoutInstance, LayoutParams } from './';
|
||||
import { LayoutInstance, LayoutParams, LayoutTypes } from './';
|
||||
import { CanvasLayout } from './canvas_layout';
|
||||
import { PreserveLayout } from './preserve_layout';
|
||||
import { PrintLayout } from './print_layout';
|
||||
|
||||
|
@ -18,6 +19,10 @@ export function createLayout(
|
|||
return new PreserveLayout(layoutParams.dimensions);
|
||||
}
|
||||
|
||||
// this is the default because some jobs won't have anything specified
|
||||
if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.CANVAS) {
|
||||
return new CanvasLayout(layoutParams.dimensions);
|
||||
}
|
||||
|
||||
// layoutParams is optional as PrintLayout doesn't use it
|
||||
return new PrintLayout(captureConfig);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,22 @@ export {
|
|||
export { createLayout } from './create_layout';
|
||||
export { Layout } from './layout';
|
||||
export { PreserveLayout } from './preserve_layout';
|
||||
export { CanvasLayout } from './canvas_layout';
|
||||
export { PrintLayout } from './print_layout';
|
||||
|
||||
export const LayoutTypes = {
|
||||
PRESERVE_LAYOUT: 'preserve_layout',
|
||||
PRINT: 'print',
|
||||
CANVAS: 'canvas', // no margins or branding in the layout
|
||||
};
|
||||
|
||||
export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({
|
||||
screenshot: '[data-shared-items-container]',
|
||||
renderComplete: '[data-shared-item]',
|
||||
itemsCountAttribute: 'data-shared-items-count',
|
||||
timefilterDurationAttribute: 'data-shared-timefilter-duration',
|
||||
});
|
||||
|
||||
interface LayoutSelectors {
|
||||
// Fields that are not part of Layout: the instances
|
||||
// independently implement these fields on their own
|
||||
|
|
|
@ -17,6 +17,10 @@ export abstract class Layout {
|
|||
public id: string = '';
|
||||
public groupCount: number = 0;
|
||||
|
||||
public hasHeader: boolean = true;
|
||||
public hasFooter: boolean = true;
|
||||
public useReportingBranding: boolean = true;
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
}
|
||||
|
@ -35,5 +39,5 @@ export abstract class Layout {
|
|||
|
||||
public abstract getBrowserViewport(): Size;
|
||||
|
||||
public abstract getCssOverridesPath(): string;
|
||||
public abstract getCssOverridesPath(): string | undefined;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ export const injectCustomCss = async (
|
|||
);
|
||||
|
||||
const filePath = layout.getCssOverridesPath();
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
const buffer = await fsp.readFile(filePath);
|
||||
try {
|
||||
await browser.evaluate(
|
||||
|
|
|
@ -26,5 +26,6 @@ export default function canvasApp({ loadTestFile, getService }) {
|
|||
loadTestFile(require.resolve('./custom_elements'));
|
||||
loadTestFile(require.resolve('./feature_controls/canvas_security'));
|
||||
loadTestFile(require.resolve('./feature_controls/canvas_spaces'));
|
||||
loadTestFile(require.resolve('./reports'));
|
||||
});
|
||||
}
|
||||
|
|
339
x-pack/test/functional/apps/canvas/reports.ts
Normal file
339
x-pack/test/functional/apps/canvas/reports.ts
Normal file
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const browser = getService('browser');
|
||||
const log = getService('log');
|
||||
const security = getService('security');
|
||||
const PageObjects = getPageObjects(['reporting', 'common', 'canvas']);
|
||||
|
||||
describe('Canvas PDF Report Generation', () => {
|
||||
before('initialize tests', async () => {
|
||||
log.debug('ReportingPage:initTests');
|
||||
await security.testUser.setRoles(['kibana_admin', 'reporting_user']);
|
||||
await esArchiver.load('canvas/reports');
|
||||
await browser.setWindowSize(1600, 850);
|
||||
});
|
||||
after('clean up archives', async () => {
|
||||
await esArchiver.unload('canvas/reports');
|
||||
await es.deleteByQuery({
|
||||
index: '.reporting-*',
|
||||
refresh: true,
|
||||
body: { query: { match_all: {} } },
|
||||
});
|
||||
});
|
||||
|
||||
describe('Print PDF button', () => {
|
||||
it('downloaded PDF base64 string is correct with borders and logo', async function () {
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout
|
||||
this.timeout(180000);
|
||||
|
||||
await PageObjects.common.navigateToApp('canvas');
|
||||
await PageObjects.canvas.loadFirstWorkpad('The Very Cool Workpad for PDF Tests');
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
|
||||
const url = await PageObjects.reporting.getReportURL(60000);
|
||||
const res = await PageObjects.reporting.getResponse(url);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.get('content-type')).to.equal('application/pdf');
|
||||
expect(res.get('content-disposition')).to.equal(
|
||||
'inline; filename="The Very Cool Workpad for PDF Tests.pdf"'
|
||||
);
|
||||
|
||||
/* Check the value of the PDF data that was generated
|
||||
* PDF files include dynamic meta info such as creation date.
|
||||
* This checks only the first few thousand bytes of the Buffer
|
||||
*/
|
||||
const pdfStrings = (res.body as Buffer).toString('utf8', 14); // start on byte 14 to skip non-utf8 data
|
||||
const [header, , contents, , info] = pdfStrings.split('stream'); // ignore everthing from `stream` to `endstream` - the non-utf8 blocks
|
||||
|
||||
// PDF Header
|
||||
|
||||
expectSnapshot(header).toMatchInline(`
|
||||
"
|
||||
7 0 obj
|
||||
<<
|
||||
/Predictor 15
|
||||
/Colors 1
|
||||
/BitsPerComponent 8
|
||||
/Columns 577
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Length 149
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
"
|
||||
`);
|
||||
|
||||
// PDF Contents
|
||||
|
||||
expectSnapshot(contents).toMatchInline(`
|
||||
"
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Type /ExtGState
|
||||
/ca 1
|
||||
/CA 1
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 1 0 R
|
||||
/MediaBox [0 0 90 189]
|
||||
/Contents 9 0 R
|
||||
/Resources 10 0 R
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
|
||||
/ExtGState <<
|
||||
/Gs1 12 0 R
|
||||
>>
|
||||
/XObject <<
|
||||
/I1 5 0 R
|
||||
/I2 6 0 R
|
||||
>>
|
||||
/Font <<
|
||||
/F1 13 0 R
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Length 270
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
"
|
||||
`);
|
||||
|
||||
// PDF Info
|
||||
|
||||
// The Info section of text includes a Datestamp which will obviously not match as a snapshot between tests
|
||||
// This does a .replace on the Info text to erase the dynamic date string
|
||||
expectSnapshot(info.replace(/D:\d+Z/, 'D:DATESTAMP')).toMatchInline(`
|
||||
"
|
||||
endobj
|
||||
15 0 obj
|
||||
(pdfmake)
|
||||
endobj
|
||||
16 0 obj
|
||||
(pdfmake)
|
||||
endobj
|
||||
17 0 obj
|
||||
(D:DATESTAMP)
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Producer 15 0 R
|
||||
/Creator 16 0 R
|
||||
/CreationDate 17 0 R
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Type /FontDescriptor
|
||||
/FontName /AZZZZZ+Roboto-Regular
|
||||
/Flags 4
|
||||
/FontBBox [-681.152344 -270.996094 1181.640625 1047.851563]
|
||||
/ItalicAngle 0
|
||||
/Ascent 927.734375
|
||||
/Descent -244.140625
|
||||
/CapHeight 710.9375
|
||||
/XHeight 528.320313
|
||||
/StemV 0
|
||||
/FontFile2 18 0 R
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /CIDFontType2
|
||||
/BaseFont /AZZZZZ+Roboto-Regular
|
||||
/CIDSystemInfo <<
|
||||
/Registry (Adobe)
|
||||
/Ordering (Identity)
|
||||
/Supplement 0
|
||||
>>
|
||||
/FontDescriptor 19 0 R
|
||||
/W [0 [507 596.679688 566.40625 526.855469 247.558594 637.207031 547.851563 566.40625 561.523438 566.40625 342.773438]]
|
||||
/CIDToGIDMap /Identity
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Length 250
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
"
|
||||
`);
|
||||
|
||||
expectSnapshot(res.get('content-length')).toMatchInline(`"20726"`);
|
||||
});
|
||||
|
||||
it('downloaded PDF base64 string is correct without borders and logo', async function () {
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout
|
||||
this.timeout(180000);
|
||||
|
||||
await PageObjects.common.navigateToApp('canvas');
|
||||
await PageObjects.canvas.loadFirstWorkpad('The Very Cool Workpad for PDF Tests');
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.toggleReportMode();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
|
||||
const url = await PageObjects.reporting.getReportURL(60000);
|
||||
const res = await PageObjects.reporting.getResponse(url);
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
expect(res.get('content-type')).to.equal('application/pdf');
|
||||
expect(res.get('content-disposition')).to.equal(
|
||||
'inline; filename="The Very Cool Workpad for PDF Tests.pdf"'
|
||||
);
|
||||
|
||||
/* Check the value of the PDF data that was generated
|
||||
* PDF files include dynamic meta info such as creation date.
|
||||
* This checks only the first few thousand bytes of the Buffer
|
||||
*/
|
||||
const pdfStrings = (res.body as Buffer).toString('utf8', 14); // start on byte 14 to skip non-utf8 data
|
||||
const [header, , contents, , info] = pdfStrings.split('stream'); // ignore everthing from `stream` to `endstream` - the non-utf8 blocks
|
||||
|
||||
// PDF Header
|
||||
|
||||
expectSnapshot(header).toMatchInline(`
|
||||
"
|
||||
9 0 obj
|
||||
<<
|
||||
/Type /ExtGState
|
||||
/ca 1
|
||||
/CA 1
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 1 0 R
|
||||
/MediaBox [0 0 8 8]
|
||||
/Contents 6 0 R
|
||||
/Resources 7 0 R
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
|
||||
/ExtGState <<
|
||||
/Gs1 9 0 R
|
||||
>>
|
||||
/XObject <<
|
||||
/I1 5 0 R
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Length 45
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
"
|
||||
`);
|
||||
|
||||
// PDF Contents
|
||||
|
||||
expectSnapshot(contents.replace(/D:\d+Z/, 'D:DATESTAMP')).toMatchInline(`
|
||||
"
|
||||
endobj
|
||||
11 0 obj
|
||||
(pdfmake)
|
||||
endobj
|
||||
12 0 obj
|
||||
(pdfmake)
|
||||
endobj
|
||||
13 0 obj
|
||||
(D:DATESTAMP)
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Producer 11 0 R
|
||||
/Creator 12 0 R
|
||||
/CreationDate 13 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 1 0 R
|
||||
/Names 2 0 R
|
||||
>>
|
||||
endobj
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Count 1
|
||||
/Kids [8 0 R]
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Dests <<
|
||||
/Names [
|
||||
]
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Type /XObject
|
||||
/Subtype /Image
|
||||
/Height 16
|
||||
/Width 16
|
||||
/BitsPerComponent 8
|
||||
/Filter /FlateDecode
|
||||
/ColorSpace /DeviceGray
|
||||
/Decode [0 1]
|
||||
/Length 12
|
||||
>>
|
||||
"
|
||||
`);
|
||||
|
||||
// PDF Info
|
||||
expectSnapshot(info).toMatchInline(`
|
||||
"
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Type /XObject
|
||||
/Subtype /Image
|
||||
/BitsPerComponent 8
|
||||
/Width 16
|
||||
/Height 16
|
||||
/Filter /FlateDecode
|
||||
/ColorSpace /DeviceRGB
|
||||
/SMask 14 0 R
|
||||
/Length 18
|
||||
>>
|
||||
"
|
||||
`);
|
||||
|
||||
expectSnapshot(res.get('content-length')).toMatchInline(`"1599"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
BIN
x-pack/test/functional/es_archives/canvas/reports/data.json.gz
Normal file
BIN
x-pack/test/functional/es_archives/canvas/reports/data.json.gz
Normal file
Binary file not shown.
2422
x-pack/test/functional/es_archives/canvas/reports/mappings.json
Normal file
2422
x-pack/test/functional/es_archives/canvas/reports/mappings.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -32,6 +32,19 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo
|
|||
await testSubjects.findAll('canvasWorkpadPage > canvasWorkpadPageElementContent');
|
||||
},
|
||||
|
||||
/*
|
||||
* Finds the first workpad in the loader (uses find, not findAll) and
|
||||
* ensures the expected name is the actual name. Then it clicks the element
|
||||
* to load the workpad. Resolves once the workpad is in the DOM
|
||||
*/
|
||||
async loadFirstWorkpad(workpadName: string) {
|
||||
const elem = await testSubjects.find('canvasWorkpadLoaderWorkpad');
|
||||
const text = await elem.getVisibleText();
|
||||
expect(text).to.be(workpadName);
|
||||
await elem.click();
|
||||
await testSubjects.existOrFail('canvasWorkpadPage');
|
||||
},
|
||||
|
||||
async fillOutCustomElementForm(name: string, description: string) {
|
||||
// Fill out the custom element form and submit it
|
||||
await testSubjects.setValue('canvasCustomElementForm-name', name, {
|
||||
|
|
|
@ -119,6 +119,10 @@ export function ReportingPageProvider({ getService, getPageObjects }: FtrProvide
|
|||
await testSubjects.click('generateReportButton');
|
||||
}
|
||||
|
||||
async toggleReportMode() {
|
||||
await testSubjects.click('reportModeToggle');
|
||||
}
|
||||
|
||||
async checkForReportingToasts() {
|
||||
log.debug('Reporting:checkForReportingToasts');
|
||||
const isToastPresent = await testSubjects.exists('completeReportSuccess', {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue