diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 8a46a16c1bf0cf..effbf8ce980d73 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -231,7 +231,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'labs:canvas:byValueEmbeddable': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'labs:canvas:useDataService': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 7575fa5d2b3f3d..34394602067bc0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -120,6 +120,7 @@ export interface UsageStats { 'banners:textColor': string; 'banners:backgroundColor': string; 'labs:canvas:enable_ui': boolean; + 'labs:canvas:byValueEmbeddable': boolean; 'labs:canvas:useDataService': boolean; 'labs:presentation:timeToPresent': boolean; 'labs:dashboard:enable_ui': boolean; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index b958f3de0814f9..86c05810723bdb 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; export const LABS_PROJECT_PREFIX = 'labs:'; export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; +export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const; -export const projectIDs = [DEFER_BELOW_FOLD] as const; +export const projectIDs = [DEFER_BELOW_FOLD, BY_VALUE_EMBEDDABLE] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -34,6 +35,19 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { }), solutions: ['dashboard'], }, + [BY_VALUE_EMBEDDABLE]: { + id: BY_VALUE_EMBEDDABLE, + isActive: true, + isDisplayed: true, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableByValueEmbeddableName', { + defaultMessage: 'By-Value Embeddables', + }), + description: i18n.translate('presentationUtil.labs.enableByValueEmbeddableDescription', { + defaultMessage: 'Enables support for by-value embeddables in Canvas', + }), + solutions: ['canvas'], + }, }; export type ProjectID = typeof projectIDs[number]; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss index 535570a51d777c..c70e317546d403 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -1,11 +1,31 @@ .quickButtonGroup { - .quickButtonGroup__button { - background-color: $euiColorEmptyShade; - @include kbnThemeStyle('v8') { - // sass-lint:disable-block no-important - border-width: $euiBorderWidthThin !important; - border-style: solid !important; - border-color: $euiBorderColor !important; + .euiButtonGroup__buttons { + border-radius: $euiBorderRadius; + + .quickButtonGroup__button { + background-color: $euiColorEmptyShade; + @include kbnThemeStyle('v8') { + // sass-lint:disable-block no-important + border-width: $euiBorderWidthThin !important; + border-style: solid !important; + border-color: $euiBorderColor !important; + } + } + + .quickButtonGroup__button:first-of-type { + @include kbnThemeStyle('v8') { + // sass-lint:disable-block no-important + border-top-left-radius: $euiBorderRadius !important; + border-bottom-left-radius: $euiBorderRadius !important; + } + } + + .quickButtonGroup__button:last-of-type { + @include kbnThemeStyle('v8') { + // sass-lint:disable-block no-important + border-top-right-radius: $euiBorderRadius !important; + border-bottom-right-radius: $euiBorderRadius !important; + } } } } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 437d50ad82473d..d1c9f3fd5d6576 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7671,6 +7671,12 @@ "description": "Non-default value of setting." } }, + "labs:canvas:byValueEmbeddable": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "labs:canvas:useDataService": { "type": "boolean", "_meta": { 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 ac2e8e8babee1e..f1ede936c6ace4 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/browser/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts index 2cfdebafb70df4..d6d7a0f867849c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts @@ -6,7 +6,6 @@ */ import { functions as commonFunctions } from '../common'; -import { functions as externalFunctions } from '../external'; import { location } from './location'; import { markdown } from './markdown'; import { urlparam } from './urlparam'; @@ -14,13 +13,4 @@ import { escount } from './escount'; import { esdocs } from './esdocs'; import { essql } from './essql'; -export const functions = [ - location, - markdown, - urlparam, - escount, - esdocs, - essql, - ...commonFunctions, - ...externalFunctions, -]; +export const functions = [location, markdown, urlparam, escount, esdocs, essql, ...commonFunctions]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts new file mode 100644 index 00000000000000..001fb0e3f62e3a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { embeddableFunctionFactory } from './embeddable'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; +import { encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +const filterContext: ExpressionValueFilter = { + type: 'filter', + and: [ + { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', + and: [], + column: 'time-column', + filterType: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('embeddable', () => { + const fn = embeddableFunctionFactory({} as InitializeArguments)().fn; + const config = { + id: 'some-id', + timerange: { from: '15m', to: 'now' }, + title: 'test embeddable', + }; + + const args = { + config: encode(config), + type: 'visualization', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {} as any); + + expect(expression.input.filters).toEqual([]); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {} as any); + const embeddableFilters = getQueryFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters); + }); +}); 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 new file mode 100644 index 00000000000000..7ef8f0a09eb907 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts @@ -0,0 +1,145 @@ +/* + * 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 { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +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'; +import { decode, encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +export interface Arguments { + config: string; + type: string; +} + +const defaultTimeRange = { + from: 'now-15m', + to: 'now', +}; + +const baseEmbeddableInput = { + timeRange: defaultTimeRange, + disableTriggers: true, + renderMode: 'noInteractivity', +}; + +type Return = EmbeddableExpression; + +type EmbeddableFunction = ExpressionFunctionDefinition< + 'embeddable', + ExpressionValueFilter | null, + Arguments, + Return +>; + +export function embeddableFunctionFactory({ + embeddablePersistableStateService, +}: InitializeArguments): () => EmbeddableFunction { + return function embeddable(): EmbeddableFunction { + const { help, args: argHelp } = getFunctionHelp().embeddable; + + return { + name: 'embeddable', + help, + args: { + config: { + aliases: ['_'], + types: ['string'], + required: true, + help: argHelp.config, + }, + type: { + types: ['string'], + required: true, + help: argHelp.type, + }, + }, + context: { + types: ['filter'], + }, + type: EmbeddableExpressionType, + fn: (input, args) => { + const filters = input ? input.and : []; + + const embeddableInput = decode(args.config) as EmbeddableInput; + + return { + type: EmbeddableExpressionType, + input: { + ...baseEmbeddableInput, + ...embeddableInput, + filters: getQueryFilters(filters), + }, + generatedAt: Date.now(), + embeddableType: args.type, + }; + }, + + extract(state) { + const input = decode(state.config[0] as string); + + // extracts references for by-reference embeddables + if (input.savedObjectId) { + const refName = 'embeddable.savedObjectId'; + + const references: SavedObjectReference[] = [ + { + name: refName, + type: state.type[0] as string, + id: input.savedObjectId as string, + }, + ]; + + return { + state, + references, + }; + } + + // extracts references for by-value embeddables + const { state: extractedState, references: extractedReferences } = + embeddablePersistableStateService.extract({ + ...input, + type: state.type[0], + }); + + const { type, ...extractedInput } = extractedState; + + return { + state: { ...state, config: [encode(extractedInput)], type: [type] }, + references: extractedReferences, + }; + }, + + inject(state, references) { + const input = decode(state.config[0] as string); + const savedObjectReference = references.find( + (ref) => ref.name === 'embeddable.savedObjectId' + ); + + // injects saved object id for by-references embeddable + if (savedObjectReference) { + input.savedObjectId = savedObjectReference.id; + state.config[0] = encode(input); + state.type[0] = savedObjectReference.type; + } else { + // injects references for by-value embeddables + const { type, ...injectedInput } = embeddablePersistableStateService.inject( + { ...input, type: state.type[0] }, + references + ); + state.config[0] = encode(injectedInput); + state.type[0] = type; + } + return state; + }, + }; + }; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts index 407a0e2ebfe05a..1d69e181b5fd9d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts @@ -5,9 +5,26 @@ * 2.0. */ +import { EmbeddableStart } from 'src/plugins/embeddable/public'; +import { embeddableFunctionFactory } from './embeddable'; import { savedLens } from './saved_lens'; import { savedMap } from './saved_map'; import { savedSearch } from './saved_search'; import { savedVisualization } from './saved_visualization'; -export const functions = [savedLens, savedMap, savedVisualization, savedSearch]; +export interface InitializeArguments { + embeddablePersistableStateService: { + extract: EmbeddableStart['extract']; + inject: EmbeddableStart['inject']; + }; +} + +export function initFunctions(initialize: InitializeArguments) { + return [ + embeddableFunctionFactory(initialize), + savedLens, + savedMap, + savedSearch, + savedVisualization, + ]; +} 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 082a69a874cae2..67947691f7757c 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, @@ -27,7 +26,7 @@ interface Arguments { } export type SavedLensInput = EmbeddableInput & { - id: string; + savedObjectId: string; timeRange?: TimeRange; filters: DataFilter[]; palette?: PaletteOutput; @@ -73,18 +72,19 @@ export function savedLens(): ExpressionFunctionDefinition< }, }, type: EmbeddableExpressionType, - fn: (input, args) => { + fn: (input, { id, timerange, title, palette }) => { const filters = input ? input.and : []; return { type: EmbeddableExpressionType, input: { - id: args.id, + id, + savedObjectId: id, filters: getQueryFilters(filters), - timeRange: args.timerange || defaultTimeRange, - title: args.title === null ? undefined : args.title, + timeRange: timerange || defaultTimeRange, + title: title === null ? undefined : title, disableTriggers: true, - palette: args.palette, + palette, }, embeddableType: EmbeddableTypes.lens, generatedAt: Date.now(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index 538ed3f9198239..a7471c755155c8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -30,7 +30,7 @@ const defaultTimeRange = { to: 'now', }; -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', @@ -85,8 +85,9 @@ export function savedMap(): ExpressionFunctionDefinition< return { type: EmbeddableExpressionType, input: { - attributes: { title: '' }, id: args.id, + attributes: { title: '' }, + savedObjectId: args.id, filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, refreshConfig: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 5c0442b43250c8..31e3fb2a8c5643 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -25,7 +25,7 @@ interface Arguments { title: string | null; } -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; const defaultTimeRange = { from: 'now-15m', @@ -94,6 +94,7 @@ export function savedVisualization(): ExpressionFunctionDefinition< type: EmbeddableExpressionType, input: { id, + savedObjectId: id, disableTriggers: true, timeRange: timerange || defaultTimeRange, filters: getQueryFilters(filters), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 91c573fc4148ba..591795637aebea 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -7,12 +7,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; import { CanvasSetup } from '../public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { functions } from './functions/browser'; +import { initFunctions } from './functions/external'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; @@ -25,6 +27,7 @@ export interface StartDeps { uiActions: UiActionsStart; inspector: InspectorStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } export type SetupInitializer = (core: CoreSetup, plugins: SetupDeps) => T; @@ -39,6 +42,13 @@ export class CanvasSrcPlugin implements Plugin plugins.canvas.addRenderers(renderFunctions); core.getStartServices().then(([coreStart, depsStart]) => { + const externalFunctions = initFunctions({ + embeddablePersistableStateService: { + extract: depsStart.embeddable.extract, + inject: depsStart.embeddable.inject, + }, + }); + plugins.canvas.addFunctions(externalFunctions); plugins.canvas.addRenderers( renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart)) ); 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 73e839433c25e0..953746c2808406 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,16 +13,17 @@ 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; +// registry of references to embeddables on the workpad const embeddablesRegistry: { [key: string]: IEmbeddable | Promise; } = {}; @@ -30,11 +31,11 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; - return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + return (embeddableObject: IEmbeddable) => { return (
@@ -56,6 +57,9 @@ export const embeddableRendererFactory = ( reuseDomNode: true, render: async (domNode, { input, embeddableType }, handlers) => { const uniqueId = handlers.getElementId(); + const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled( + 'labs:canvas:byValueEmbeddable' + ); if (!embeddablesRegistry[uniqueId]) { const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find( @@ -67,15 +71,27 @@ export const embeddableRendererFactory = ( throw new EmbeddableFactoryNotFoundError(embeddableType); } - const embeddablePromise = factory - .createFromSavedObject(input.id, input) - .then((embeddable) => { - 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(); @@ -86,7 +102,8 @@ export const embeddableRendererFactory = ( const updatedExpression = embeddableInputToExpression( updatedInput, embeddableType, - palettes + palettes, + isByValueEnabled ); if (updatedExpression) { @@ -94,15 +111,7 @@ export const embeddableRendererFactory = ( } }); - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - - handlers.onResize(() => { - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - }); + ReactDOM.render(renderEmbeddable(embeddableObject), domNode, () => handlers.done()); handlers.onDestroy(() => { subscription.unsubscribe(); @@ -115,6 +124,7 @@ export const embeddableRendererFactory = ( } else { const embeddable = embeddablesRegistry[uniqueId]; + // updating embeddable input with changes made to expression or filters if ('updateInput' in embeddable) { embeddable.updateInput(input); embeddable.reload(); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts index 41cefad6a470fa..80830eac240212 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts @@ -10,6 +10,7 @@ import { EmbeddableTypes, EmbeddableInput } from '../../expression_types'; import { toExpression as mapToExpression } from './input_type_to_expression/map'; import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization'; import { toExpression as lensToExpression } from './input_type_to_expression/lens'; +import { toExpression as genericToExpression } from './input_type_to_expression/embeddable'; export const inputToExpressionTypeMap = { [EmbeddableTypes.map]: mapToExpression, @@ -23,8 +24,13 @@ export const inputToExpressionTypeMap = { export function embeddableInputToExpression( input: EmbeddableInput, embeddableType: string, - palettes: PaletteRegistry + palettes: PaletteRegistry, + useGenericEmbeddable?: boolean ): string | undefined { + if (useGenericEmbeddable) { + return genericToExpression(input, embeddableType); + } + if (inputToExpressionTypeMap[embeddableType]) { return inputToExpressionTypeMap[embeddableType](input as any, palettes); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts new file mode 100644 index 00000000000000..4b78acec8750ac --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { toExpression } from './embeddable'; +import { EmbeddableInput } from '../../../../types'; +import { decode } from '../../../../common/lib/embeddable_dataurl'; +import { fromExpression } from '@kbn/interpreter/common'; + +describe('toExpression', () => { + describe('by-reference embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + savedObjectId: 'embeddableId', + filters: [], + }; + + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config.savedObjectId).toStrictEqual(input.savedObjectId); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); + + describe('by-value embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + disableTriggers: true, + filters: [], + }; + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + expect(config.filters).toStrictEqual(input.filters); + expect(config.disableTriggers).toStrictEqual(input.disableTriggers); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts new file mode 100644 index 00000000000000..94d86f6640be1d --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts @@ -0,0 +1,13 @@ +/* + * 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 { encode } from '../../../../common/lib/embeddable_dataurl'; +import { EmbeddableInput } from '../../../expression_types'; + +export function toExpression(input: EmbeddableInput, embeddableType: string): string { + return `embeddable config="${encode(input)}" type="${embeddableType}"`; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts index 24da7238bcee94..224cdfba389d77 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts @@ -11,7 +11,8 @@ import { fromExpression, Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; const baseEmbeddableInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', filters: [], }; @@ -27,7 +28,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedLens'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('timerange'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 35e106f234fa4e..5a13b73b3fe746 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -14,7 +14,7 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): expressionParts.push('savedLens'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index 804d0d849cc7f7..af7b40a9b283d9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -6,12 +6,12 @@ */ import { toExpression } from './map'; -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { + id: 'elementId', attributes: { title: '' }, - id: 'embeddableId', + savedObjectId: 'embeddableId', filters: [], isLayerTOCOpen: false, refreshConfig: { @@ -23,7 +23,7 @@ const baseSavedMapInput = { describe('toExpression', () => { it('converts to a savedMap expression', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, }; @@ -33,7 +33,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedMap'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('center'); @@ -41,7 +41,7 @@ describe('toExpression', () => { }); it('includes optional input values', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, mapCenter: { lat: 1, @@ -73,7 +73,7 @@ describe('toExpression', () => { }); it('includes empty panel title', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, title: '', }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index 3fd6a68a327c60..03746f38b4696c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; +import { MapEmbeddableInput } from '../../../../../../plugins/maps/public'; -export function toExpression(input: MapEmbeddableInput): string { +export function toExpression(input: MapEmbeddableInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedMap'); - expressionParts.push(`id="${input.id}"`); + + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts index c5106b9a102b40..4c61a130f3c95f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts @@ -9,7 +9,8 @@ import { toExpression } from './visualization'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', }; describe('toExpression', () => { @@ -24,7 +25,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedVisualization'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); }); it('includes timerange if given', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts index bcb73b2081fee5..364d7cd0755db4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts @@ -7,11 +7,11 @@ import { VisualizeInput } from 'src/plugins/visualizations/public'; -export function toExpression(input: VisualizeInput): string { +export function toExpression(input: VisualizeInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedVisualization'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts new file mode 100644 index 00000000000000..e76dedfe63b14a --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts @@ -0,0 +1,13 @@ +/* + * 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 { EmbeddableInput } from '../../types'; + +export const encode = (input: Partial) => + Buffer.from(JSON.stringify(input)).toString('base64'); +export const decode = (serializedInput: string) => + JSON.parse(Buffer.from(serializedInput, 'base64').toString()); diff --git a/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts new file mode 100644 index 00000000000000..279f58799e8c00 --- /dev/null +++ b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts @@ -0,0 +1,25 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { embeddableFunctionFactory } from '../../../canvas_plugin_src/functions/external/embeddable'; +import { FunctionHelp } from '../function_help'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp>> = { + help: i18n.translate('xpack.canvas.functions.embeddableHelpText', { + defaultMessage: `Returns an embeddable with the provided configuration`, + }), + args: { + config: i18n.translate('xpack.canvas.functions.embeddable.args.idHelpText', { + defaultMessage: `The base64 encoded embeddable input object`, + }), + type: i18n.translate('xpack.canvas.functions.embeddable.args.typeHelpText', { + defaultMessage: `The embeddable type`, + }), + }, +}; diff --git a/x-pack/plugins/canvas/i18n/functions/function_help.ts b/x-pack/plugins/canvas/i18n/functions/function_help.ts index 5eae785fefa2ea..520d32af1c272c 100644 --- a/x-pack/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/plugins/canvas/i18n/functions/function_help.ts @@ -27,6 +27,7 @@ import { help as demodata } from './dict/demodata'; import { help as doFn } from './dict/do'; import { help as dropdownControl } from './dict/dropdown_control'; import { help as eq } from './dict/eq'; +import { help as embeddable } from './dict/embeddable'; import { help as escount } from './dict/escount'; import { help as esdocs } from './dict/esdocs'; import { help as essql } from './dict/essql'; @@ -182,6 +183,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ do: doFn, dropdownControl, eq, + embeddable, escount, esdocs, essql, diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 9c4d1b2179d821..2fd312502a3c74 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -25,6 +25,7 @@ "features", "inspector", "presentationUtil", + "visualizations", "uiActions", "share" ], diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index bf731876bf8c88..57f52fcf21f0f9 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -27,38 +27,44 @@ const strings = { }; export interface Props { onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; + onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void; availableEmbeddables: string[]; + isByValueEnabled?: boolean; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { +export const AddEmbeddableFlyout: FC = ({ + onSelect, + availableEmbeddables, + onClose, + isByValueEnabled, +}) => { const embeddablesService = useEmbeddablesService(); const platformService = usePlatformService(); const { getEmbeddableFactories } = embeddablesService; const { getSavedObjects, getUISettings } = platformService; - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); + const onAddPanel = useCallback( + (id: string, savedObjectType: string) => { + const embeddableFactories = getEmbeddableFactories(); + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); - - const foundEmbeddableType = found ? found.type : 'unknown'; + const foundEmbeddableType = found ? found.type : 'unknown'; - onSelect(id, foundEmbeddableType); - }; + onSelect(id, foundEmbeddableType, isByValueEnabled); + }, + [isByValueEnabled, getEmbeddableFactories, onSelect] + ); const embeddableFactories = getEmbeddableFactories(); const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) + .filter((factory) => isByValueEnabled || availableEmbeddables.includes(factory.type)) .map((factory) => factory.savedObjectMetaData) .filter>(function ( maybeSavedObjectMetaData 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 770a4cac606b0b..4dc8d963932d8f 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -8,12 +8,14 @@ import React, { useMemo, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useSelector, useDispatch } from 'react-redux'; +import { encode } from '../../../common/lib/embeddable_dataurl'; import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; // @ts-expect-error untyped local import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; import { State } from '../../../types'; +import { useLabsService } from '../../services'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -65,6 +67,9 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ availableEmbeddables, ...restProps }) => { + const labsService = useLabsService(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const dispatch = useDispatch(); const pageId = useSelector((state) => getSelectedPage(state)); @@ -74,18 +79,27 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ ); const onSelect = useCallback( - (id: string, type: string) => { + (id: string, type: string): void => { const partialElement = { expression: `markdown "Could not find embeddable for type ${type}" | render`, }; - if (allowedEmbeddables[type]) { + + // If by-value is enabled, we'll handle both by-reference and by-value embeddables + // with the new generic `embeddable` function. + // Otherwise we fallback to the embeddable type specific expressions. + if (isByValueEnabled) { + const config = encode({ savedObjectId: id }); + partialElement.expression = `embeddable config="${config}" + type="${type}" +| render`; + } else if (allowedEmbeddables[type]) { partialElement.expression = allowedEmbeddables[type](id); } addEmbeddable(pageId, partialElement); restProps.onClose(); }, - [addEmbeddable, pageId, restProps] + [addEmbeddable, pageId, restProps, isByValueEnabled] ); return ( @@ -93,6 +107,7 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ {...restProps} availableEmbeddables={availableEmbeddables || []} onSelect={onSelect} + isByValueEnabled={isByValueEnabled} /> ); }; 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 50d527036560ad..ffd5b095b12e52 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 00000000000000..2f8e2503ea57ef --- /dev/null +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts @@ -0,0 +1,86 @@ +/* + * 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 { fromExpression } from '@kbn/interpreter/common'; +import { CANVAS_APP } from '../../../../common/lib'; +import { decode, encode } from '../../../../common/lib/embeddable_dataurl'; +import { CanvasElement, CanvasPage } from '../../../../types'; +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 = (selectedPage: CanvasPage) => { + 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: incomingInput, type } = incomingEmbeddable; + + // retrieve existing element + const originalElement = selectedPage.elements.find( + ({ id }: CanvasElement) => id === embeddableId + ); + + if (originalElement) { + const originalAst = fromExpression(originalElement!.expression); + + const functionIndex = originalAst.chain.findIndex( + ({ function: fn }) => fn === 'embeddable' + ); + + const originalInput = decode( + originalAst.chain[functionIndex].arguments.config[0] as string + ); + + // clear out resolved arg for old embeddable + const argumentPath = [embeddableId, 'expressionRenderable']; + dispatch(clearValue({ path: argumentPath })); + + const updatedInput = { ...originalInput, ...incomingInput }; + + const expression = `embeddable config="${encode(updatedInput)}" + type="${type}" +| render`; + + dispatch( + updateEmbeddableExpression({ + elementId: originalElement.id, + embeddableExpression: expression, + }) + ); + + // update resolved args + dispatch(fetchEmbeddableRenderable(originalElement.id)); + + // select new embeddable element + dispatch(selectToplevelNodes([embeddableId])); + } else { + const expression = `embeddable config="${encode(incomingInput)}" + type="${type}" +| render`; + dispatch(addElement(selectedPage.id, { expression })); + } + } + }, [dispatch, selectedPage, 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 622c885b6ef281..7cc077203c7372 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 selectedPage = propsFromState.pages[propsFromState.selectedPageNumber - 1]; + useIncomingEmbeddable(selectedPage); + const fetchAllRenderables = useCallback(() => { dispatch(fetchAllRenderablesAction()); }, [dispatch]); diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss index 4acdca10d61cc2..0ddd44ed8f9a81 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: $euiSizeS; + padding: $euiSizeS $euiSize; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot new file mode 100644 index 00000000000000..f4aab0e59e7ee6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/EditorMenu dark mode 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots components/WorkpadHeader/EditorMenu default 1`] = ` +
+
+ +
+
+`; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx new file mode 100644 index 00000000000000..01048bc0af3010 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx @@ -0,0 +1,107 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { EmbeddableFactoryDefinition, IEmbeddable } from 'src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from 'src/plugins/visualizations/public'; +import { EditorMenu } from '../editor_menu.component'; + +const testFactories: EmbeddableFactoryDefinition[] = [ + { + type: 'ml_anomaly_swimlane', + getDisplayName: () => 'Anomaly swimlane', + getIconType: () => '', + getDescription: () => 'Description for anomaly swimlane', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'swimlane_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'ml_anomaly_chart', + getDisplayName: () => 'Anomaly chart', + getIconType: () => '', + getDescription: () => 'Description for anomaly chart', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'log_stream', + getDisplayName: () => 'Log stream', + getIconType: () => '', + getDescription: () => 'Description for log stream', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + }, +]; + +const testVisTypes: BaseVisType[] = [ + { title: 'TSVB', icon: '', description: 'Description of TSVB', name: 'tsvb' } as BaseVisType, + { + titleInWizard: 'Custom visualization', + title: 'Vega', + icon: '', + description: 'Description of Vega', + name: 'vega', + } as BaseVisType, +]; + +const testVisTypeAliases: VisTypeAlias[] = [ + { + title: 'Lens', + aliasApp: 'lens', + aliasPath: 'path/to/lens', + icon: 'lensApp', + name: 'lens', + description: 'Description of Lens app', + stage: 'production', + }, + { + title: 'Maps', + aliasApp: 'maps', + aliasPath: 'path/to/maps', + icon: 'gisApp', + name: 'maps', + description: 'Description of Maps app', + stage: 'production', + }, +]; + +storiesOf('components/WorkpadHeader/EditorMenu', module) + .add('default', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )) + .add('dark mode', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx new file mode 100644 index 00000000000000..e8f762f9731a19 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx @@ -0,0 +1,170 @@ +/* + * 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 React, { FC } from 'react'; +import { + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiContextMenuItemIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EmbeddableFactoryDefinition } from '../../../../../../../src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from '../../../../../../../src/plugins/visualizations/public'; +import { SolutionToolbarPopover } from '../../../../../../../src/plugins/presentation_util/public'; + +const strings = { + getEditorMenuButtonLabel: () => + i18n.translate('xpack.canvas.solutionToolbar.editorMenuButtonLabel', { + defaultMessage: 'Select type', + }), +}; + +interface FactoryGroup { + id: string; + appName: string; + icon: EuiContextMenuItemIcon; + panelId: number; + factories: EmbeddableFactoryDefinition[]; +} + +interface Props { + factories: EmbeddableFactoryDefinition[]; + isDarkThemeEnabled?: boolean; + promotedVisTypes: BaseVisType[]; + visTypeAliases: VisTypeAlias[]; + createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void; + createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void; +} + +export const EditorMenu: FC = ({ + factories, + isDarkThemeEnabled, + promotedVisTypes, + visTypeAliases, + createNewVisType, + createNewEmbeddable, +}: Props) => { + const factoryGroupMap: Record = {}; + const ungroupedFactories: EmbeddableFactoryDefinition[] = []; + + let panelCount = 1; + + // Maps factories with a group to create nested context menus for each group type + // and pushes ungrouped factories into a separate array + factories.forEach((factory: EmbeddableFactoryDefinition, index) => { + const { grouping } = factory; + + if (grouping) { + grouping.forEach((group) => { + if (factoryGroupMap[group.id]) { + factoryGroupMap[group.id].factories.push(factory); + } else { + factoryGroupMap[group.id] = { + id: group.id, + appName: group.getDisplayName ? group.getDisplayName({}) : group.id, + icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon, + factories: [factory], + panelId: panelCount, + }; + + panelCount++; + } + }); + } else { + ungroupedFactories.push(factory); + } + }); + + const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => { + const { name, title, titleInWizard, description, icon = 'empty' } = visType; + return { + name: titleInWizard || title, + icon: icon as string, + onClick: createNewVisType(visType), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getVisTypeAliasMenuItem = ( + visTypeAlias: VisTypeAlias + ): EuiContextMenuPanelItemDescriptor => { + const { name, title, description, icon = 'empty' } = visTypeAlias; + + return { + name: title, + icon, + onClick: createNewVisType(visTypeAlias), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getEmbeddableFactoryMenuItem = ( + factory: EmbeddableFactoryDefinition + ): EuiContextMenuPanelItemDescriptor => { + const icon = factory?.getIconType ? factory.getIconType() : 'empty'; + + const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined; + + return { + name: factory.getDisplayName(), + icon, + toolTipContent, + onClick: createNewEmbeddable(factory), + 'data-test-subj': `createNew-${factory.type}`, + }; + }; + + const editorMenuPanels = [ + { + id: 0, + items: [ + ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ + name: appName, + icon, + panel: panelId, + 'data-test-subj': `canvasEditorMenu-${id}Group`, + })), + ...ungroupedFactories.map(getEmbeddableFactoryMenuItem), + ...promotedVisTypes.map(getVisTypeMenuItem), + ], + }, + ...Object.values(factoryGroupMap).map( + ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ + id: panelId, + title: appName, + items: groupFactories.map(getEmbeddableFactoryMenuItem), + }) + ), + ]; + + return ( + + {() => ( + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx new file mode 100644 index 00000000000000..dad34e6983c5db --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -0,0 +1,147 @@ +/* + * 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 React, { FC, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric'; +import { + useEmbeddablesService, + usePlatformService, + useVisualizationsService, +} from '../../../services'; +import { + BaseVisType, + VisGroups, + VisTypeAlias, +} from '../../../../../../../src/plugins/visualizations/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableInput, +} from '../../../../../../../src/plugins/embeddable/public'; +import { CANVAS_APP } from '../../../../common/lib'; +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { ElementSpec } from '../../../../types'; +import { EditorMenu as Component } from './editor_menu.component'; + +interface Props { + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: Partial) => void; +} + +export const EditorMenu: FC = ({ addElement }) => { + const embeddablesService = useEmbeddablesService(); + const { pathname, search } = useLocation(); + const platformService = usePlatformService(); + const stateTransferService = embeddablesService.getStateTransfer(); + const visualizationsService = useVisualizationsService(); + const IS_DARK_THEME = platformService.getUISetting('theme:darkMode'); + + const createNewVisType = useCallback( + (visType?: BaseVisType | VisTypeAlias) => () => { + let path = ''; + let appId = ''; + + if (visType) { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); + } + + if ('aliasPath' in visType) { + appId = visType.aliasApp; + path = visType.aliasPath; + } else { + appId = 'visualize'; + path = `#/create?type=${encodeURIComponent(visType.name)}`; + } + } else { + appId = 'visualize'; + path = '#/create?'; + } + + stateTransferService.navigateToEditor(appId, { + path, + state: { + originatingApp: CANVAS_APP, + originatingPath: `#/${pathname}${search}`, + }, + }); + }, + [stateTransferService, pathname, search] + ); + + const createNewEmbeddable = useCallback( + (factory: EmbeddableFactoryDefinition) => async () => { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type); + } + let embeddableInput; + if (factory.getExplicitInput) { + embeddableInput = await factory.getExplicitInput(); + } else { + const newEmbeddable = await factory.create({} as EmbeddableInput); + embeddableInput = newEmbeddable?.getInput(); + } + + if (embeddableInput) { + const config = encode(embeddableInput); + const expression = `embeddable config="${config}" + type="${factory.type}" +| render`; + + addElement({ expression }); + } + }, + [addElement] + ); + + const getVisTypesByGroup = (group: VisGroups): BaseVisType[] => + visualizationsService + .getByGroup(group) + .sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }) + .filter(({ hidden }: BaseVisType) => !hidden); + + const visTypeAliases = visualizationsService + .getAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ); + + const factories = embeddablesService + ? Array.from(embeddablesService.getEmbeddableFactories()).filter( + ({ type, isEditable, canCreateNew, isContainerType }) => + isEditable() && + !isContainerType && + canCreateNew() && + !['visualization', 'ml'].some((factoryType) => { + return type.includes(factoryType); + }) + ) + : []; + + const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts new file mode 100644 index 00000000000000..0f903b1bbbe2ed --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { EditorMenu } from './editor_menu'; +export { EditorMenu as EditorMenuComponent } from './editor_menu.component'; 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 8ac581b0866a46..1cfab236d9a9c9 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 @@ -12,11 +12,11 @@ import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@ela import { i18n } from '@kbn/i18n'; import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public'; import { getId } from '../../../lib/get_id'; -import { ClosePopoverFn } from '../../popover'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { AssetManager } from '../../asset_manager'; +import { ClosePopoverFn } from '../../popover'; import { SavedElementsModal } from '../../saved_elements_modal'; interface CategorizedElementLists { @@ -112,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele return categories; }; -interface Props { +export interface Props { /** * Dictionary of elements from elements registry */ @@ -120,7 +120,7 @@ interface Props { /** * Handler for adding a selected element to the workpad */ - addElement: (element: ElementSpec) => void; + addElement: (element: Partial) => void; } export const ElementMenu: FunctionComponent = ({ elements, addElement }) => { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts index 52c8daece7690a..037bb84b0cdba4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { ElementMenu } from './element_menu'; -export { ElementMenu as ElementMenuComponent } from './element_menu.component'; +export { ElementMenu } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index f031d7c2631991..b84e4faf2925e7 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -27,6 +27,7 @@ import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; import { LabsControl } from './labs_control'; +import { EditorMenu } from './editor_menu'; const strings = { getFullScreenButtonAriaLabel: () => @@ -160,24 +161,22 @@ export const WorkpadHeader: FC = ({ + {isWriteable && ( + + + {{ + primaryActionButton: , + quickButtonGroup: , + addFromLibraryButton: , + extraButtons: [], + }} + + + )} - {isWriteable && ( - - - {{ - primaryActionButton: ( - - ), - quickButtonGroup: , - addFromLibraryButton: , - }} - - - )} @@ -192,6 +191,7 @@ export const WorkpadHeader: FC = ({ + diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 723d1afea2860f..912055dd47a627 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -8,6 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import type { SharePluginSetup } from 'src/plugins/share/public'; import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public'; +import { VisualizationsStart } from 'src/plugins/visualizations/public'; import { ReportingStart } from '../../reporting/public'; import { CoreSetup, @@ -63,6 +64,7 @@ export interface CanvasStartDeps { charts: ChartsPluginStart; data: DataPublicPluginStart; presentationUtil: PresentationUtilPluginStart; + visualizations: VisualizationsStart; spaces?: SpacesPluginStart; } @@ -122,7 +124,12 @@ export class CanvasPlugin const { pluginServices } = await import('./services'); pluginServices.setRegistry( - pluginServiceRegistry.start({ coreStart, startPlugins, initContext: this.initContext }) + pluginServiceRegistry.start({ + coreStart, + startPlugins, + appUpdater: this.appUpdater, + initContext: this.initContext, + }) ); // Load application bundle 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 35e79b442a15d7..bd9a4e7141c272 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 @@ -53,14 +53,24 @@ 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)); + } } catch (e) { setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadResolve]); + }, [ + workpadId, + dispatch, + setError, + loadPages, + workpadResolve, + storedWorkpad.id, + storedWorkpad.aliasId, + ]); useEffect(() => { (() => { diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts index 24d7a57e086f2d..26b150b7a53493 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/index.ts b/x-pack/plugins/canvas/public/services/index.ts index f4292810b80896..ed55f919e4c767 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -17,6 +17,7 @@ import { CanvasNavLinkService } from './nav_link'; import { CanvasNotifyService } from './notify'; import { CanvasPlatformService } from './platform'; import { CanvasReportingService } from './reporting'; +import { CanvasVisualizationsService } from './visualizations'; import { CanvasWorkpadService } from './workpad'; export interface CanvasPluginServices { @@ -28,6 +29,7 @@ export interface CanvasPluginServices { notify: CanvasNotifyService; platform: CanvasPlatformService; reporting: CanvasReportingService; + visualizations: CanvasVisualizationsService; workpad: CanvasWorkpadService; } @@ -44,4 +46,6 @@ export const useNavLinkService = () => (() => pluginServices.getHooks().navLink. export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())(); +export const useVisualizationsService = () => + (() => pluginServices.getHooks().visualizations.useService())(); export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts index 054b9da7409fbb..8d1a86edab3d89 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/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 1eb010e8d6f9da..91767947bc0a65 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders< @@ -45,6 +47,7 @@ export const pluginServiceProviders: PluginServiceProviders< notify: new PluginServiceProvider(notifyServiceFactory), platform: new PluginServiceProvider(platformServiceFactory), reporting: new PluginServiceProvider(reportingServiceFactory), + visualizations: new PluginServiceProvider(visualizationsServiceFactory), workpad: new PluginServiceProvider(workpadServiceFactory), }; diff --git a/x-pack/plugins/canvas/public/services/kibana/visualizations.ts b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts new file mode 100644 index 00000000000000..e319ec1c1f4272 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts @@ -0,0 +1,21 @@ +/* + * 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 { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasStartDeps } from '../../plugin'; +import { CanvasVisualizationsService } from '../visualizations'; + +export type VisualizationsServiceFactory = KibanaPluginServiceFactory< + CanvasVisualizationsService, + CanvasStartDeps +>; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = ({ startPlugins }) => ({ + showNewVisModal: startPlugins.visualizations.showNewVisModal, + getByGroup: startPlugins.visualizations.getByGroup, + getAliases: startPlugins.visualizations.getAliases, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts index 173d27563e2b2a..9c2cf4d0650abe 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/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 06a5ff49e9c04e..2216013a29c129 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { @@ -42,6 +44,7 @@ export const pluginServiceProviders: PluginServiceProviders; + +const noop = (..._args: any[]): any => {}; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = () => ({ + showNewVisModal: noop, + getByGroup: noop, + getAliases: noop, +}); diff --git a/x-pack/plugins/canvas/public/services/visualizations.ts b/x-pack/plugins/canvas/public/services/visualizations.ts new file mode 100644 index 00000000000000..c602b1dd39f3d2 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/visualizations.ts @@ -0,0 +1,14 @@ +/* + * 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 { VisualizationsStart } from '../../../../../src/plugins/visualizations/public'; + +export interface CanvasVisualizationsService { + showNewVisModal: VisualizationsStart['showNewVisModal']; + getByGroup: VisualizationsStart['getByGroup']; + getAliases: VisualizationsStart['getAliases']; +} diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts index 4cfdc7f21945f6..092d4300d86b79 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts @@ -40,7 +40,7 @@ export const embeddableReducer = handleActions< const element = pageWithElement.elements.find((elem) => elem.id === elementId); - if (!element) { + if (!element || element.expression === embeddableExpression) { return workpadState; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 4071b891e4c3d7..ebe43ba76a46ac 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -14,6 +14,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { EmbeddableSetup } from 'src/plugins/embeddable/server'; import { ESSQL_SEARCH_STRATEGY } from '../common/lib/constants'; import { ReportingSetup } from '../../reporting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -30,6 +31,7 @@ import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_ interface PluginsSetup { expressions: ExpressionsServerSetup; + embeddable: EmbeddableSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; bfetch: BfetchServerSetup; @@ -82,7 +84,12 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index 2fe23eb86c086f..849ad79717056c 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -7,9 +7,15 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; +import { + initFunctions as initExternalFunctions, + InitializeArguments, +} from '../canvas_plugin_src/functions/external'; -export function setupInterpreter(expressions: ExpressionsServerSetup) { +export function setupInterpreter( + expressions: ExpressionsServerSetup, + dependencies: InitializeArguments +) { functions.forEach((f) => expressions.registerFunction(f)); - externalFunctions.forEach((f) => expressions.registerFunction(f)); + initExternalFunctions(dependencies).forEach((f) => expressions.registerFunction(f)); } diff --git a/x-pack/plugins/canvas/types/embeddables.ts b/x-pack/plugins/canvas/types/embeddables.ts new file mode 100644 index 00000000000000..b78efece59d8f8 --- /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/functions.ts b/x-pack/plugins/canvas/types/functions.ts index 2569e0b10685b7..c80102915ed953 100644 --- a/x-pack/plugins/canvas/types/functions.ts +++ b/x-pack/plugins/canvas/types/functions.ts @@ -10,8 +10,8 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { functions as commonFunctions } from '../canvas_plugin_src/functions/common'; import { functions as browserFunctions } from '../canvas_plugin_src/functions/browser'; import { functions as serverFunctions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; -import { initFunctions } from '../public/functions'; +import { initFunctions as initExternalFunctions } from '../canvas_plugin_src/functions/external'; +import { initFunctions as initClientFunctions } from '../public/functions'; /** * A `ExpressionFunctionFactory` is a powerful type used for any function that produces @@ -90,9 +90,11 @@ export type FunctionFactory = type CommonFunction = FunctionFactory; type BrowserFunction = FunctionFactory; type ServerFunction = FunctionFactory; -type ExternalFunction = FunctionFactory; +type ExternalFunction = FunctionFactory< + ReturnType extends Array ? U : never +>; type ClientFunctions = FunctionFactory< - ReturnType extends Array ? U : never + ReturnType extends Array ? U : never >; /** diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 09ae1510be6da0..930f3372920884 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';