From f744dd67920b1fdd97417258c890882919762048 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 9 Sep 2021 11:22:55 -0700 Subject: [PATCH] Adds save and return flow to Canvas Updates existing embeddable with updates on edit Select incoming embeddable element on load Fixed ts errors Added use incoming embeddable hook Fixed eslint errors Added comment Fixed typo Fixed story --- .../expression_types/embeddable.ts | 3 +- .../functions/external/embeddable.ts | 16 +---- .../functions/external/saved_lens.ts | 3 +- .../renderers/embeddable/embeddable.tsx | 35 +++++++---- .../canvas/common/lib/embeddable_dataurl.ts | 2 +- .../components/embeddable_flyout/flyout.tsx | 5 +- .../public/components/hooks/workpad/index.tsx | 2 + .../hooks/workpad/use_incoming_embeddable.ts | 63 +++++++++++++++++++ .../public/components/workpad/workpad.tsx | 4 ++ .../__stories__/element_menu.stories.tsx | 1 + .../element_menu/element_menu.component.tsx | 20 ++++++ .../element_menu/element_menu.tsx | 34 +++++++++- .../routes/workpad/hooks/use_workpad.ts | 20 ++++-- .../canvas/public/services/embeddables.ts | 6 +- .../public/services/kibana/embeddables.ts | 1 + .../public/services/stubs/embeddables.ts | 1 + x-pack/plugins/canvas/types/embeddables.ts | 16 +++++ x-pack/plugins/canvas/types/index.ts | 1 + 18 files changed, 193 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts create mode 100644 x-pack/plugins/canvas/types/embeddables.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index ac2e8e8babee1e6..f1ede936c6ace44 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -6,12 +6,11 @@ */ import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions'; -import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/'; +import { EmbeddableInput } from '../../types'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; export { EmbeddableTypes, EmbeddableInput }; - export interface EmbeddableExpression { /** * The type of the expression result diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts index 6642a6e64fdaef3..f846f23ff7f73dc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts @@ -6,14 +6,8 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { TimeRange } from 'src/plugins/data/public'; -import { Filter } from '@kbn/es-query'; -import { ExpressionValueFilter } from '../../../types'; -import { - EmbeddableExpressionType, - EmbeddableExpression, - EmbeddableInput as Input, -} from '../../expression_types'; +import { ExpressionValueFilter, EmbeddableInput } from '../../../types'; +import { EmbeddableExpressionType, EmbeddableExpression } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; @@ -29,12 +23,6 @@ const defaultTimeRange = { to: 'now', }; -export type EmbeddableInput = Input & { - timeRange?: TimeRange; - filters?: Filter[]; - savedObjectId: string; -}; - const baseEmbeddableInput = { timeRange: defaultTimeRange, disableTriggers: true, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 082a69a874cae27..5dcad702bcf6978 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -9,9 +9,8 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { PaletteOutput } from 'src/plugins/charts/common'; import { Filter as DataFilter } from '@kbn/es-query'; import { TimeRange } from 'src/plugins/data/common'; -import { EmbeddableInput } from 'src/plugins/embeddable/common'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; -import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, EmbeddableInput, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 2db4c78ca4b3259..953746c28084061 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -13,12 +13,12 @@ import { IEmbeddable, EmbeddableFactory, EmbeddableFactoryNotFoundError, + isErrorEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; -import { EmbeddableInput } from '../../expression_types'; -import { RendererFactory } from '../../../types'; +import { RendererFactory, EmbeddableInput } from '../../../types'; import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; const { embeddable: strings } = RendererStrings; @@ -71,16 +71,27 @@ export const embeddableRendererFactory = ( throw new EmbeddableFactoryNotFoundError(embeddableType); } - const embeddablePromise = factory - .createFromSavedObject(input.id, input) - .then((embeddable) => { - // stores embeddable in registrey - embeddablesRegistry[uniqueId] = embeddable; - return embeddable; - }); - embeddablesRegistry[uniqueId] = embeddablePromise; - - const embeddableObject = await (async () => embeddablePromise)(); + const embeddableInput = { ...input, id: uniqueId }; + + const embeddablePromise = input.savedObjectId + ? factory + .createFromSavedObject(input.savedObjectId, embeddableInput) + .then((embeddable) => { + // stores embeddable in registrey + embeddablesRegistry[uniqueId] = embeddable; + return embeddable; + }) + : factory.create(embeddableInput).then((embeddable) => { + if (!embeddable || isErrorEmbeddable(embeddable)) { + return; + } + // stores embeddable in registry + embeddablesRegistry[uniqueId] = embeddable as IEmbeddable; + return embeddable; + }); + embeddablesRegistry[uniqueId] = embeddablePromise as Promise; + + const embeddableObject = (await (async () => embeddablePromise)()) as IEmbeddable; const palettes = await plugins.charts.palettes.getPalettes(); diff --git a/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts index d8246449f90ba9d..e76dedfe63b14a5 100644 --- a/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts +++ b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EmbeddableInput } from '../../canvas_plugin_src/expression_types'; +import { EmbeddableInput } from '../../types'; export const encode = (input: Partial) => Buffer.from(JSON.stringify(input)).toString('base64'); diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 5985c997478702d..4dc8d963932d8f4 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -85,9 +85,10 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ }; // If by-value is enabled, we'll handle both by-reference and by-value embeddables - // with the new generic `embeddable` function + // with the new generic `embeddable` function. + // Otherwise we fallback to the embeddable type specific expressions. if (isByValueEnabled) { - const config = encode({ id }); + const config = encode({ savedObjectId: id }); partialElement.expression = `embeddable config="${config}" type="${type}" | render`; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx index 50d527036560adf..ffd5b095b12e522 100644 --- a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx @@ -6,3 +6,5 @@ */ export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad'; + +export { useIncomingEmbeddable } from './use_incoming_embeddable'; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts new file mode 100644 index 000000000000000..27f68ca15a20002 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { CANVAS_APP } from '../../../../common/lib'; +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { useEmbeddablesService, useLabsService } from '../../../services'; +// @ts-expect-error unconverted file +import { addElement } from '../../../state/actions/elements'; +// @ts-expect-error unconverted file +import { selectToplevelNodes } from '../../../state/actions/transient'; + +import { + updateEmbeddableExpression, + fetchEmbeddableRenderable, +} from '../../../state/actions/embeddable'; +import { clearValue } from '../../../state/actions/resolved_args'; + +export const useIncomingEmbeddable = (pageId: string) => { + const embeddablesService = useEmbeddablesService(); + const labsService = useLabsService(); + const dispatch = useDispatch(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const stateTransferService = embeddablesService.getStateTransfer(); + + // fetch incoming embeddable from state transfer service. + const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true); + + useEffect(() => { + if (isByValueEnabled && incomingEmbeddable) { + const { embeddableId, input, type } = incomingEmbeddable; + + const config = encode(input); + const expression = `embeddable config="${config}" + type="${type}" +| render`; + + if (embeddableId) { + // clear out resolved arg for old embeddable + const argumentPath = [embeddableId, 'expressionRenderable']; + dispatch(clearValue({ path: argumentPath })); + + // update existing embeddable expression + dispatch( + updateEmbeddableExpression({ elementId: embeddableId, embeddableExpression: expression }) + ); + + // update resolved args + dispatch(fetchEmbeddableRenderable(embeddableId)); + + // select new embeddable element + dispatch(selectToplevelNodes([embeddableId])); + } else { + dispatch(addElement(pageId, { expression })); + } + } + }, [dispatch, pageId, incomingEmbeddable, isByValueEnabled]); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx index 622c885b6ef281d..bc867333b648f93 100644 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx @@ -27,6 +27,7 @@ import { WorkpadRoutingContext } from '../../routes/workpad'; import { usePlatformService } from '../../services'; import { Workpad as WorkpadComponent, Props } from './workpad.component'; import { State } from '../../../types'; +import { useIncomingEmbeddable } from '../hooks'; type ContainerProps = Pick; @@ -58,6 +59,9 @@ export const Workpad: FC = (props) => { }; }); + const pageId = propsFromState.pages[propsFromState.selectedPageNumber - 1].id; + useIncomingEmbeddable(pageId); + const fetchAllRenderables = useCallback(() => { dispatch(fetchAllRenderablesAction()); }, [dispatch]); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx index 80280d55a4e1c8f..390d4a75ea0ae29 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx @@ -136,5 +136,6 @@ storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => ( elements={testElements} addElement={action('addElement')} renderEmbedPanel={mockRenderEmbedPanel} + createNewEmbeddable={action('createNewEmbeddable')} /> )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index 937912570b77fbf..a8713f380d6a41c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -22,6 +22,7 @@ import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { AssetManager } from '../../asset_manager'; import { SavedElementsModal } from '../../saved_elements_modal'; +import { useLabsService } from '../../../services'; interface CategorizedElementLists { [key: string]: ElementSpec[]; @@ -129,13 +130,20 @@ export interface Props { * Renders embeddable flyout */ renderEmbedPanel: (onClose: () => void) => JSX.Element; + /** + * Crete new embeddable + */ + createNewEmbeddable: () => void; } export const ElementMenu: FunctionComponent = ({ elements, addElement, renderEmbedPanel, + createNewEmbeddable, }) => { + const labsService = useLabsService(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); const [isAssetModalVisible, setAssetModalVisible] = useState(false); const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); @@ -223,6 +231,18 @@ export const ElementMenu: FunctionComponent = ({ closePopover(); }, }, + // TODO: Remove this menu option. This is a temporary menu options just for testing, + // will be removed once toolbar is implemented + isByValueEnabled + ? { + name: 'Lens', + icon: , + onClick: () => { + createNewEmbeddable(); + closePopover(); + }, + } + : {}, ], }; }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index 5b5491a7c6454a0..8b891a625101460 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -5,10 +5,14 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; +import { useLocation } from 'react-router-dom'; + import { compose, withProps } from 'recompose'; import { Dispatch } from 'redux'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric'; +import { CANVAS_APP } from '../../../../common/lib'; import { State, ElementSpec } from '../../../../types'; // @ts-expect-error untyped local import { elementsRegistry } from '../../../lib/elements_registry'; @@ -17,6 +21,7 @@ import { ElementMenu as Component, Props as ComponentProps } from './element_men import { addElement } from '../../../state/actions/elements'; import { getSelectedPage } from '../../../state/selectors/workpad'; import { AddEmbeddablePanel } from '../../embeddable_flyout'; +import { useEmbeddablesService } from '../../../services'; interface StateProps { pageId: string; @@ -42,7 +47,32 @@ const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ renderEmbedPanel: (onClose: () => void) => , }); +const ElementMenuComponent = (props: ComponentProps) => { + const embeddablesService = useEmbeddablesService(); + const stateTransferService = embeddablesService.getStateTransfer(); + const { pathname, search } = useLocation(); + + const createNewEmbeddable = useCallback(() => { + const path = '#/'; + const appId = 'lens'; + + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, `${appId}:create`); + } + + stateTransferService.navigateToEditor(appId, { + path, + state: { + originatingApp: CANVAS_APP, + originatingPath: `#/${pathname}${search}`, + }, + }); + }, [pathname, search, stateTransferService]); + + return ; +}; + export const ElementMenu = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), withProps(() => ({ elements: elementsRegistry.toJS() })) -)(Component); +)(ElementMenuComponent); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts index f8ddd769aac43d6..a0076970fbcf798 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -46,9 +46,11 @@ export const useWorkpad = ( workpad.aliasId = aliasId; } - dispatch(setAssets(assets)); - dispatch(setWorkpad(workpad, { loadPages })); - dispatch(setZoomScale(1)); + if (storedWorkpad.id !== workpadId || storedWorkpad.aliasId !== aliasId) { + dispatch(setAssets(assets)); + dispatch(setWorkpad(workpad, { loadPages })); + dispatch(setZoomScale(1)); + } if (outcome === 'aliasMatch' && platformService.redirectLegacyUrl && aliasId) { platformService.redirectLegacyUrl(`#${getRedirectPath(aliasId)}`, getWorkpadLabel()); @@ -57,7 +59,17 @@ export const useWorkpad = ( setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadService, getRedirectPath, platformService]); + }, [ + workpadId, + dispatch, + setError, + loadPages, + workpadService, + getRedirectPath, + platformService, + storedWorkpad.id, + storedWorkpad.aliasId, + ]); return [storedWorkpad.id === workpadId ? storedWorkpad : undefined, error]; }; diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts index 24d7a57e086f2db..26b150b7a534936 100644 --- a/x-pack/plugins/canvas/public/services/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/embeddables.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public'; +import { + EmbeddableFactory, + EmbeddableStateTransfer, +} from '../../../../../src/plugins/embeddable/public'; export interface CanvasEmbeddablesService { getEmbeddableFactories: () => IterableIterator; + getStateTransfer: () => EmbeddableStateTransfer; } diff --git a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts index 054b9da7409fbbc..8d1a86edab3d890 100644 --- a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts @@ -16,4 +16,5 @@ export type EmbeddablesServiceFactory = KibanaPluginServiceFactory< export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({ getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories, + getStateTransfer: startPlugins.embeddable.getStateTransfer, }); diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts index 173d27563e2b2a3..9c2cf4d0650abef 100644 --- a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts @@ -14,4 +14,5 @@ const noop = (..._args: any[]): any => {}; export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({ getEmbeddableFactories: noop, + getStateTransfer: noop, }); diff --git a/x-pack/plugins/canvas/types/embeddables.ts b/x-pack/plugins/canvas/types/embeddables.ts new file mode 100644 index 000000000000000..b78efece59d8f88 --- /dev/null +++ b/x-pack/plugins/canvas/types/embeddables.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimeRange } from 'src/plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { EmbeddableInput as Input } from '../../../../src/plugins/embeddable/common/'; + +export type EmbeddableInput = Input & { + timeRange?: TimeRange; + filters?: Filter[]; + savedObjectId?: string; +}; diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 09ae1510be6da0e..930f33729208842 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -9,6 +9,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './embeddables'; export * from './filters'; export * from './functions'; export * from './renderers';