mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Reporting/Dashboard] Use Chromium for print-optimized PDFs (#130546)
* first version of semi-sane results * getting a bit more sophisticated * wip on footer, page numbers not working, but logo working * re-work PoC for readability, added a lot of comments * change up formatting for readability * added comment * remove some comments and remove HACK * use page.pdf function * remove controls from shared PoC ui * preserveDrawingBuffer fix for maps, needs review * minor clean up * update sass * clean up experimental code * moved a few files around to get this ready for review * added appservices as print media code owners * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * added PDFJS to get num pages * fix getting page number using pdfjs-dist * update inline snapshot * Revert "update inline snapshot" This reverts commiteb413234a7
. * do not create a new page at the very end * major overhaul, rather use puppeteers footerTemplate and headerTemplate to get visual parity with current reports * add TODO * update test fixture * update doc comment * remove whitespace * fix missing time range from print PDF header and make size much smaller * update tests * update test * try out slash instead of nbsp * Revert "try out slash instead of nbsp" This reverts commit1de112a6f5
. * implement ability to inject logo using handlebars templates * move assets to shared location * fix injecting of values via handlebars and minor style tweaks for 3rd party logos * inject a few more values to the footer * update casing check * use locales version of headless chromium zip * fix tests and update sizing of logos * use locales version for arm64 too * fix jest test * fix types * made pdf capture check stricter * fix PDF generation issue due to query bar rendering content that caused an issue; need to figure out what exactly the issue was... Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c46b457d36
commit
9a78d3dde4
50 changed files with 567 additions and 120 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -83,6 +83,7 @@
|
|||
/x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services
|
||||
/x-pack/plugins/runtime_fields @elastic/kibana-app-services
|
||||
/x-pack/test/search_sessions_integration/ @elastic/kibana-app-services
|
||||
/src/plugins/dashboard/public/application/embeddable/viewport/print_media @elastic/kibana-app-services
|
||||
|
||||
### Observability Plugins
|
||||
|
||||
|
|
|
@ -339,6 +339,7 @@
|
|||
"p-retry": "^4.2.0",
|
||||
"papaparse": "^5.2.0",
|
||||
"pbf": "3.2.1",
|
||||
"pdfjs-dist": "^2.13.216",
|
||||
"pdfmake": "^0.2.4",
|
||||
"peggy": "^1.2.0",
|
||||
"pluralize": "3.1.0",
|
||||
|
|
|
@ -146,10 +146,10 @@ export const TEMPORARILY_IGNORED_PATHS = [
|
|||
'x-pack/plugins/monitoring/public/icons/health-green.svg',
|
||||
'x-pack/plugins/monitoring/public/icons/health-red.svg',
|
||||
'x-pack/plugins/monitoring/public/icons/health-yellow.svg',
|
||||
'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf',
|
||||
'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf',
|
||||
'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf',
|
||||
'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf',
|
||||
'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf',
|
||||
'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png',
|
||||
'x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf',
|
||||
'x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf',
|
||||
'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf',
|
||||
'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf',
|
||||
'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf',
|
||||
'x-pack/plugins/screenshotting/server/assets/img/logo-grey.png',
|
||||
];
|
||||
|
|
|
@ -119,13 +119,12 @@ export function DashboardApp({
|
|||
<>
|
||||
{isCompleteDashboardAppState(dashboardAppState) && (
|
||||
<>
|
||||
{!printMode && (
|
||||
<DashboardTopNav
|
||||
redirectTo={redirectTo}
|
||||
embedSettings={embedSettings}
|
||||
dashboardAppState={dashboardAppState}
|
||||
/>
|
||||
)}
|
||||
<DashboardTopNav
|
||||
printMode={printMode}
|
||||
redirectTo={redirectTo}
|
||||
embedSettings={embedSettings}
|
||||
dashboardAppState={dashboardAppState}
|
||||
/>
|
||||
|
||||
{dashboardAppState.savedDashboard.outcome === 'conflict' &&
|
||||
dashboardAppState.savedDashboard.id &&
|
||||
|
|
|
@ -1,9 +1,68 @@
|
|||
.printViewport {
|
||||
&__vis {
|
||||
height: 600px; // These values might need to be passed in as dimensions for the report. I.e., print should use layout dimensions.
|
||||
width: 975px;
|
||||
@import './print_media/styling/index';
|
||||
|
||||
// Some vertical space between vis, but center horizontally
|
||||
margin: 10px auto;
|
||||
$visualisationsPerPage: 2;
|
||||
$visPadding: 4mm;
|
||||
|
||||
/*
|
||||
We set the same visual padding on the browser and print versions of the UI so that
|
||||
we don't hit a race condition where padding is being updated while the print image
|
||||
is being formed. This can result in parts of the vis being cut out.
|
||||
*/
|
||||
@mixin visualizationPadding {
|
||||
// Open space from page margin
|
||||
padding-left: $visPadding;
|
||||
padding-right: $visPadding;
|
||||
|
||||
// Last vis on the page
|
||||
&:nth-child(#{$visualisationsPerPage}n) {
|
||||
page-break-after: always;
|
||||
padding-top: $visPadding;
|
||||
padding-bottom: $visPadding;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen, projection {
|
||||
.printViewport {
|
||||
&__vis {
|
||||
@include visualizationPadding();
|
||||
|
||||
& .embPanel__header button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
margin: $euiSizeL auto;
|
||||
height: calc(#{$a4PageContentHeight} / #{$visualisationsPerPage});
|
||||
width: $a4PageContentWidth;
|
||||
padding: $visPadding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.printViewport {
|
||||
&__vis {
|
||||
@include visualizationPadding();
|
||||
|
||||
height: calc(#{$a4PageContentHeight} / #{$visualisationsPerPage});
|
||||
width: $a4PageContentWidth;
|
||||
|
||||
& .euiPanel {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
& .embPanel__header button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
page-break-inside: avoid;
|
||||
|
||||
& * {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# Print media
|
||||
|
||||
The code here is designed to be movable outside the domain of Dashboard. Currently,
|
||||
the components and styles are only used by Dashboard but we may choose to move them to,
|
||||
for example, a Kibana package in the future.
|
||||
|
||||
Any changes to this code must be tested by generating a print-optimized PDF in dashboard.
|
|
@ -0,0 +1,52 @@
|
|||
@import './vars';
|
||||
|
||||
/*
|
||||
This styling contains utility and minimal layout styles to help plugins create
|
||||
print-ready HTML.
|
||||
|
||||
Observations:
|
||||
1. We currently do not control the user-agent's header and footer content
|
||||
(including the style of fonts) for client-side printing.
|
||||
|
||||
2. Page box model is quite different from what we have in browsers - page
|
||||
margins define where the "no-mans-land" exists for actual content. Moving
|
||||
content into this space by, for example setting negative margins resulted
|
||||
in slightly unpredictable behaviour because the browser wants to either
|
||||
move this content to another page or it may get split across two
|
||||
pages.
|
||||
|
||||
3. page-break-* is your friend!
|
||||
*/
|
||||
|
||||
// Currently we cannot control or style the content the browser places in
|
||||
// margins, this might change in the future:
|
||||
// See https://drafts.csswg.org/css-page-3/#margin-boxes
|
||||
@page {
|
||||
size: A4;
|
||||
orientation: portrait;
|
||||
margin: 0;
|
||||
margin-top: $a4PageHeaderHeight;
|
||||
margin-bottom: $a4PageFooterHeight;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
html {
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
// It is good practice to show the full URL in the final, printed output
|
||||
a[href]:after {
|
||||
content: ' [' attr(href) ']';
|
||||
}
|
||||
|
||||
figure {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-print-color-adjust: exact !important; /* Chrome, Safari, Edge */
|
||||
color-adjust: exact !important; /*Firefox*/
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
$a4PageHeight: 297mm;
|
||||
$a4PageWidth: 210mm;
|
||||
$a4PageMargin: 0;
|
||||
$a4PagePadding: 0;
|
||||
$a4PageHeaderHeight: 15mm;
|
||||
$a4PageFooterHeight: 20mm;
|
||||
|
||||
$a4PageContentHeight: $a4PageHeight - $a4PageHeaderHeight - $a4PageFooterHeight;
|
||||
$a4PageContentWidth: $a4PageWidth;
|
|
@ -183,8 +183,7 @@ export const useDashboardAppState = ({
|
|||
savedDashboard,
|
||||
});
|
||||
|
||||
// Backwards compatible way of detecting that we are taking a screenshot
|
||||
const legacyPrintLayoutDetected =
|
||||
const printLayoutDetected =
|
||||
screenshotModeService?.isScreenshotMode() &&
|
||||
screenshotModeService.getScreenshotContext('layout') === 'print';
|
||||
|
||||
|
@ -194,8 +193,7 @@ export const useDashboardAppState = ({
|
|||
...initialDashboardStateFromUrl,
|
||||
...forwardedAppState,
|
||||
|
||||
// if we are in legacy print mode, dashboard needs to be in print viewMode
|
||||
...(legacyPrintLayoutDetected ? { viewMode: ViewMode.PRINT } : {}),
|
||||
...(printLayoutDetected ? { viewMode: ViewMode.PRINT } : {}),
|
||||
|
||||
// if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it.
|
||||
...(incomingEmbeddable ? { viewMode: ViewMode.EDIT } : {}),
|
||||
|
|
|
@ -82,6 +82,7 @@ export interface DashboardTopNavProps {
|
|||
dashboardAppState: CompleteDashboardAppState;
|
||||
embedSettings?: DashboardEmbedSettings;
|
||||
redirectTo: DashboardRedirect;
|
||||
printMode: boolean;
|
||||
}
|
||||
|
||||
const LabsFlyout = withSuspense(LazyLabsFlyout, null);
|
||||
|
@ -90,6 +91,7 @@ export function DashboardTopNav({
|
|||
dashboardAppState,
|
||||
embedSettings,
|
||||
redirectTo,
|
||||
printMode,
|
||||
}: DashboardTopNavProps) {
|
||||
const {
|
||||
core,
|
||||
|
@ -488,7 +490,9 @@ export function DashboardTopNav({
|
|||
|
||||
const isFullScreenMode = dashboardState.fullScreenMode;
|
||||
const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
|
||||
const showQueryInput = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowQueryInput));
|
||||
const showQueryInput = shouldShowNavBarComponent(
|
||||
Boolean(embedSettings?.forceShowQueryInput || printMode)
|
||||
);
|
||||
const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
|
||||
const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar));
|
||||
const showQueryBar = showQueryInput || showDatePicker || showFilterBar;
|
||||
|
@ -535,6 +539,7 @@ export function DashboardTopNav({
|
|||
useDefaultBehaviors: true,
|
||||
savedQuery: state.savedQuery,
|
||||
savedQueryId: dashboardState.savedQuery,
|
||||
visible: printMode !== true,
|
||||
onQuerySubmit: (_payload, isUpdate) => {
|
||||
if (isUpdate === false) {
|
||||
dashboardAppState.$triggerDashboardRefresh.next({ force: true });
|
||||
|
@ -585,10 +590,10 @@ export function DashboardTopNav({
|
|||
return (
|
||||
<>
|
||||
<TopNavMenu {...getNavBarProps()} />
|
||||
{isLabsEnabled && isLabsShown ? (
|
||||
{!printMode && isLabsEnabled && isLabsShown ? (
|
||||
<LabsFlyout solutions={['dashboard']} onClose={() => setIsLabsShown(false)} />
|
||||
) : null}
|
||||
{dashboardState.viewMode !== ViewMode.VIEW ? (
|
||||
{dashboardState.viewMode !== ViewMode.VIEW && !printMode ? (
|
||||
<>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<SolutionToolbar isDarkModeEnabled={IS_DARK_THEME}>
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.kbnTopNavMenu__wrapper {
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnTopNavMenu__badgeWrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
|
@ -28,6 +28,7 @@ export type TopNavMenuProps = StatefulSearchBarProps &
|
|||
showFilterBar?: boolean;
|
||||
unifiedSearch?: UnifiedSearchPublicPluginStart;
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
/**
|
||||
* If provided, the menu part of the component will be rendered as a portal inside the given mount point.
|
||||
*
|
||||
|
@ -105,9 +106,11 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null {
|
|||
}
|
||||
|
||||
function renderLayout() {
|
||||
const { setMenuMountPoint } = props;
|
||||
const { setMenuMountPoint, visible } = props;
|
||||
const menuClassName = classNames('kbnTopNavMenu', props.className);
|
||||
const wrapperClassName = 'kbnTopNavMenu__wrapper';
|
||||
const wrapperClassName = classNames('kbnTopNavMenu__wrapper', {
|
||||
'kbnTopNavMenu__wrapper--hidden': visible === false,
|
||||
});
|
||||
if (setMenuMountPoint) {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["dataViews", "data", "uiActions"],
|
||||
"requiredPlugins": ["dataViews", "data", "uiActions", "screenshotMode"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "data"],
|
||||
"serviceFolders": ["autocomplete"],
|
||||
"configPath": ["unifiedSearch"]
|
||||
|
|
|
@ -55,7 +55,7 @@ export class UnifiedSearchPublicPlugin
|
|||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{ data, dataViews, uiActions }: UnifiedSearchStartDependencies
|
||||
{ data, dataViews, uiActions, screenshotMode }: UnifiedSearchStartDependencies
|
||||
): UnifiedSearchPublicPluginStart {
|
||||
setTheme(core.theme);
|
||||
setOverlays(core.overlays);
|
||||
|
@ -68,6 +68,7 @@ export class UnifiedSearchPublicPlugin
|
|||
data,
|
||||
storage: this.storage,
|
||||
usageCollection: this.usageCollection,
|
||||
isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()),
|
||||
});
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
|
|
|
@ -87,6 +87,7 @@ export interface QueryBarTopRowProps {
|
|||
filterBar?: React.ReactNode;
|
||||
showDatePickerAsBadge?: boolean;
|
||||
showSubmitButton?: boolean;
|
||||
isScreenshotMode?: boolean;
|
||||
}
|
||||
|
||||
const SharingMetaFields = React.memo(function SharingMetaFields({
|
||||
|
@ -474,6 +475,8 @@ export const QueryBarTopRow = React.memo(
|
|||
);
|
||||
}
|
||||
|
||||
const isScreenshotMode = props.isScreenshotMode === true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SharingMetaFields
|
||||
|
@ -481,25 +484,29 @@ export const QueryBarTopRow = React.memo(
|
|||
to={currentDateRange.to}
|
||||
dateFormat={uiSettings.get('dateFormat')}
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
className="kbnQueryBar"
|
||||
direction={isMobile && !shouldShowDatePickerAsBadge() ? 'column' : 'row'}
|
||||
responsive={false}
|
||||
gutterSize="s"
|
||||
justifyContent={shouldShowDatePickerAsBadge() ? 'flexStart' : 'flexEnd'}
|
||||
wrap
|
||||
>
|
||||
{renderDataViewsPicker()}
|
||||
<EuiFlexItem
|
||||
grow={!shouldShowDatePickerAsBadge()}
|
||||
style={{ minWidth: shouldShowDatePickerAsBadge() ? 'auto' : 320 }}
|
||||
>
|
||||
{renderQueryInput()}
|
||||
</EuiFlexItem>
|
||||
{shouldShowDatePickerAsBadge() && props.filterBar}
|
||||
{renderUpdateButton()}
|
||||
</EuiFlexGroup>
|
||||
{!shouldShowDatePickerAsBadge() && props.filterBar}
|
||||
{!isScreenshotMode && (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
className="kbnQueryBar"
|
||||
direction={isMobile && !shouldShowDatePickerAsBadge() ? 'column' : 'row'}
|
||||
responsive={false}
|
||||
gutterSize="s"
|
||||
justifyContent={shouldShowDatePickerAsBadge() ? 'flexStart' : 'flexEnd'}
|
||||
wrap
|
||||
>
|
||||
{renderDataViewsPicker()}
|
||||
<EuiFlexItem
|
||||
grow={!shouldShowDatePickerAsBadge()}
|
||||
style={{ minWidth: shouldShowDatePickerAsBadge() ? 'auto' : 320 }}
|
||||
>
|
||||
{renderQueryInput()}
|
||||
</EuiFlexItem>
|
||||
{shouldShowDatePickerAsBadge() && props.filterBar}
|
||||
{renderUpdateButton()}
|
||||
</EuiFlexGroup>
|
||||
{!shouldShowDatePickerAsBadge() && props.filterBar}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ interface StatefulSearchBarDeps {
|
|||
data: Omit<DataPublicPluginStart, 'ui'>;
|
||||
storage: IStorageWrapper;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
isScreenshotMode?: boolean;
|
||||
}
|
||||
|
||||
export type StatefulSearchBarProps = SearchBarOwnProps & {
|
||||
|
@ -110,7 +111,13 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => {
|
|||
return props.useDefaultBehaviors ? {} : props;
|
||||
};
|
||||
|
||||
export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) {
|
||||
export function createSearchBar({
|
||||
core,
|
||||
storage,
|
||||
data,
|
||||
usageCollection,
|
||||
isScreenshotMode = false,
|
||||
}: StatefulSearchBarDeps) {
|
||||
// App name should come from the core application service.
|
||||
// Until it's available, we'll ask the user to provide it for the pre-wired component.
|
||||
return (props: StatefulSearchBarProps) => {
|
||||
|
@ -197,6 +204,7 @@ export function createSearchBar({ core, storage, data, usageCollection }: Statef
|
|||
{...overrideDefaultBehaviors(props)}
|
||||
dataViewPickerComponentProps={props.dataViewPickerComponentProps}
|
||||
displayStyle={props.displayStyle}
|
||||
isScreenshotMode={isScreenshotMode}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
|
|
@ -20,5 +20,8 @@ export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => {
|
|||
inPage: css`
|
||||
padding: 0;
|
||||
`,
|
||||
hidden: css`
|
||||
display: none;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -87,6 +87,7 @@ export interface SearchBarOwnProps {
|
|||
fillSubmitButton?: boolean;
|
||||
dataViewPickerComponentProps?: DataViewPickerProps;
|
||||
showSubmitButton?: boolean;
|
||||
isScreenshotMode?: boolean;
|
||||
}
|
||||
|
||||
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
|
||||
|
@ -341,13 +342,16 @@ class SearchBarUI extends Component<SearchBarProps & WithEuiThemeProps, State> {
|
|||
|
||||
public render() {
|
||||
const { theme } = this.props;
|
||||
const isScreenshotMode = this.props.isScreenshotMode === true;
|
||||
const styles = searchBarStyles(theme);
|
||||
const cssStyles = [
|
||||
styles.uniSearchBar,
|
||||
this.props.displayStyle && styles[this.props.displayStyle],
|
||||
isScreenshotMode && styles.hidden,
|
||||
];
|
||||
|
||||
const classes = classNames('uniSearchBar', {
|
||||
[`uniSearchBar--hidden`]: isScreenshotMode,
|
||||
[`uniSearchBar--${this.props.displayStyle}`]: this.props.displayStyle,
|
||||
});
|
||||
|
||||
|
@ -470,6 +474,7 @@ class SearchBarUI extends Component<SearchBarProps & WithEuiThemeProps, State> {
|
|||
dataViewPickerComponentProps={this.props.dataViewPickerComponentProps}
|
||||
showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()}
|
||||
filterBar={filterBar}
|
||||
isScreenshotMode={this.props.isScreenshotMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
|
@ -29,6 +30,7 @@ export interface UnifiedSearchStartDependencies {
|
|||
fieldFormats: FieldFormatsStart;
|
||||
data: DataPublicPluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
screenshotMode?: ScreenshotModePluginStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"mapsEms",
|
||||
"savedObjects",
|
||||
"share",
|
||||
"presentationUtil"
|
||||
"presentationUtil",
|
||||
"screenshotMode"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"cloud",
|
||||
|
|
|
@ -41,6 +41,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/public';
|
|||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import type { LensPublicSetup } from '@kbn/lens-plugin/public';
|
||||
import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public';
|
||||
import {
|
||||
createRegionMapFn,
|
||||
regionMapRenderer,
|
||||
|
@ -88,6 +89,7 @@ export interface MapsPluginSetupDependencies {
|
|||
share: SharePluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
}
|
||||
|
||||
export interface MapsPluginStartDependencies {
|
||||
|
@ -144,7 +146,15 @@ export class MapsPlugin
|
|||
registerLicensedFeatures(plugins.licensing);
|
||||
|
||||
const config = this._initializerContext.config.get<MapsConfigType>();
|
||||
setMapAppConfig(config);
|
||||
setMapAppConfig({
|
||||
...config,
|
||||
|
||||
// Override this when we know we are taking a screenshot (i.e. no user interaction)
|
||||
// to avoid a blank-canvas issue when rendering maps on a PDF
|
||||
preserveDrawingBuffer: plugins.screenshotMode.isScreenshotMode()
|
||||
? true
|
||||
: config.preserveDrawingBuffer,
|
||||
});
|
||||
|
||||
const locator = plugins.share.url.locators.create(
|
||||
new MapsAppLocatorDefinition({
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/shared_ux/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/screenshot_mode/tsconfig.json" },
|
||||
{ "path": "../cloud/tsconfig.json" },
|
||||
{ "path": "../features/tsconfig.json" },
|
||||
{ "path": "../lens/tsconfig.json" },
|
||||
|
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
@ -17,6 +17,7 @@ import {
|
|||
import { ConfigType } from '../../config';
|
||||
import { allowRequest } from '../network_policy';
|
||||
import { stripUnsafeHeaders } from './strip_unsafe_headers';
|
||||
import { getFooterTemplate, getHeaderTemplate } from './templates';
|
||||
|
||||
export type Context = Record<string, unknown>;
|
||||
|
||||
|
@ -155,6 +156,18 @@ export class HeadlessChromiumDriver {
|
|||
return !this.page.isClosed();
|
||||
}
|
||||
|
||||
async printA4Pdf({ title, logo }: { title: string; logo?: string }): Promise<Buffer> {
|
||||
return this.page.pdf({
|
||||
format: 'a4',
|
||||
preferCSSPageSize: true,
|
||||
scale: 1,
|
||||
landscape: false,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: await getHeaderTemplate({ title }),
|
||||
footerTemplate: await getFooterTemplate({ logo }),
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Call Page.screenshot and return a base64-encoded string of the image
|
||||
*/
|
||||
|
|
|
@ -67,8 +67,8 @@ export class ChromiumArchivePaths {
|
|||
{
|
||||
platform: 'linux',
|
||||
architecture: 'x64',
|
||||
archiveFilename: 'chromium-70f5d88-linux_x64.zip',
|
||||
archiveChecksum: '7b1c9c2fb613444fbdf004a3b75a58df',
|
||||
archiveFilename: 'chromium-70f5d88-locales-linux_x64.zip',
|
||||
archiveChecksum: '759bda5e5d32533cb136a85e37c0d102',
|
||||
binaryChecksum: '82e80f9727a88ba3836ce230134bd126',
|
||||
binaryRelativePath: 'headless_shell-linux_x64/headless_shell',
|
||||
location: 'custom',
|
||||
|
@ -78,8 +78,8 @@ export class ChromiumArchivePaths {
|
|||
{
|
||||
platform: 'linux',
|
||||
architecture: 'arm64',
|
||||
archiveFilename: 'chromium-70f5d88-linux_arm64.zip',
|
||||
archiveChecksum: '4a0217cfe7da86ad1e3d0e9e5895ddb5',
|
||||
archiveFilename: 'chromium-70f5d88-locales-linux_arm64.zip',
|
||||
archiveChecksum: '33613b8dc5212c0457210d5a37ea4b43',
|
||||
binaryChecksum: '29e943fbee6d87a217abd6cb6747058e',
|
||||
binaryRelativePath: 'headless_shell-linux_arm64/headless_shell',
|
||||
location: 'custom',
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<style>
|
||||
div.container {
|
||||
position: relative;
|
||||
font-size: 10px;
|
||||
font-family: system-ui;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
width: 100%;
|
||||
}
|
||||
div.pages {
|
||||
position: absolute;
|
||||
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
bottom: 3mm;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
img.logo {
|
||||
position: absolute;
|
||||
|
||||
left: 2mm;
|
||||
bottom: 3mm;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.poweredByElastic {
|
||||
position: absolute;
|
||||
color: #aaa;
|
||||
font-size: 2mm;
|
||||
|
||||
left: 2mm;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<!-- DPI is 72/96 so scaling of all values here must * 0.75 to get values on page -->
|
||||
<img class="logo" height="30mm" width="80mm" src="{{base64FooterLogo}}" />
|
||||
{{#if hasCustomLogo}}
|
||||
<div class="poweredByElastic">{{poweredByElasticCopy}}</div>
|
||||
{{/if}}
|
||||
<div class="pages">
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<style>
|
||||
.myTitle {
|
||||
font-size: 8px;
|
||||
color: #aaa;
|
||||
font-family: system-ui;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<span class="myTitle">{{title}}</span>
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import Handlebars from 'handlebars';
|
||||
import { assetPath } from '../../../constants';
|
||||
|
||||
async function compileTemplate<T>(pathToTemplate: string): Promise<Handlebars.TemplateDelegate<T>> {
|
||||
const contentsBuffer = await fs.readFile(pathToTemplate);
|
||||
return Handlebars.compile(contentsBuffer.toString());
|
||||
}
|
||||
|
||||
interface HeaderTemplateInput {
|
||||
title: string;
|
||||
}
|
||||
interface GetHeaderArgs {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function getHeaderTemplate({ title }: GetHeaderArgs): Promise<string> {
|
||||
const template = await compileTemplate<HeaderTemplateInput>(
|
||||
path.resolve(__dirname, './header.handlebars.html')
|
||||
);
|
||||
return template({ title });
|
||||
}
|
||||
|
||||
async function getDefaultFooterLogo(): Promise<string> {
|
||||
const logoBuffer = await fs.readFile(path.resolve(assetPath, 'img', 'logo-grey.png'));
|
||||
return `data:image/png;base64,${logoBuffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
interface FooterTemplateInput {
|
||||
base64FooterLogo: string;
|
||||
hasCustomLogo: boolean;
|
||||
poweredByElasticCopy: string;
|
||||
}
|
||||
|
||||
interface GetFooterArgs {
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
export async function getFooterTemplate({ logo }: GetFooterArgs): Promise<string> {
|
||||
const template = await compileTemplate<FooterTemplateInput>(
|
||||
path.resolve(__dirname, './footer.handlebars.html')
|
||||
);
|
||||
const hasCustomLogo = Boolean(logo);
|
||||
return template({
|
||||
base64FooterLogo: hasCustomLogo ? logo! : await getDefaultFooterLogo(),
|
||||
hasCustomLogo,
|
||||
poweredByElasticCopy: i18n.translate(
|
||||
'xpack.screenshotting.exportTypes.printablePdf.footer.logoDescription',
|
||||
{
|
||||
defaultMessage: 'Powered by Elastic',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
|
@ -88,11 +88,11 @@ describe('ensureDownloaded', () => {
|
|||
expect.arrayContaining([
|
||||
'chrome-mac.zip',
|
||||
'chrome-win.zip',
|
||||
'chromium-70f5d88-linux_x64.zip',
|
||||
'chromium-70f5d88-locales-linux_x64.zip',
|
||||
])
|
||||
);
|
||||
expect(readdirSync(path.resolve(`${paths.archivesPath}/arm64`))).toEqual(
|
||||
expect.arrayContaining(['chrome-mac.zip', 'chromium-70f5d88-linux_arm64.zip'])
|
||||
expect.arrayContaining(['chrome-mac.zip', 'chromium-70f5d88-locales-linux_arm64.zip'])
|
||||
);
|
||||
});
|
||||
|
||||
|
|
10
x-pack/plugins/screenshotting/server/constants.ts
Normal file
10
x-pack/plugins/screenshotting/server/constants.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 path from 'path';
|
||||
|
||||
export const assetPath = path.resolve(__dirname, 'assets');
|
|
@ -5,6 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// FIXME: Once/if we have the ability to get page count directly from Chrome/puppeteer
|
||||
// we should get rid of this lib.
|
||||
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js';
|
||||
|
||||
import type { Values } from '@kbn/utility-types';
|
||||
import { groupBy } from 'lodash';
|
||||
import type { PackageInfo } from '@kbn/core/server';
|
||||
|
@ -99,30 +103,51 @@ export async function toPdf(
|
|||
{ logo, title }: PdfScreenshotOptions,
|
||||
{ metrics, results }: CaptureResult
|
||||
): Promise<PdfScreenshotResult> {
|
||||
const timeRange = getTimeRange(results);
|
||||
try {
|
||||
const { buffer, pages } = await pngsToPdf({
|
||||
title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined,
|
||||
results,
|
||||
layout,
|
||||
logo,
|
||||
packageInfo,
|
||||
eventLogger,
|
||||
let buffer: Buffer;
|
||||
let pages: number;
|
||||
const shouldConvertPngsToPdf = layout.id !== LayoutTypes.PRINT;
|
||||
if (shouldConvertPngsToPdf) {
|
||||
const timeRange = getTimeRange(results);
|
||||
try {
|
||||
({ buffer, pages } = await pngsToPdf({
|
||||
title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined,
|
||||
results,
|
||||
layout,
|
||||
logo,
|
||||
packageInfo,
|
||||
eventLogger,
|
||||
}));
|
||||
|
||||
return {
|
||||
metrics: {
|
||||
...(metrics ?? {}),
|
||||
pages,
|
||||
},
|
||||
data: buffer,
|
||||
errors: results.flatMap(({ error }) => (error ? [error] : [])),
|
||||
renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []),
|
||||
};
|
||||
} catch (error) {
|
||||
eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`);
|
||||
eventLogger.error(error, Transactions.PDF);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
buffer = results[0].screenshots[0].data; // This buffer is already the PDF
|
||||
pages = await PDFJS.getDocument({ data: buffer }).promise.then((doc) => {
|
||||
const numPages = doc.numPages;
|
||||
doc.destroy();
|
||||
return numPages;
|
||||
});
|
||||
|
||||
return {
|
||||
metrics: {
|
||||
...(metrics ?? {}),
|
||||
pages,
|
||||
},
|
||||
data: buffer,
|
||||
errors: results.flatMap(({ error }) => (error ? [error] : [])),
|
||||
renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []),
|
||||
};
|
||||
} catch (error) {
|
||||
eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`);
|
||||
eventLogger.error(error, Transactions.PDF);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
metrics: {
|
||||
...(metrics ?? {}),
|
||||
pages,
|
||||
},
|
||||
data: buffer,
|
||||
errors: results.flatMap(({ error }) => (error ? [error] : [])),
|
||||
renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { assetPath } from '../../../constants';
|
||||
|
||||
export const assetPath = path.resolve(__dirname, 'assets');
|
||||
export const tableBorderWidth = 1;
|
||||
export const pageMarginTop = 40;
|
||||
export const pageMarginBottom = 80;
|
||||
|
@ -21,3 +20,4 @@ export const subheadingMarginTop = 0;
|
|||
export const subheadingMarginBottom = 5;
|
||||
export const subheadingHeight =
|
||||
subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom;
|
||||
export { assetPath };
|
||||
|
|
|
@ -83,6 +83,8 @@ export class PdfMaker {
|
|||
const groupCount = this.content.length;
|
||||
|
||||
// inject a page break for every 2 groups on the page
|
||||
// TODO: Remove this code since we are now using Chromium to drive this
|
||||
// layout via native print functionality.
|
||||
if (groupCount > 0 && groupCount % this.layout.groupCount === 0) {
|
||||
contents = [
|
||||
{
|
||||
|
|
|
@ -12,7 +12,7 @@ import { CaptureResult } from '..';
|
|||
import { PLUGIN_ID } from '../../../common';
|
||||
import { ConfigType } from '../../config';
|
||||
import { ElementPosition } from '../get_element_position_data';
|
||||
import { Screenshot } from '../get_screenshots';
|
||||
import type { Screenshot } from '../types';
|
||||
|
||||
export enum Actions {
|
||||
OPEN_URL = 'open-url',
|
||||
|
@ -25,6 +25,7 @@ export enum Actions {
|
|||
WAIT_RENDER = 'wait-for-render',
|
||||
WAIT_VISUALIZATIONS = 'wait-for-visualizations',
|
||||
GET_SCREENSHOT = 'get-screenshots',
|
||||
PRINT_A4_PDF = 'print-a4-pdf',
|
||||
ADD_IMAGE = 'add-pdf-image',
|
||||
COMPILE = 'compile-pdf',
|
||||
}
|
||||
|
|
33
x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts
Normal file
33
x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts
Normal file
|
@ -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 { Actions, EventLogger } from './event_logger';
|
||||
import type { HeadlessChromiumDriver } from '../browsers';
|
||||
import type { Screenshot } from './types';
|
||||
|
||||
export async function getPdf(
|
||||
browser: HeadlessChromiumDriver,
|
||||
logger: EventLogger,
|
||||
title: string,
|
||||
logo?: string
|
||||
): Promise<Screenshot[]> {
|
||||
logger.kbnLogger.info('printing PDF');
|
||||
|
||||
const spanEnd = logger.logPdfEvent('printing A4 PDF', Actions.PRINT_A4_PDF, 'output');
|
||||
|
||||
const result = [
|
||||
{
|
||||
data: await browser.printA4Pdf({ title, logo }),
|
||||
title: null,
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
|
||||
spanEnd();
|
||||
|
||||
return result;
|
||||
}
|
|
@ -8,23 +8,7 @@
|
|||
import type { HeadlessChromiumDriver } from '../browsers';
|
||||
import { Actions, EventLogger } from './event_logger';
|
||||
import type { ElementsPositionAndAttribute } from './get_element_position_data';
|
||||
|
||||
export interface Screenshot {
|
||||
/**
|
||||
* Screenshot PNG image data.
|
||||
*/
|
||||
data: Buffer;
|
||||
|
||||
/**
|
||||
* Screenshot title.
|
||||
*/
|
||||
title: string | null;
|
||||
|
||||
/**
|
||||
* Screenshot description.
|
||||
*/
|
||||
description: string | null;
|
||||
}
|
||||
import type { Screenshot } from './types';
|
||||
|
||||
export const getScreenshots = async (
|
||||
browser: HeadlessChromiumDriver,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { Headers } from '@kbn/core/server';
|
||||
import { defer, forkJoin, Observable, throwError } from 'rxjs';
|
||||
import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators';
|
||||
import { errors } from '../../common';
|
||||
import { errors, LayoutTypes } from '../../common';
|
||||
import type { Context, HeadlessChromiumDriver } from '../browsers';
|
||||
import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers';
|
||||
import { ConfigType, durationToNumber as toNumber } from '../config';
|
||||
|
@ -18,13 +18,15 @@ import type { ElementsPositionAndAttribute } from './get_element_position_data';
|
|||
import { getElementPositionAndAttributes } from './get_element_position_data';
|
||||
import { getNumberOfItems } from './get_number_of_items';
|
||||
import { getRenderErrors } from './get_render_errors';
|
||||
import type { Screenshot } from './get_screenshots';
|
||||
import type { Screenshot } from './types';
|
||||
import { getScreenshots } from './get_screenshots';
|
||||
import { getPdf } from './get_pdf';
|
||||
import { getTimeRange } from './get_time_range';
|
||||
import { injectCustomCss } from './inject_css';
|
||||
import { openUrl } from './open_url';
|
||||
import { waitForRenderComplete } from './wait_for_render';
|
||||
import { waitForVisualizations } from './wait_for_visualizations';
|
||||
import type { PdfScreenshotOptions } from '../formats';
|
||||
|
||||
type CaptureTimeouts = ConfigType['capture']['timeouts'];
|
||||
export interface PhaseTimeouts extends CaptureTimeouts {
|
||||
|
@ -237,6 +239,26 @@ export class ScreenshotObservableHandler {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a title and time range value look like:
|
||||
*
|
||||
* "[Logs] Web Traffic - Apr 14, 2022 @ 120742.318 to Apr 21, 2022 @ 120742.318"
|
||||
*
|
||||
* Otherwise closest thing to that or a blank string.
|
||||
*/
|
||||
private getTitle(timeRange: null | string): string {
|
||||
return `${(this.options as PdfScreenshotOptions).title ?? ''} ${
|
||||
timeRange ? `- ${timeRange}` : ''
|
||||
}`.trim();
|
||||
}
|
||||
|
||||
private shouldCapturePdf(): boolean {
|
||||
return (
|
||||
this.layout.id === LayoutTypes.PRINT &&
|
||||
(this.options as PdfScreenshotOptions).format === 'pdf'
|
||||
);
|
||||
}
|
||||
|
||||
public getScreenshots() {
|
||||
return (withRenderComplete: Observable<PageSetupResults>) =>
|
||||
withRenderComplete.pipe(
|
||||
|
@ -247,7 +269,14 @@ export class ScreenshotObservableHandler {
|
|||
getDefaultElementPosition(this.layout.getViewport(1));
|
||||
let screenshots: Screenshot[] = [];
|
||||
try {
|
||||
screenshots = await getScreenshots(this.driver, this.eventLogger, elements);
|
||||
screenshots = this.shouldCapturePdf()
|
||||
? await getPdf(
|
||||
this.driver,
|
||||
this.eventLogger,
|
||||
this.getTitle(data.timeRange),
|
||||
(this.options as PdfScreenshotOptions).logo
|
||||
)
|
||||
: await getScreenshots(this.driver, this.eventLogger, elements);
|
||||
} catch (e) {
|
||||
throw new errors.FailedToCaptureScreenshot(e.message);
|
||||
}
|
||||
|
|
23
x-pack/plugins/screenshotting/server/screenshots/types.ts
Normal file
23
x-pack/plugins/screenshotting/server/screenshots/types.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 interface Screenshot {
|
||||
/**
|
||||
* Screenshot PNG image data.
|
||||
*/
|
||||
data: Buffer;
|
||||
|
||||
/**
|
||||
* Screenshot title.
|
||||
*/
|
||||
title: string | null;
|
||||
|
||||
/**
|
||||
* Screenshot description.
|
||||
*/
|
||||
description: string | null;
|
||||
}
|
|
@ -19,7 +19,7 @@ export const PDF_PRESERVE_PIE_VISUALIZATION_6_3 = `/api/reporting/generate/print
|
|||
)}`;
|
||||
|
||||
export const PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 = `/api/reporting/generate/printablePdf?jobParams=${encodeURIComponent(
|
||||
`(browserTimezone:America/New_York,layout:(id:print),objectType:visualization,relativeUrls:!('/app/kibana#/visualize/edit/befdb6b0-3e59-11e8-9fc3-39e49624228e?_g=(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!'Mon+Apr+09+2018+17:56:08+GMT-0400!',mode:absolute,to:!'Wed+Apr+11+2018+17:56:08+GMT-0400!'))&_a=(filters:!!((!'$state!':(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!'!'),uiState:(),vis:(aggs:!!((enabled:!!t,id:!'1!',params:(),schema:metric,type:count),(enabled:!!t,id:!'2!',params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!'1!',otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!'Filter+Test:+animals:+linked+to+search+with+filter!',type:pie))'),title:'Filter Test: animals: linked to search with filter')`
|
||||
`(browserTimezone:America/New_York,layout:(dimensions:(height:588,width:1038),id:preserve_layout),objectType:visualization,relativeUrls:!('/app/kibana#/visualize/edit/befdb6b0-3e59-11e8-9fc3-39e49624228e?_g=(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!'Mon+Apr+09+2018+17:56:08+GMT-0400!',mode:absolute,to:!'Wed+Apr+11+2018+17:56:08+GMT-0400!'))&_a=(filters:!!((!'$state!':(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!'!'),uiState:(),vis:(aggs:!!((enabled:!!t,id:!'1!',params:(),schema:metric,type:count),(enabled:!!t,id:!'2!',params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!'1!',otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!'Filter+Test:+animals:+linked+to+search+with+filter!',type:pie))'),title:'Filter Test: animals: linked to search with filter')`
|
||||
)}`;
|
||||
|
||||
export const JOB_PARAMS_CSV_DEFAULT_SPACE = `/api/reporting/generate/csv_searchsource?jobParams=${encodeURIComponent(
|
||||
|
|
|
@ -74,15 +74,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const usage = await usageAPI.getUsageStats();
|
||||
reportingAPI.expectRecentPdfAppStats(usage, 'visualization', 1);
|
||||
reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1);
|
||||
reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 0);
|
||||
reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 2);
|
||||
reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 1);
|
||||
reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 1);
|
||||
reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0);
|
||||
reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2);
|
||||
|
||||
reportingAPI.expectAllTimePdfAppStats(usage, 'visualization', 1);
|
||||
reportingAPI.expectAllTimePdfAppStats(usage, 'dashboard', 1);
|
||||
reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 0);
|
||||
reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 2);
|
||||
reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 1);
|
||||
reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 1);
|
||||
reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv_searchsource', 0);
|
||||
reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'printable_pdf', 2);
|
||||
});
|
||||
|
|
|
@ -101,31 +101,47 @@ export function createUsageServices({ getService }: FtrProviderContext) {
|
|||
},
|
||||
|
||||
expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) {
|
||||
expect(
|
||||
stats.reporting.last_7_days.printable_pdf.app![app as keyof AvailableTotal['app']]
|
||||
).to.be(count);
|
||||
const actual =
|
||||
stats.reporting.last_7_days.printable_pdf.app![app as keyof AvailableTotal['app']];
|
||||
log.info(`expecting recent ${app} stats to have ${count} printable pdfs (actual: ${actual})`);
|
||||
expect(actual).to.be(count);
|
||||
},
|
||||
|
||||
expectAllTimePdfAppStats(stats: UsageStats, app: string, count: number) {
|
||||
expect(stats.reporting.printable_pdf.app![app as keyof AvailableTotal['app']]).to.be(count);
|
||||
const actual = stats.reporting.printable_pdf.app![app as keyof AvailableTotal['app']];
|
||||
log.info(
|
||||
`expecting all time pdf ${app} stats to have ${count} printable pdfs (actual: ${actual})`
|
||||
);
|
||||
expect(actual).to.be(count);
|
||||
},
|
||||
|
||||
expectRecentPdfLayoutStats(stats: UsageStats, layout: string, count: number) {
|
||||
expect(stats.reporting.last_7_days.printable_pdf.layout![layout as keyof LayoutCounts]).to.be(
|
||||
count
|
||||
);
|
||||
const actual =
|
||||
stats.reporting.last_7_days.printable_pdf.layout![layout as keyof LayoutCounts];
|
||||
log.info(`expecting recent stats to report ${count} ${layout} layouts (actual: ${actual})`);
|
||||
expect(actual).to.be(count);
|
||||
},
|
||||
|
||||
expectAllTimePdfLayoutStats(stats: UsageStats, layout: string, count: number) {
|
||||
expect(stats.reporting.printable_pdf.layout![layout as keyof LayoutCounts]).to.be(count);
|
||||
const actual = stats.reporting.printable_pdf.layout![layout as keyof LayoutCounts];
|
||||
log.info(`expecting all time stats to report ${count} ${layout} layouts (actual: ${actual})`);
|
||||
expect(actual).to.be(count);
|
||||
},
|
||||
|
||||
expectRecentJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) {
|
||||
expect(stats.reporting.last_7_days[jobType as keyof JobTypes].total).to.be(count);
|
||||
const actual = stats.reporting.last_7_days[jobType as keyof JobTypes].total;
|
||||
log.info(
|
||||
`expecting recent stats to report ${count} ${jobType} job types (actual: ${actual})`
|
||||
);
|
||||
expect(actual).to.be(count);
|
||||
},
|
||||
|
||||
expectAllTimeJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) {
|
||||
expect(stats.reporting[jobType as keyof JobTypes].total).to.be(count);
|
||||
const actual = stats.reporting[jobType as keyof JobTypes].total;
|
||||
log.info(
|
||||
`expecting all time stats to report ${count} ${jobType} job types (actual: ${actual})`
|
||||
);
|
||||
expect(actual).to.be(count);
|
||||
},
|
||||
|
||||
getCompletedReportCount(stats: UsageStats) {
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -22202,6 +22202,13 @@ pbkdf2@^3.0.3:
|
|||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
pdfjs-dist@^2.13.216:
|
||||
version "2.13.216"
|
||||
resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.13.216.tgz#251a11c9c8c6db19baacd833a4e6986c517d1ab3"
|
||||
integrity sha512-qn/9a/3IHIKZarTK6ajeeFXBkG15Lg1Fx99PxU09PAU2i874X8mTcHJYyDJxu7WDfNhV6hM7bRQBZU384anoqQ==
|
||||
dependencies:
|
||||
web-streams-polyfill "^3.2.0"
|
||||
|
||||
pdfmake@^0.2.4:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.2.4.tgz#7d58d64b59f8e9b9ed0b2494b17a9d94c575825b"
|
||||
|
@ -29757,6 +29764,11 @@ web-streams-polyfill@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.0.1.tgz#1f836eea307e8f4af15758ee473c7af755eb879e"
|
||||
integrity sha512-M+EmTdszMWINywOZaqpZ6VIEDUmNpRaTOuizF0ZKPjSDC8paMRe/jBBwFv0Yeyn5WYnM5pMqMQa82vpaE+IJRw==
|
||||
|
||||
web-streams-polyfill@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
|
||||
integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue