Skip to content

Commit

Permalink
[AppServices/Examples] Add the example for Reporting integration (ela…
Browse files Browse the repository at this point in the history
…stic#82091)

* Add developer example for Reporting

Refactor Reporting plugin to have shareable services

* Update plugin.ts

* use constant

* add more description to using reporting as a service

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
tsullivan and kibanamachine committed Dec 29, 2020
1 parent 8a2cd92 commit cac5ab5
Show file tree
Hide file tree
Showing 17 changed files with 310 additions and 19 deletions.
7 changes: 7 additions & 0 deletions x-pack/examples/reporting_example/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
root: true,
extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'],
rules: {
'@kbn/eslint/require-license-header': 'off',
},
};
33 changes: 33 additions & 0 deletions x-pack/examples/reporting_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Example Reporting integration!

Use this example code to understand how to add a "Generate Report" button to a
Kibana page. This simple example shows that the end-to-end functionality of
generating a screenshot report of a page just requires you to render a React
component that you import from the Reportinng plugin.

A "reportable" Kibana page is one that has an **alternate version to show the data in a "screenshot-friendly" way**. The alternate version can be reached at a variation of the page's URL that the App team builds.

A "screenshot-friendly" page has **all interactive features turned off**. These are typically notifications, popups, tooltips, controls, autocomplete libraries, etc.

Turning off these features **keeps glitches out of the screenshot**, and makes the server-side headless browser **run faster and use less RAM**.

The URL that Reporting captures is controlled by the application, is a part of
a "jobParams" object that gets passed to the React component imported from
Reporting. The job params give the app control over the end-resulting report:

- Layout
- Page dimensions
- DOM attributes to select where the visualization container(s) is/are. The App team must add the attributes to DOM elements in their app.
- DOM events that the page fires off and signals when the rendering is done. The App team must implement triggering the DOM events around rendering the data in their app.
- Export type definition
- Processes the jobParams into output data, which is stored in Elasticsearch in the Reporting system index.
- Export type definitions are registered with the Reporting plugin at setup time.

The existing export type definitions are PDF, PNG, and CSV. They should be
enough for nearly any use case.

If the existing options are too limited for a future use case, the AppServices
team can assist the App team to implement a custom export type definition of
their own, and register it using the Reporting plugin API **(documentation coming soon)**.

---
2 changes: 2 additions & 0 deletions x-pack/examples/reporting_example/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const PLUGIN_ID = 'reportingExample';
export const PLUGIN_NAME = 'reportingExample';
9 changes: 9 additions & 0 deletions x-pack/examples/reporting_example/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "reportingExample",
"version": "1.0.0",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"optionalPlugins": [],
"requiredPlugins": ["reporting", "developerExamples", "navigation"]
}
18 changes: 18 additions & 0 deletions x-pack/examples/reporting_example/public/application.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
import { StartDeps } from './types';
import { ReportingExampleApp } from './components/app';

export const renderApp = (
coreStart: CoreStart,
startDeps: StartDeps,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<ReportingExampleApp basename={appBasePath} {...coreStart} {...startDeps} />,
element
);

return () => ReactDOM.unmountComponentAtNode(element);
};
130 changes: 130 additions & 0 deletions x-pack/examples/reporting_example/public/components/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
EuiCard,
EuiCode,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n/react';
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import * as Rx from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { CoreStart } from '../../../../../src/core/public';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public';
import { JobParamsPDF } from '../../../../plugins/reporting/server/export_types/printable_pdf/types';

interface ReportingExampleAppDeps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
navigation: NavigationPublicPluginStart;
reporting: ReportingStart;
}

const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana'];

