From 54e2edce192dea3434ac2282dc5abb9a495fb342 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Wed, 21 Aug 2019 08:08:15 -0500 Subject: [PATCH] [Canvas] Canvas embeddable (#39839) * Adds embeddable objects to canvas * Handle embeddable_api -> NewPlatform changes * Addressing PR feedback * Properly mock new platform * Snake case filenames{ * Switch relative paths to src/ --- .../expression_types/embeddable.ts | 34 ++++++ .../expression_types/embeddable_types.ts | 16 +++ .../expression_types/index.ts | 11 ++ .../functions/common/index.ts | 6 + .../functions/common/saved_map.test.ts | 43 +++++++ .../functions/common/saved_map.ts | 68 +++++++++++ .../functions/common/saved_search.test.ts | 43 +++++++ .../functions/common/saved_search.ts | 50 ++++++++ .../common/saved_visualization.test.ts | 43 +++++++ .../functions/common/saved_visualization.ts | 54 +++++++++ .../renderers/embeddable.tsx | 96 +++++++++++++++ .../canvas_plugin_src/renderers/index.js | 2 + .../strings/functions/function_help.ts | 6 + .../strings/functions/saved_map.ts | 19 +++ .../strings/functions/saved_search.ts | 19 +++ .../strings/functions/saved_visualization.ts | 19 +++ .../canvas/common/lib/autocomplete.test.ts | 1 + x-pack/legacy/plugins/canvas/index.js | 6 +- .../public/angular/controllers/canvas.js | 9 +- x-pack/legacy/plugins/canvas/public/app.js | 1 + .../components/embeddable_flyout/flyout.tsx | 77 ++++++++++++ .../components/embeddable_flyout/index.tsx | 114 ++++++++++++++++++ .../workpad_header/workpad_header.js | 15 ++- .../event_handlers.js | 21 +++- .../workpad_interactive_page/index.js | 5 + .../interactive_workpad_page.js | 2 +- .../public/interpreter_expression_types.ts | 12 ++ .../lib/build_embeddable_filters.test.ts | 38 ++++++ .../server/lib/build_embeddable_filters.ts | 42 +++++++ 29 files changed, 862 insertions(+), 10 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/index.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_map.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_search.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_visualization.ts create mode 100644 x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx create mode 100644 x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx create mode 100644 x-pack/legacy/plugins/canvas/public/interpreter_expression_types.ts create mode 100644 x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.test.ts create mode 100644 x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts new file mode 100644 index 00000000000000..1a0774619fce49 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExpressionType } from 'src/plugins/data/common/expressions'; +import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { EmbeddableTypes } from './embeddable_types'; + +export const EmbeddableExpressionType = 'embeddable'; +export { EmbeddableTypes }; + +export interface EmbeddableExpression { + type: typeof EmbeddableExpressionType; + input: Input; + embeddableType: string; +} + +export const embeddableType = (): ExpressionType< + typeof EmbeddableExpressionType, + EmbeddableExpression +> => ({ + name: EmbeddableExpressionType, + to: { + render: (embeddableExpression: EmbeddableExpression) => { + return { + type: 'render', + as: EmbeddableExpressionType, + value: embeddableExpression, + }; + }, + }, +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts new file mode 100644 index 00000000000000..c94b0dcd2bd9d9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable'; + +export const EmbeddableTypes = { + map: MAP_SAVED_OBJECT_TYPE, + search: SEARCH_EMBEDDABLE_TYPE, + visualization: VISUALIZE_EMBEDDABLE_TYPE, +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/index.ts new file mode 100644 index 00000000000000..e73fb68165e0ca --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { embeddableType } from './embeddable'; + +export * from './embeddable'; + +export const typeFunctions = [embeddableType]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts index 717ecb33ef327f..097aef69d4b4c5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts @@ -47,6 +47,9 @@ import { rounddate } from './rounddate'; import { rowCount } from './rowCount'; import { repeatImage } from './repeatImage'; import { revealImage } from './revealImage'; +import { savedMap } from './saved_map'; +import { savedSearch } from './saved_search'; +import { savedVisualization } from './saved_visualization'; import { seriesStyle } from './seriesStyle'; import { shape } from './shape'; import { sort } from './sort'; @@ -103,6 +106,9 @@ export const functions = [ revealImage, rounddate, rowCount, + savedMap, + savedSearch, + savedVisualization, seriesStyle, shape, sort, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts new file mode 100644 index 00000000000000..25f035bbb6d8c3 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +jest.mock('ui/new_platform'); +import { savedMap } from './saved_map'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; + +const filterContext = { + and: [ + { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, + { + and: [], + column: 'time-column', + type: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('savedMap', () => { + const fn = savedMap().fn; + const args = { + id: 'some-id', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {}); + + expect(expression.input.filters).toEqual([]); + expect(expression.input.timeRange).toBeUndefined(); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {}); + const embeddableFilters = buildEmbeddableFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters.filters); + expect(expression.input.timeRange).toEqual(embeddableFilters.timeRange); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts new file mode 100644 index 00000000000000..6778c43010956e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Filter as ESFilterType } from '@kbn/es-query'; +import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/public'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter } from '../../../types'; +import { + EmbeddableTypes, + EmbeddableExpressionType, + EmbeddableExpression, +} from '../../expression_types'; +import { getFunctionHelp } from '../../strings'; + +interface Arguments { + id: string; +} + +// Map embeddable is missing proper typings, so type is just to document what we +// are expecting to pass to the embeddable +interface SavedMapInput extends EmbeddableInput { + id: string; + timeRange?: TimeRange; + refreshConfig: { + isPaused: boolean; + interval: number; + }; + filters: ESFilterType[]; +} + +type Return = EmbeddableExpression; + +export function savedMap(): ExpressionFunction<'savedMap', Filter | null, Arguments, Return> { + const { help, args: argHelp } = getFunctionHelp().savedMap; + return { + name: 'savedMap', + help, + args: { + id: { + types: ['string'], + required: false, + help: argHelp.id, + }, + }, + type: EmbeddableExpressionType, + fn: (context, { id }) => { + const filters = context ? context.and : []; + + return { + type: EmbeddableExpressionType, + input: { + id, + ...buildEmbeddableFilters(filters), + + refreshConfig: { + isPaused: false, + interval: 0, + }, + }, + embeddableType: EmbeddableTypes.map, + }; + }, + }; +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts new file mode 100644 index 00000000000000..9e5d4b2dd31a1f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +jest.mock('ui/new_platform'); +import { savedSearch } from './saved_search'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; + +const filterContext = { + and: [ + { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, + { + and: [], + column: 'time-column', + type: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('savedSearch', () => { + const fn = savedSearch().fn; + const args = { + id: 'some-id', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {}); + + expect(expression.input.filters).toEqual([]); + expect(expression.input.timeRange).toBeUndefined(); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {}); + const embeddableFilters = buildEmbeddableFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters.filters); + expect(expression.input.timeRange).toEqual(embeddableFilters.timeRange); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts new file mode 100644 index 00000000000000..40bf235761b927 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/public'; +import { SearchInput } from 'src/legacy/core_plugins/kibana/public/discover/embeddable'; +import { + EmbeddableTypes, + EmbeddableExpressionType, + EmbeddableExpression, +} from '../../expression_types'; + +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter } from '../../../types'; +import { getFunctionHelp } from '../../strings'; + +interface Arguments { + id: string; +} + +type Return = EmbeddableExpression & { id: SearchInput['id'] }>; + +export function savedSearch(): ExpressionFunction<'savedSearch', Filter | null, Arguments, Return> { + const { help, args: argHelp } = getFunctionHelp().savedSearch; + return { + name: 'savedSearch', + help, + args: { + id: { + types: ['string'], + required: false, + help: argHelp.id, + }, + }, + type: EmbeddableExpressionType, + fn: (context, { id }) => { + const filters = context ? context.and : []; + return { + type: EmbeddableExpressionType, + input: { + id, + ...buildEmbeddableFilters(filters), + }, + embeddableType: EmbeddableTypes.search, + }; + }, + }; +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts new file mode 100644 index 00000000000000..965491272cef83 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +jest.mock('ui/new_platform'); +import { savedVisualization } from './saved_visualization'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; + +const filterContext = { + and: [ + { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, + { + and: [], + column: 'time-column', + type: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('savedVisualization', () => { + const fn = savedVisualization().fn; + const args = { + id: 'some-id', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {}); + + expect(expression.input.filters).toEqual([]); + expect(expression.input.timeRange).toBeUndefined(); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {}); + const embeddableFilters = buildEmbeddableFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters.filters); + expect(expression.input.timeRange).toEqual(embeddableFilters.timeRange); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts new file mode 100644 index 00000000000000..930bdb74ee3630 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/public'; +import { VisualizeInput } from 'src/legacy/core_plugins/kibana/public/visualize/embeddable'; +import { + EmbeddableTypes, + EmbeddableExpressionType, + EmbeddableExpression, +} from '../../expression_types'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter } from '../../../types'; +import { getFunctionHelp } from '../../strings'; + +interface Arguments { + id: string; +} + +type Return = EmbeddableExpression; + +export function savedVisualization(): ExpressionFunction< + 'savedVisualization', + Filter | null, + Arguments, + Return +> { + const { help, args: argHelp } = getFunctionHelp().savedVisualization; + return { + name: 'savedVisualization', + help, + args: { + id: { + types: ['string'], + required: false, + help: argHelp.id, + }, + }, + type: EmbeddableExpressionType, + fn: (context, { id }) => { + const filters = context ? context.and : []; + + return { + type: EmbeddableExpressionType, + input: { + id, + ...buildEmbeddableFilters(filters), + }, + embeddableType: EmbeddableTypes.visualization, + }; + }, + }; +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx new file mode 100644 index 00000000000000..cf47ebab09c658 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; +import { + IEmbeddable, + EmbeddablePanel, + EmbeddableFactoryNotFoundError, + EmbeddableInput, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { start } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { EmbeddableExpression } from '../expression_types/embeddable'; +import { SavedObjectFinder } from '../../../../../../src/legacy/ui/public/saved_objects/components/saved_object_finder'; + +const embeddablesRegistry: { + [key: string]: IEmbeddable; +} = {}; + +interface Handlers { + setFilter: (text: string) => void; + getFilter: () => string | null; + done: () => void; + onResize: (fn: () => void) => void; + onDestroy: (fn: () => void) => void; +} + +const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + return ( +
+ + + +
+ ); +}; + +const embeddable = () => ({ + name: 'embeddable', + displayName: 'embeddable', + help: 'embeddable', + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + { input, embeddableType }: EmbeddableExpression, + handlers: Handlers + ) => { + if (!embeddablesRegistry[input.id]) { + const factory = Array.from(start.getEmbeddableFactories()).find( + embeddableFactory => embeddableFactory.type === embeddableType + ); + + if (!factory) { + handlers.done(); + throw new EmbeddableFactoryNotFoundError(embeddableType); + } + + const embeddableObject = await factory.createFromSavedObject(input.id, input); + embeddablesRegistry[input.id] = embeddableObject; + + ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => handlers.done()); + + handlers.onResize(() => { + ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => + handlers.done() + ); + }); + + handlers.onDestroy(() => { + delete embeddablesRegistry[input.id]; + return ReactDOM.unmountComponentAtNode(domNode); + }); + } else { + embeddablesRegistry[input.id].updateInput(input); + } + }, +}); + +export { embeddable }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js index ec2e8636690930..ef1c502aadac97 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js @@ -7,6 +7,7 @@ import { advancedFilter } from './advanced_filter'; import { dropdownFilter } from './dropdown_filter'; import { debug } from './debug'; +import { embeddable } from './embeddable'; import { error } from './error'; import { image } from './image'; import { repeatImage } from './repeat_image'; @@ -25,6 +26,7 @@ export const renderFunctions = [ advancedFilter, dropdownFilter, debug, + embeddable, error, image, repeatImage, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts index 2f86c48925c43e..302f97d06ebed4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts @@ -59,6 +59,9 @@ import { help as replace } from './replace'; import { help as revealImage } from './revealImage'; import { help as rounddate } from './rounddate'; import { help as rowCount } from './rowCount'; +import { help as savedMap } from './saved_map'; +import { help as savedSearch } from './saved_search'; +import { help as savedVisualization } from './saved_visualization'; import { help as seriesStyle } from './seriesStyle'; import { help as shape } from './shape'; import { help as sort } from './sort'; @@ -204,6 +207,9 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ revealImage, rounddate, rowCount, + savedMap, + savedSearch, + savedVisualization, seriesStyle, shape, sort, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_map.ts new file mode 100644 index 00000000000000..c63890002da466 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_map.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { savedMap } from '../../functions/common/saved_map'; +import { FunctionHelp } from '.'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp> = { + help: i18n.translate('xpack.canvas.functions.savedMapHelpText', { + defaultMessage: `Returns an embeddable for a saved map object`, + }), + args: { + id: 'The id of the saved map object', + }, +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_search.ts new file mode 100644 index 00000000000000..fb213275be2332 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_search.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { savedSearch } from '../../functions/common/saved_search'; +import { FunctionHelp } from '.'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp> = { + help: i18n.translate('xpack.canvas.functions.savedSearchHelpText', { + defaultMessage: `Returns an embeddable for a saved search object`, + }), + args: { + id: 'The id of the saved search object', + }, +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_visualization.ts new file mode 100644 index 00000000000000..9d782bbf69252f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/saved_visualization.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { savedVisualization } from '../../functions/common/saved_visualization'; +import { FunctionHelp } from '.'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp> = { + help: i18n.translate('xpack.canvas.functions.savedVisualizationHelpText', { + defaultMessage: `Returns an embeddable for a saved visualization object`, + }), + args: { + id: 'The id of the saved visualization object', + }, +}; diff --git a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts index 8ee9991ec7db45..616e45c86c4afc 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/autocomplete.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('ui/new_platform'); import { functionSpecs } from '../../__tests__/fixtures/function_specs'; import { getAutocompleteSuggestions } from './autocomplete'; diff --git a/x-pack/legacy/plugins/canvas/index.js b/x-pack/legacy/plugins/canvas/index.js index 02e9e6ac408b03..d35c920283e62a 100644 --- a/x-pack/legacy/plugins/canvas/index.js +++ b/x-pack/legacy/plugins/canvas/index.js @@ -24,7 +24,11 @@ export function canvas(kibana) { euiIconType: 'canvasApp', main: 'plugins/canvas/app', }, - interpreter: ['plugins/canvas/browser_functions', 'plugins/canvas/renderers'], + interpreter: [ + 'plugins/canvas/browser_functions', + 'plugins/canvas/renderers', + 'plugins/canvas/interpreter_expression_types', + ], styleSheetPaths: resolve(__dirname, 'public/style/index.scss'), hacks: [ // window.onerror override diff --git a/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js b/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js index 1bb2588fc90380..ad51d7b57d1ee5 100644 --- a/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js +++ b/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js @@ -5,6 +5,7 @@ */ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; import { Provider } from 'react-redux'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; @@ -29,9 +30,11 @@ export function CanvasRootController(canvasStore, $scope, $element, uiCapabiliti ); render( - - - , + + + + + , domNode ); diff --git a/x-pack/legacy/plugins/canvas/public/app.js b/x-pack/legacy/plugins/canvas/public/app.js index b43f55fea891fe..0a467d491e2c6c 100644 --- a/x-pack/legacy/plugins/canvas/public/app.js +++ b/x-pack/legacy/plugins/canvas/public/app.js @@ -20,6 +20,7 @@ import 'uiExports/visEditorTypes'; import 'uiExports/savedObjectTypes'; import 'uiExports/spyModes'; import 'uiExports/fieldFormats'; +import 'uiExports/embeddableFactories'; // load application code import './lib/load_expression_types'; diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx new file mode 100644 index 00000000000000..373f509bf7d9a1 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + SavedObjectFinder, + SavedObjectMetaData, +} from 'ui/saved_objects/components/saved_object_finder'; +import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; +import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; + +export interface Props { + onClose: () => void; + onSelect: (id: string, embeddableType: string) => void; + availableEmbeddables: string[]; +} + +export class AddEmbeddableFlyout extends React.Component { + onAddPanel = (id: string, savedObjectType: string, name: string) => { + const embeddableFactories = start.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 + ); + }); + + const foundEmbeddableType = found ? found.type : 'unknown'; + + this.props.onSelect(id, foundEmbeddableType); + }; + + render() { + const embeddableFactories = start.getEmbeddableFactories(); + + const availableSavedObjects = Array.from(embeddableFactories) + .filter(factory => { + return this.props.availableEmbeddables.includes(factory.type); + }) + .map(factory => factory.savedObjectMetaData) + .filter>(function( + maybeSavedObjectMetaData + ): maybeSavedObjectMetaData is SavedObjectMetaData<{}> { + return maybeSavedObjectMetaData !== undefined; + }); + + return ( + + + +

+ +

+
+
+ + + +
+ ); + } +} diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx new file mode 100644 index 00000000000000..3d5598c073f804 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { compose } from 'recompose'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AddEmbeddableFlyout, Props } from './flyout'; +// @ts-ignore Untyped Local +import { addElement } from '../../state/actions/elements'; +// @ts-ignore Untyped Local +import { getSelectedPage } from '../../state/selectors/workpad'; +import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; + +const allowedEmbeddables = { + [EmbeddableTypes.map]: (id: string) => { + return `filters | savedMap id="${id}" | render`; + }, + [EmbeddableTypes.visualization]: (id: string) => { + return `filters | savedVisualization id="${id}" | render`; + }, + [EmbeddableTypes.search]: (id: string) => { + return `filters | savedSearch id="${id}" | render`; + }, +}; + +interface StateProps { + pageId: number; +} + +interface DispatchProps { + addEmbeddable: (pageId: number, partialElement: { expression: string }) => void; +} + +// FIX: Missing state type +const mapStateToProps = (state: any) => ({ pageId: getSelectedPage(state) }); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + addEmbeddable: (pageId, partialElement): DispatchProps['addEmbeddable'] => + dispatch(addElement(pageId, partialElement)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: Props +): Props => { + const { pageId, ...remainingStateProps } = stateProps; + const { addEmbeddable } = dispatchProps; + + return { + ...remainingStateProps, + ...ownProps, + onSelect: (id: string, type: string): void => { + const partialElement = { + expression: `markdown "Could not find embeddable for type ${type}" | render`, + }; + if (allowedEmbeddables[type]) { + partialElement.expression = allowedEmbeddables[type](id); + } + + addEmbeddable(pageId, partialElement); + ownProps.onClose(); + }, + }; +}; + +export class EmbeddableFlyoutPortal extends React.Component { + el?: HTMLElement; + + constructor(props: Props) { + super(props); + + this.el = document.createElement('div'); + } + componentDidMount() { + const body = document.querySelector('body'); + if (body && this.el) { + body.appendChild(this.el); + } + } + + componentWillUnmount() { + const body = document.querySelector('body'); + + if (body && this.el) { + body.removeChild(this.el); + } + } + + render() { + if (this.el) { + return ReactDOM.createPortal( + , + this.el + ); + } + } +} + +export const AddEmbeddablePanel = compose( + connect( + mapStateToProps, + mapDispatchToProps, + mergeProps + ) +)(EmbeddableFlyoutPortal); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js index 9c7f3f4e311529..a3d2811c6c1be6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js @@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiButtonIcon, EuiButton, + EuiButtonEmpty, EuiOverlayMask, EuiModal, EuiModalFooter, @@ -20,6 +21,7 @@ import { import { AssetManager } from '../asset_manager'; import { ElementTypes } from '../element_types'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; +import { AddEmbeddablePanel } from '../embeddable_flyout'; import { ControlSettings } from './control_settings'; import { RefreshControl } from './refresh_control'; import { FullscreenControl } from './fullscreen_control'; @@ -32,7 +34,7 @@ export class WorkpadHeader extends React.PureComponent { toggleWriteable: PropTypes.func, }; - state = { isModalVisible: false }; + state = { isModalVisible: false, isPanelVisible: false }; _fullscreenButton = ({ toggleFullscreen }) => ( this.setState({ isModalVisible: false }); _showElementModal = () => this.setState({ isModalVisible: true }); + _hideEmbeddablePanel = () => this.setState({ isPanelVisible: false }); + _showEmbeddablePanel = () => this.setState({ isPanelVisible: true }); + _elementAdd = () => ( ); + _embeddableAdd = () => ; + _getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { if (!this.props.canUserWrite) { return "You don't have permission to edit this workpad"; @@ -98,11 +105,12 @@ export class WorkpadHeader extends React.PureComponent { render() { const { isWriteable, canUserWrite, toggleWriteable } = this.props; - const { isModalVisible } = this.state; + const { isModalVisible, isPanelVisible } = this.state; return (
{isModalVisible ? this._elementAdd() : null} + {isPanelVisible ? this._embeddableAdd() : null} + + Embed object + { } }; -const handleMouseDown = (commit, e, canvasOrigin, zoomScale) => { +const handleMouseDown = (commit, e, canvasOrigin, zoomScale, allowDrag = true) => { e.stopPropagation(); const { clientX, clientY, buttons, altKey, metaKey, shiftKey, ctrlKey } = e; if (buttons !== 1 || !commit) { resetHandler(); return; // left-click only } - setupHandler(commit, canvasOrigin, zoomScale); + + if (allowDrag) { + setupHandler(commit, canvasOrigin, zoomScale); + } + const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale); commit('mouseEvent', { event: 'mouseDown', x, y, altKey, metaKey, shiftKey, ctrlKey }); + + if (!allowDrag) { + commit('mouseEvent', { event: 'mouseUp', x, y, altKey, metaKey, shiftKey, ctrlKey }); + } }; export const eventHandlers = { - onMouseDown: props => e => handleMouseDown(props.commit, e, props.canvasOrigin, props.zoomScale), + onMouseDown: props => e => + handleMouseDown( + props.commit, + e, + props.canvasOrigin, + props.zoomScale, + props.canDragElement(e.target) + ), onMouseMove: props => e => handleMouseMove(props.commit, e, props.canvasOrigin, props.zoomScale), onMouseLeave: props => e => handleMouseLeave(props.commit, e), onWheel: props => e => handleMouseMove(props.commit, e, props.canvasOrigin), diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index d3150902fefa79..56ea35a6887ec6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -195,6 +195,11 @@ export const InteractivePage = compose( withProps(({ commit, forceRerender }) => ({ commit: (...args) => forceRerender(commit(...args)), })), + withProps((...props) => ({ + ...props, + canDragElement: element => + !element.closest('.embeddable') || element.closest('.embPanel__header'), + })), withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state () => InteractiveComponent ); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js index 448e94163d9f85..68f47f35c6fa13 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interactive_workpad_page.js @@ -89,6 +89,7 @@ export class InteractiveWorkpadPage extends PureComponent { onAnimationEnd={onAnimationEnd} onWheel={onWheel} > + {shortcuts} {elements .map(node => { @@ -127,7 +128,6 @@ export class InteractiveWorkpadPage extends PureComponent { } }) .filter(element => !!element)} -
); } diff --git a/x-pack/legacy/plugins/canvas/public/interpreter_expression_types.ts b/x-pack/legacy/plugins/canvas/public/interpreter_expression_types.ts new file mode 100644 index 00000000000000..e443f7e40879fa --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/interpreter_expression_types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { typesRegistry } from '../../../../../src/legacy/core_plugins/interpreter/public/registries'; +import { typeFunctions } from '../canvas_plugin_src/expression_types'; + +typeFunctions.forEach(r => { + typesRegistry.register(r); +}); diff --git a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.test.ts b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.test.ts new file mode 100644 index 00000000000000..d1632fc3eef285 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildEmbeddableFilters } from './build_embeddable_filters'; +import { Filter } from '../../types'; + +const columnFilter: Filter = { + and: [], + value: 'filter-value', + column: 'filter-column', + type: 'exactly', +}; + +const timeFilter: Filter = { + and: [], + column: 'time-column', + type: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', +}; + +describe('buildEmbeddableFilters', () => { + it('converts non time Canvas Filters to ES Filters ', () => { + const filters = buildEmbeddableFilters([timeFilter, columnFilter, columnFilter]); + + expect(filters.filters).toHaveLength(2); + }); + + it('converts time filter to time range', () => { + const filters = buildEmbeddableFilters([timeFilter]); + + expect(filters.timeRange!.from).toBe(timeFilter.from); + expect(filters.timeRange!.to).toBe(timeFilter.to); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts new file mode 100644 index 00000000000000..b97d520de5c5ae --- /dev/null +++ b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildQueryFilter, Filter as ESFilterType } from '@kbn/es-query'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Filter } from '../../types'; +// @ts-ignore Untyped Local +import { buildBoolArray } from './build_bool_array'; + +export interface EmbeddableFilterInput { + filters: ESFilterType[]; + timeRange?: TimeRange; +} + +const TimeFilterType = 'time'; + +function getTimeRangeFromFilters(filters: Filter[]): TimeRange | undefined { + const timeFilter = filters.find( + filter => filter.type !== undefined && filter.type === TimeFilterType + ); + + return timeFilter !== undefined && timeFilter.from !== undefined && timeFilter.to !== undefined + ? { + from: timeFilter.from, + to: timeFilter.to, + } + : undefined; +} + +function getQueryFilters(filters: Filter[]): ESFilterType[] { + return buildBoolArray(filters.filter(filter => filter.type !== 'time')).map(buildQueryFilter); +} + +export function buildEmbeddableFilters(filters: Filter[]): EmbeddableFilterInput { + return { + timeRange: getTimeRangeFromFilters(filters), + filters: getQueryFilters(filters), + }; +}