[AppServices/Examples] Add the example for Reporting integration (#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>
This commit is contained in:
Tim Sullivan 2020-12-29 09:55:48 -07:00 committed by GitHub
parent e699d91b2c
commit b120cb3218
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 310 additions and 19 deletions

View file

@ -0,0 +1,7 @@
module.exports = {
root: true,
extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'],
rules: {
'@kbn/eslint/require-license-header': 'off',
},
};

View file

@ -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)**.
---

View file

@ -0,0 +1,2 @@
export const PLUGIN_ID = 'reportingExample';
export const PLUGIN_NAME = 'reportingExample';

View file

@ -0,0 +1,9 @@
{
"id": "reportingExample",
"version": "1.0.0",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"optionalPlugins": [],
"requiredPlugins": ["reporting", "developerExamples", "navigation"]
}

View file

@ -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);
};

View file

@ -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>
);
};

View file

@ -0,0 +1,6 @@
import { ReportingExamplePlugin } from './plugin';
export function plugin() {
return new ReportingExamplePlugin();
}
export { PluginSetup, PluginStart } from './types';

View file

@ -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() {}
}

View file

@ -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;
}

View file

@ -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" }
]
}

View file

@ -6,6 +6,7 @@
import { LayoutSelectorDictionary } from './types';
export * as constants from './constants';
export { CancellationToken } from './cancellation_token';
export { Poller } from './poller';

View file

@ -20,11 +20,10 @@ 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;
}
@ -32,6 +31,7 @@ interface State {
isStale: boolean;
absoluteUrl: string;
layoutId: string;
objectType: string;
}
class ReportingPanelContentUi extends Component<Props, State> {
@ -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,
};
}
@ -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,
}}
/>
);
@ -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
@ -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') {
@ -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

View file

@ -17,10 +17,9 @@ export interface Props {
toasts: ToastsSetup;
reportType: string;
objectId?: string;
objectType: string;
getJobParams: () => BaseParams;
isDirty: boolean;
onClose: () => void;
isDirty?: boolean;
onClose?: () => void;
}
interface State {
@ -32,8 +31,8 @@ export class ScreenCapturePanelContent extends Component<Props, State> {
constructor(props: Props) {
super(props);
const isPreserveLayoutSupported =
props.reportType !== 'png' && props.objectType !== 'visualization';
const { objectType } = props.getJobParams();
const isPreserveLayoutSupported = props.reportType !== 'png' && objectType !== 'visualization';
this.state = {
isPreserveLayoutSupported,
usePrintLayout: false,
@ -47,7 +46,6 @@ export class ScreenCapturePanelContent extends Component<Props, State> {
toasts={this.props.toasts}
reportType={this.props.reportType}
layoutId={this.getLayout().id}
objectType={this.props.objectType}
objectId={this.props.objectId}
getJobParams={this.getJobParams}
options={this.renderOptions()}

View file

@ -5,6 +5,7 @@
*/
import { PluginInitializerContext } from 'src/core/public';
import { getDefaultLayoutSelectors } from '../common';
import { ScreenCapturePanelContent } from './components/screen_capture_panel_content';
import * as jobCompletionNotifications from './lib/job_completion_notifications';
import { ReportingAPIClient } from './lib/reporting_api_client';
@ -14,10 +15,13 @@ export interface ReportingSetup {
components: {
ScreenCapturePanel: typeof ScreenCapturePanelContent;
};
getDefaultLayoutSelectors: typeof getDefaultLayoutSelectors;
ReportingAPIClient: typeof ReportingAPIClient;
}
export type ReportingStart = ReportingSetup;
export { constants, getDefaultLayoutSelectors } from '../common';
export { ReportingAPIClient, ReportingPublicPlugin as Plugin, jobCompletionNotifications };
export function plugin(initializerContext: PluginInitializerContext) {

View file

@ -24,7 +24,7 @@ import {
import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public';
import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public';
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../common/constants';
import { constants, getDefaultLayoutSelectors } from '../common';
import { durationToNumber } from '../common/schema_utils';
import { JobId, JobSummarySet } from '../common/types';
import { ReportingSetup, ReportingStart } from './';
@ -48,7 +48,7 @@ export interface ClientConfigType {
}
function getStored(): JobId[] {
const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
const sessionValue = sessionStorage.getItem(constants.JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
return sessionValue ? JSON.parse(sessionValue) : [];
}
@ -89,7 +89,11 @@ export class ReportingPublicPlugin
ReportingPublicPluginSetupDendencies,
ReportingPublicPluginStartDendencies
> {
private readonly contract: ReportingStart = { components: { ScreenCapturePanel } };
private readonly contract: ReportingStart = {
components: { ScreenCapturePanel },
getDefaultLayoutSelectors,
ReportingAPIClient,
};
private readonly stop$ = new Rx.ReplaySubject(1);
private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', {
defaultMessage: 'Reporting',

View file

@ -97,7 +97,6 @@ export const csvReportingProvider = ({
toasts={toasts}
reportType="csv"
layoutId={undefined}
objectType={objectType}
objectId={objectId}
getJobParams={getJobParams}
isDirty={isDirty}

View file

@ -135,7 +135,6 @@ export const reportingPDFPNGProvider = ({
apiClient={apiClient}
toasts={toasts}
reportType="png"
objectType={objectType}
objectId={objectId}
getJobParams={getPngJobParams}
isDirty={isDirty}
@ -162,7 +161,6 @@ export const reportingPDFPNGProvider = ({
apiClient={apiClient}
toasts={toasts}
reportType="printablePdf"
objectType={objectType}
objectId={objectId}
getJobParams={getPdfJobParams}
isDirty={isDirty}