From e8d6df96a627ff16511478dc02db9de89e95e05b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 22 Dec 2020 15:52:52 +0100 Subject: [PATCH] remove custom lens component, use embeddable renderer an clean up --- .../embedded_lens_example/public/app.tsx | 11 ++- .../plugins/lens/public/app_plugin/types.ts | 2 +- .../embeddable/embeddable.tsx | 42 ++++++++- .../embeddable/embeddable_component.tsx | 72 ++++---------- .../embeddable_component_loader.tsx | 39 -------- .../embeddable/expression_wrapper.tsx | 94 ++++++++++--------- .../public/editor_frame_service/service.tsx | 13 --- x-pack/plugins/lens/public/index.ts | 9 +- x-pack/plugins/lens/public/plugin.ts | 37 +++++++- x-pack/plugins/lens/public/types.ts | 2 - 10 files changed, 152 insertions(+), 169 deletions(-) delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component_loader.tsx diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index e2e365493552b4..84f0d53604a316 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -16,10 +16,15 @@ import { EuiTitle, } from '@elastic/eui'; import { CoreStart } from 'kibana/public'; -import { LensProps } from '../../../plugins/lens/public'; +import { TypedLensByValueInput } from '../../../plugins/lens/public'; import { StartDependencies } from './plugin'; -function getLensAttributes(defaultIndex: string, color: string): LensProps['attributes'] { +// Generate a Lens state based on some app-specific input parameters. +// `TypedLensByValueInput` can be used for type-safety - it uses the same interfaces as Lens-internal code. +function getLensAttributes( + defaultIndex: string, + color: string +): TypedLensByValueInput['attributes'] { return { visualizationType: 'lnsXY', title: '', @@ -131,7 +136,7 @@ export const App = (props: { core: CoreStart; plugins: StartDependencies }) => { { this.reload(); }); + + // Re-initialize the visualization if either the attributes or the saved object id changes + input$ + .pipe( + distinctUntilChanged((a, b) => + isEqual( + ['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId], + ['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId] + ) + ), + skip(1) + ) + .subscribe(async (input) => { + await this.initializeSavedVis(input); + this.reload(); + }); + + // Update search context and reload on changes related to search + input$ + .pipe( + distinctUntilChanged((a, b) => + isEqual( + [a.filters, a.query, a.timeRange, a.searchSessionId], + [b.filters, b.query, b.timeRange, b.searchSessionId] + ) + ), + skip(1) + ) + .subscribe(async (input) => { + this.onContainerStateChanged(input); + }); } public supportedTriggers() { @@ -261,6 +293,8 @@ export class Embeddable onData$={this.updateActiveData} renderMode={input.renderMode} hasCompatibleActions={this.hasCompatibleActions} + className={input.className} + style={input.style} />, domNode ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component.tsx index 29ac432d132b55..5df0a456b5206d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component.tsx @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef, MutableRefObject, useEffect } from 'react'; -import { RenderMode } from 'src/plugins/expressions'; -import { PaletteOutput } from 'src/plugins/charts/public'; -import { TimeRange } from 'src/plugins/data/public'; -import { LensEmbeddableDeps, LensEmbeddableInput } from './embeddable'; -import { Embeddable } from './embeddable'; +import React from 'react'; +import { + EmbeddableRenderer, + EmbeddableStart, +} from '../../../../../../src/plugins/embeddable/public'; +import { LensByReferenceInput, LensByValueInput } from './embeddable'; import { Document } from '../../persistence'; import { IndexPatternPersistedState } from '../../indexpattern_datasource/types'; import { XYState } from '../../xy_visualization/types'; @@ -29,11 +29,11 @@ type LensAttributes = Omit< }; }; -export type LensProps = Omit & { - palette?: PaletteOutput; - renderMode?: RenderMode; - timeRange?: TimeRange; - height: number; +/** + * Type-safe variant of by value embeddable input for Lens. + * This can be used to hardcode certain Lens chart configurations within another app. + */ +export type TypedLensByValueInput = Omit & { attributes: | LensAttributes<'lnsXY', XYState> | LensAttributes<'lnsPie', PieVisualizationState> @@ -41,49 +41,11 @@ export type LensProps = Omit & { | LensAttributes<'lnsMetric', MetricState>; }; -function LensComponent({ - props: { timeRange, height, ...props }, - deps, -}: { - props: LensProps; - deps: LensEmbeddableDeps; -}) { - const elementRef: MutableRefObject = useRef(null); - const embeddableRef: MutableRefObject = useRef(null); - - useEffect(() => { - (async () => { - if (elementRef.current && embeddableRef.current) { - if (timeRange) { - embeddableRef.current.onContainerStateChanged({ ...props, timeRange }); - } - await embeddableRef.current.initializeSavedVis(props); - embeddableRef.current.render(elementRef.current); - } - })(); - // TODO do not re-render too often - }, [props, timeRange]); +export type EmbeddableComponentProps = TypedLensByValueInput | LensByReferenceInput; - return ( -
{ - if (newElement) { - if (!embeddableRef.current) { - embeddableRef.current = new Embeddable(deps, props); - } - if (timeRange) { - embeddableRef.current.onContainerStateChanged({ ...props, timeRange }); - } - if (elementRef.current !== newElement) { - embeddableRef.current!.render(newElement); - } - elementRef.current = newElement; - } - }} - /> - ); +export function getEmbeddableComponent(embeddableStart: EmbeddableStart) { + return (props: EmbeddableComponentProps) => { + const factory = embeddableStart.getEmbeddableFactory('lens')!; + return ; + }; } - -// eslint-disable-next-line import/no-default-export -export { LensComponent as default }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component_loader.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component_loader.tsx deleted file mode 100644 index 743493a738cd44..00000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_component_loader.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { Suspense, useState, useEffect } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import { LensEmbeddableDeps } from './embeddable'; -import { LensAttributeService } from '../../lens_attribute_service'; -import { LensProps } from './embeddable_component'; - -const LazyLensComponent = React.lazy(() => import('./embeddable_component')); - -export function getEmbeddableComponent({ - attributeService: getAttributeService, - ...deps -}: Omit & { - attributeService: () => Promise; -}) { - return function LensLoader(props: LensProps) { - const [attributeService, setAttributeService] = useState( - undefined - ); - useEffect(() => { - (async () => { - const value = await getAttributeService(); - setAttributeService(value); - })(); - }, []); - if (!attributeService) { - return null; - } - return ( - }> - - - ); - }; -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index e2607886a42196..a2dc3dc0dd2a13 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -30,6 +30,8 @@ export interface ExpressionWrapperProps { ) => void; renderMode?: RenderMode; hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions']; + style?: React.CSSProperties; + className?: string; } export function ExpressionWrapper({ @@ -42,53 +44,57 @@ export function ExpressionWrapper({ onData$, renderMode, hasCompatibleActions, + style, + className, }: ExpressionWrapperProps) { return ( - {expression === null || expression === '' ? ( - - - - - - - - - - - ) : ( -
- ( -
- - - - - - - {getOriginalRequestErrorMessage(error) || errorMessage} - - - -
- )} - onEvent={handleEvent} - hasCompatibleActions={hasCompatibleActions} - /> -
- )} +
+ {expression === null || expression === '' ? ( + + + + + + + + + + + ) : ( +
+ ( +
+ + + + + + + {getOriginalRequestErrorMessage(error) || errorMessage} + + + +
+ )} + onEvent={handleEvent} + hasCompatibleActions={hasCompatibleActions} + /> +
+ )} +
); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 74fc81902ad703..0562e9bf4d32e4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -28,7 +28,6 @@ import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DashboardStart } from '../../../../../src/plugins/dashboard/public'; import { LensAttributeService } from '../lens_attribute_service'; -import { getEmbeddableComponent } from './embeddable/embeddable_component_loader'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; @@ -65,7 +64,6 @@ export class EditorFrameService { private readonly datasources: Array Promise)> = []; private readonly visualizations: Array Promise)> = []; - private getAttributeService: (() => Promise) | null = null; /** * This method takes a Lens saved object as returned from the persistence helper, @@ -89,7 +87,6 @@ export class EditorFrameService { plugins: EditorFrameSetupPlugins, getAttributeService: () => Promise ): EditorFrameSetup { - this.getAttributeService = getAttributeService; plugins.expressions.registerFunction(() => mergeTables); const getStartServices = async (): Promise => { @@ -190,16 +187,6 @@ export class EditorFrameService { return { createInstance, - EmbeddableComponent: getEmbeddableComponent({ - indexPatternService: plugins.data.indexPatterns, - timefilter: plugins.data.query.timefilter.timefilter, - expressionRenderer: plugins.expressions.ReactExpressionRenderer, - editable: false, - basePath: core.http.basePath, - getTrigger: plugins.uiActions?.getTrigger, - documentToExpression: this.documentToExpression.bind(this), - attributeService: this.getAttributeService!, - }), }; } } diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 78d7e322d88cae..217173b06d1d5c 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -6,14 +6,15 @@ import { LensPlugin } from './plugin'; -export type LensPublicStart = ReturnType; - -export * from './types'; -export { LensProps } from './editor_frame_service/embeddable/embeddable_component'; +export { + EmbeddableComponentProps, + TypedLensByValueInput, +} from './editor_frame_service/embeddable/embeddable_component'; export { XYState } from './xy_visualization/types'; export { PieVisualizationState } from './pie_visualization/types'; export { DatatableVisualizationState } from './datatable_visualization/visualization'; export { State as MetricState } from './metric_visualization/types'; export { IndexPatternPersistedState } from './indexpattern_datasource/types'; +export { LensPublicStart } from './plugin'; export const plugin = () => new LensPlugin(); diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 0298a3760d5395..9f51d28262029f 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -38,7 +38,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { PLUGIN_ID_OSS } from '../../../../src/plugins/lens_oss/common/constants'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; @@ -47,6 +47,10 @@ import { getSearchProvider } from './search_provider'; import { LensAttributeService } from './lens_attribute_service'; import { LensEmbeddableInput } from './editor_frame_service/embeddable'; +import { + EmbeddableComponentProps, + getEmbeddableComponent, +} from './editor_frame_service/embeddable/embeddable_component'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -70,6 +74,27 @@ export interface LensPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; } +export interface LensPublicStart { + /** + * React component which can be used to embed a Lens visualization into another application. + * See `x-pack/examples/embedded_lens_example` for exemplary usage. + * + * This API might undergo breaking changes even in minor versions. + * + * @experimental + */ + EmbeddableComponent: React.ComponentType; + /** + * Method which navigates to the Lens editor, loading the state specified by the `input` parameter. + * See `x-pack/examples/embedded_lens_example` for exemplary usage. + * + * This API might undergo breaking changes even in minor versions. + * + * @experimental + */ + navigateToPrefilledEditor: (input: LensEmbeddableInput) => void; +} + export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; @@ -176,7 +201,7 @@ export class LensPlugin { urlForwarding.forwardApp('lens', 'lens'); } - start(core: CoreStart, startDependencies: LensPluginStartDependencies) { + start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { const frameStart = this.editorFrameService.start(core, startDependencies); this.createEditorFrame = frameStart.createInstance; // unregisters the OSS alias @@ -191,13 +216,17 @@ export class LensPlugin { ); return { - EmbeddableComponent: frameStart.EmbeddableComponent, + EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable), navigateToPrefilledEditor: (input: LensEmbeddableInput) => { if (input.timeRange) { startDependencies.data.query.timefilter.timefilter.setTime(input.timeRange); } - const transfer = new EmbeddableStateTransfer(core.application.navigateToApp); + const transfer = new EmbeddableStateTransfer( + core.application.navigateToApp, + core.application.currentAppId$ + ); transfer.navigateToEditor('lens', { + path: getEditPath(undefined), state: { originatingApp: '', valueInput: input, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 72ae2bd536e91b..4d29e13e9d11f6 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -20,7 +20,6 @@ import { Document } from './persistence'; import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; import { TriggerContext, VisualizeFieldContext } from '../../../../src/plugins/ui_actions/public'; -import type { LensProps } from './editor_frame_service/embeddable/embeddable_component'; import { ROW_CLICK_TRIGGER } from '../../../../src/plugins/ui_actions/public'; import { SELECT_RANGE_TRIGGER, @@ -76,7 +75,6 @@ export interface EditorFrameSetup { export interface EditorFrameStart { createInstance: () => Promise; - EmbeddableComponent: React.ComponentType; } export interface TableSuggestionColumn {