export const ReportingExampleApp = ({
basename,
notifications,
http,
reporting,
}: ReportingExampleAppDeps) => {
const { getDefaultLayoutSelectors, ReportingAPIClient } = reporting;
const [logos, setLogos] = useState<string[]>([]);

useEffect(() => {
Rx.timer(2200)
.pipe(takeWhile(() => logos.length < sourceLogos.length))
.subscribe(() => {
setLogos([...sourceLogos.slice(0, logos.length + 1)]);
});
});

const getPDFJobParams = (): JobParamsPDF => {
return {
layout: {
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
selectors: getDefaultLayoutSelectors(),
},
relativeUrls: ['/app/reportingExample#/intended-visualization'],
objectType: 'develeloperExample',
title: 'Reporting Developer Example',
};
};

// Render the application DOM.
return (
<Router basename={basename}>
<I18nProvider>
<EuiPage>
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>Reporting Example</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiText>
<p>
Use the <EuiCode>ReportingStart.components.ScreenCapturePanel</EuiCode>{' '}
component to add the Reporting panel to your page.
</p>

<EuiHorizontalRule />

<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiPanel>
<reporting.components.ScreenCapturePanel
apiClient={new ReportingAPIClient(http)}
toasts={notifications.toasts}
reportType={constants.PDF_REPORT_TYPE}
getJobParams={getPDFJobParams}
objectId="Visualization:Id:ToEnsure:Visualization:IsSaved"
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>

<EuiHorizontalRule />

<p>
The logos below are in a <EuiCode>data-shared-items-container</EuiCode> element
for Reporting.
</p>

<div data-shared-items-container data-shared-items-count="4">
<EuiFlexGroup gutterSize="l">
{logos.map((item, index) => (
<EuiFlexItem key={index} data-shared-item>
<EuiCard
icon={<EuiIcon size="xxl" type={`logo${item}`} />}
title={`Elastic ${item}`}
description="Example of a card's description. Stick to one or two sentences."
onClick={() => {}}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</div>
</EuiText>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</I18nProvider>
</Router>
);
};
6 changes: 6 additions & 0 deletions x-pack/examples/reporting_example/public/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ReportingExamplePlugin } from './plugin';

export function plugin() {
return new ReportingExamplePlugin();
}
export { PluginSetup, PluginStart } from './types';
41 changes: 41 additions & 0 deletions x-pack/examples/reporting_example/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
AppMountParameters,
AppNavLinkStatus,
CoreSetup,
CoreStart,
Plugin,
} from '../../../../src/core/public';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { SetupDeps, StartDeps } from './types';

export class ReportingExamplePlugin implements Plugin<void, void, {}, {}> {
public setup(core: CoreSetup, { developerExamples, ...depsSetup }: SetupDeps): void {
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
// Load application bundle
const { renderApp } = await import('./application');
const [coreStart, depsStart] = (await core.getStartServices()) as [
CoreStart,
StartDeps,
unknown
];
// Render the application
return renderApp(coreStart, { ...depsSetup, ...depsStart }, params);
},
});

// Show the app in Developer Examples
developerExamples.register({
appId: 'reportingExample',
title: 'Reporting integration',
description: 'Demonstrate how to put an Export button on a page and generate reports.',
});
}

public start() {}

public stop() {}
}
16 changes: 16 additions & 0 deletions x-pack/examples/reporting_example/public/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public';
import { ReportingStart } from '../../../plugins/reporting/public';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginStart {}

export interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
}
export interface StartDeps {
navigation: NavigationPublicPluginStart;
reporting: ReportingStart;
}
19 changes: 19 additions & 0 deletions x-pack/examples/reporting_example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target"
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"common/**/*.ts",
"../../../typings/**/*",
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" }
]
}

1 change: 1 addition & 0 deletions x-pack/plugins/reporting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { LayoutSelectorDictionary } from './types';

export * as constants from './constants';
export { CancellationToken } from './cancellation_token';
export { Poller } from './poller';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ export interface Props {
reportType: string;
layoutId: string | undefined;
objectId?: string;
objectType: string;
getJobParams: () => BaseParams;
options?: ReactElement<any>;
isDirty: boolean;
onClose: () => void;
isDirty?: boolean;
onClose?: () => void;
intl: InjectedIntl;
}

interface State {
isStale: boolean;
absoluteUrl: string;
layoutId: string;
objectType: string;
}

class ReportingPanelContentUi extends Component<Props, State> {
Expand All @@ -40,10 +40,14 @@ class ReportingPanelContentUi extends Component<Props, State> {
constructor(props: Props) {
super(props);

// Get objectType from job params
const { objectType } = props.getJobParams();

this.state = {
isStale: false,
absoluteUrl: this.getAbsoluteReportGenerationUrl(props),
layoutId: '',
objectType,
};
}

Expand Down Expand Up @@ -104,7 +108,7 @@ class ReportingPanelContentUi extends Component<Props, State> {
description="Here 'reportingType' can be 'PDF' or 'CSV'"
values={{
reportingType: this.prettyPrintReportingType(),
objectType: this.props.objectType,
objectType: this.state.objectType,
}}
/>
);
Expand Down Expand Up @@ -209,7 +213,7 @@ class ReportingPanelContentUi extends Component<Props, State> {
id: 'xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle',
defaultMessage: 'Queued report for {objectType}',
},
{ objectType: this.props.objectType }
{ objectType: this.state.objectType }
),
text: toMountPoint(
<FormattedMessage
Expand All @@ -219,7 +223,9 @@ class ReportingPanelContentUi extends Component<Props, State> {
),
'data-test-subj': 'queueReportSuccess',
});
this.props.onClose();
if (this.props.onClose) {
this.props.onClose();
}
})
.catch((error: any) => {
if (error.message === 'not exportable') {
Expand All @@ -229,7 +235,7 @@ class ReportingPanelContentUi extends Component<Props, State> {
id: 'xpack.reporting.panelContent.whatCanBeExportedWarningTitle',
defaultMessage: 'Only saved {objectType} can be exported',
},
{ objectType: this.props.objectType }
{ objectType: this.state.objectType }
),
text: toMountPoint(
<FormattedMessage
Expand Down
Loading

0 comments on commit cac5ab5

Please sign in to comment.