From b120cb321827de222c7391e8ca8cb53e6cd65a98 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 29 Dec 2020 09:55:48 -0700 Subject: [PATCH] [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> --- .../examples/reporting_example/.eslintrc.js | 7 + x-pack/examples/reporting_example/README.md | 33 +++++ .../reporting_example/common/index.ts | 2 + x-pack/examples/reporting_example/kibana.json | 9 ++ .../reporting_example/public/application.tsx | 18 +++ .../public/components/app.tsx | 130 ++++++++++++++++++ .../reporting_example/public/index.ts | 6 + .../reporting_example/public/plugin.ts | 41 ++++++ .../reporting_example/public/types.ts | 16 +++ .../examples/reporting_example/tsconfig.json | 19 +++ x-pack/plugins/reporting/common/index.ts | 1 + .../components/reporting_panel_content.tsx | 20 ++- .../screen_capture_panel_content.tsx | 10 +- x-pack/plugins/reporting/public/index.ts | 4 + x-pack/plugins/reporting/public/plugin.ts | 10 +- .../register_csv_reporting.tsx | 1 - .../register_pdf_png_reporting.tsx | 2 - 17 files changed, 310 insertions(+), 19 deletions(-) create mode 100644 x-pack/examples/reporting_example/.eslintrc.js create mode 100755 x-pack/examples/reporting_example/README.md create mode 100644 x-pack/examples/reporting_example/common/index.ts create mode 100644 x-pack/examples/reporting_example/kibana.json create mode 100644 x-pack/examples/reporting_example/public/application.tsx create mode 100644 x-pack/examples/reporting_example/public/components/app.tsx create mode 100644 x-pack/examples/reporting_example/public/index.ts create mode 100644 x-pack/examples/reporting_example/public/plugin.ts create mode 100644 x-pack/examples/reporting_example/public/types.ts create mode 100644 x-pack/examples/reporting_example/tsconfig.json diff --git a/x-pack/examples/reporting_example/.eslintrc.js b/x-pack/examples/reporting_example/.eslintrc.js new file mode 100644 index 00000000000000..b267018448ba62 --- /dev/null +++ b/x-pack/examples/reporting_example/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@kbn/eslint/require-license-header': 'off', + }, +}; diff --git a/x-pack/examples/reporting_example/README.md b/x-pack/examples/reporting_example/README.md new file mode 100755 index 00000000000000..186a3fa37f93b0 --- /dev/null +++ b/x-pack/examples/reporting_example/README.md @@ -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)**. + +--- diff --git a/x-pack/examples/reporting_example/common/index.ts b/x-pack/examples/reporting_example/common/index.ts new file mode 100644 index 00000000000000..e47604bd7b8231 --- /dev/null +++ b/x-pack/examples/reporting_example/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'reportingExample'; +export const PLUGIN_NAME = 'reportingExample'; diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json new file mode 100644 index 00000000000000..22768338aec379 --- /dev/null +++ b/x-pack/examples/reporting_example/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "reportingExample", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "optionalPlugins": [], + "requiredPlugins": ["reporting", "developerExamples", "navigation"] +} diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx new file mode 100644 index 00000000000000..1bb944faad3eae --- /dev/null +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -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( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx new file mode 100644 index 00000000000000..8f7176675f2c2d --- /dev/null +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -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([]); + + 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 ( + + + + + + +

Reporting Example

+
+
+ + + +

+ Use the ReportingStart.components.ScreenCapturePanel{' '} + component to add the Reporting panel to your page. +

+ + + + + + + + + + + + + +

+ The logos below are in a data-shared-items-container element + for Reporting. +

+ +
+ + {logos.map((item, index) => ( + + } + title={`Elastic ${item}`} + description="Example of a card's description. Stick to one or two sentences." + onClick={() => {}} + /> + + ))} + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/x-pack/examples/reporting_example/public/index.ts b/x-pack/examples/reporting_example/public/index.ts new file mode 100644 index 00000000000000..a490cf96895be9 --- /dev/null +++ b/x-pack/examples/reporting_example/public/index.ts @@ -0,0 +1,6 @@ +import { ReportingExamplePlugin } from './plugin'; + +export function plugin() { + return new ReportingExamplePlugin(); +} +export { PluginSetup, PluginStart } from './types'; diff --git a/x-pack/examples/reporting_example/public/plugin.ts b/x-pack/examples/reporting_example/public/plugin.ts new file mode 100644 index 00000000000000..95b4d917f549ae --- /dev/null +++ b/x-pack/examples/reporting_example/public/plugin.ts @@ -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 { + 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() {} +} diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts new file mode 100644 index 00000000000000..d574053266fae8 --- /dev/null +++ b/x-pack/examples/reporting_example/public/types.ts @@ -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; +} diff --git a/x-pack/examples/reporting_example/tsconfig.json b/x-pack/examples/reporting_example/tsconfig.json new file mode 100644 index 00000000000000..ef727b3368b12b --- /dev/null +++ b/x-pack/examples/reporting_example/tsconfig.json @@ -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" } + ] +} + diff --git a/x-pack/plugins/reporting/common/index.ts b/x-pack/plugins/reporting/common/index.ts index 0be6ab66827746..467cc9f4d04cfe 100644 --- a/x-pack/plugins/reporting/common/index.ts +++ b/x-pack/plugins/reporting/common/index.ts @@ -6,6 +6,7 @@ import { LayoutSelectorDictionary } from './types'; +export * as constants from './constants'; export { CancellationToken } from './cancellation_token'; export { Poller } from './poller'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 7f48b5d9101baa..bbdc2e1aebe776 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -20,11 +20,10 @@ export interface Props { reportType: string; layoutId: string | undefined; objectId?: string; - objectType: string; getJobParams: () => BaseParams; options?: ReactElement; - 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 { @@ -40,10 +40,14 @@ class ReportingPanelContentUi extends Component { 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 { 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 { id: 'xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle', defaultMessage: 'Queued report for {objectType}', }, - { objectType: this.props.objectType } + { objectType: this.state.objectType } ), text: toMountPoint( { ), '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 { id: 'xpack.reporting.panelContent.whatCanBeExportedWarningTitle', defaultMessage: 'Only saved {objectType} can be exported', }, - { objectType: this.props.objectType } + { objectType: this.state.objectType } ), text: toMountPoint( BaseParams; - isDirty: boolean; - onClose: () => void; + isDirty?: boolean; + onClose?: () => void; } interface State { @@ -32,8 +31,8 @@ export class ScreenCapturePanelContent extends Component { 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 { 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()} diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index f15a5ca4817571..39013ba1713735 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -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) { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 52362b4c68734f..3a5a6a50616aa4 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -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', diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index e90d6786b58f21..7126762c0f4ee9 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -97,7 +97,6 @@ export const csvReportingProvider = ({ toasts={toasts} reportType="csv" layoutId={undefined} - objectType={objectType} objectId={objectId} getJobParams={getJobParams} isDirty={isDirty} diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index d17d4af3c01029..f0f379ae032aea 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -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}