[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:
parent
e699d91b2c
commit
b120cb3218
7
x-pack/examples/reporting_example/.eslintrc.js
Normal file
7
x-pack/examples/reporting_example/.eslintrc.js
Normal 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',
|
||||
},
|
||||
};
|
33
x-pack/examples/reporting_example/README.md
Executable file
33
x-pack/examples/reporting_example/README.md
Executable 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)**.
|
||||
|
||||
---
|
2
x-pack/examples/reporting_example/common/index.ts
Normal file
2
x-pack/examples/reporting_example/common/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const PLUGIN_ID = 'reportingExample';
|
||||
export const PLUGIN_NAME = 'reportingExample';
|
9
x-pack/examples/reporting_example/kibana.json
Normal file
9
x-pack/examples/reporting_example/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "reportingExample",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"optionalPlugins": [],
|
||||
"requiredPlugins": ["reporting", "developerExamples", "navigation"]
|
||||
}
|
18
x-pack/examples/reporting_example/public/application.tsx
Normal file
18
x-pack/examples/reporting_example/public/application.tsx
Normal 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);
|
||||
};
|
130
x-pack/examples/reporting_example/public/components/app.tsx
Normal file
130
x-pack/examples/reporting_example/public/components/app.tsx
Normal 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>
|
||||
);
|
||||
};
|
6
x-pack/examples/reporting_example/public/index.ts
Normal file
6
x-pack/examples/reporting_example/public/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { ReportingExamplePlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new ReportingExamplePlugin();
|
||||
}
|
||||
export { PluginSetup, PluginStart } from './types';
|
41
x-pack/examples/reporting_example/public/plugin.ts
Normal file
41
x-pack/examples/reporting_example/public/plugin.ts
Normal 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() {}
|
||||
}
|
16
x-pack/examples/reporting_example/public/types.ts
Normal file
16
x-pack/examples/reporting_example/public/types.ts
Normal 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;
|
||||
}
|
19
x-pack/examples/reporting_example/tsconfig.json
Normal file
19
x-pack/examples/reporting_example/tsconfig.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { LayoutSelectorDictionary } from './types';
|
||||
|
||||
export * as constants from './constants';
|
||||
export { CancellationToken } from './cancellation_token';
|
||||
export { Poller } from './poller';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -97,7 +97,6 @@ export const csvReportingProvider = ({
|
|||
toasts={toasts}
|
||||
reportType="csv"
|
||||
layoutId={undefined}
|
||||
objectType={objectType}
|
||||
objectId={objectId}
|
||||
getJobParams={getJobParams}
|
||||
isDirty={isDirty}
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue