diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index fa8cae2b6b86e..006e5c1d31243 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -46,6 +46,7 @@ export function createJestConfig({ ], transform: { '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + '^.+\\.html?$': 'jest-raw-loader', }, transformIgnorePatterns: [ // ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import() 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 0000000000000..dd85d3d468fc1 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.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. + */ + +import { ExpressionType } from '../../../../../../src/plugins/data/common/expressions'; +// @ts-ignore Untyped Local +import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; +// TODO: Doing this visualize import makes type_check fail +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable'; +import { EmbeddableInput } from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; + +export const EmbeddableExpressionType = 'embeddable'; + +export const EmbeddableTypes = { + map: MAP_SAVED_OBJECT_TYPE, + search: SEARCH_EMBEDDABLE_TYPE, + visualization: VISUALIZE_EMBEDDABLE_TYPE, +}; + +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/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/index.ts new file mode 100644 index 0000000000000..e73fb68165e0c --- /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 717ecb33ef327..78617d9c1cfdd 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 './savedMap'; +import { savedSearch } from './savedSearch'; +import { savedVisualization } from './savedVisualization'; 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/savedMap.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedMap.test.ts new file mode 100644 index 0000000000000..17c297aa54f25 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedMap.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. + */ + +import { savedMap } from './savedMap'; +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/savedMap.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedMap.ts new file mode 100644 index 0000000000000..aaa583b27554d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedMap.ts @@ -0,0 +1,66 @@ +/* + * 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'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter } from '../../../types'; +import { + EmbeddableTypes, + EmbeddableExpressionType, + EmbeddableExpression, +} from '../../expression_types'; + +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> { + return { + name: 'savedMap', + help: 'Render a Saved Map', + args: { + id: { + types: ['string'], + required: false, + help: 'Id of the saved map', + }, + }, + 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/savedSearch.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedSearch.test.ts new file mode 100644 index 0000000000000..91de3857f822a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedSearch.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. + */ + +import { savedSearch } from './savedSearch'; +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/savedSearch.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedSearch.ts new file mode 100644 index 0000000000000..efa818ac533b6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedSearch.ts @@ -0,0 +1,48 @@ +/* + * 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 { + EmbeddableTypes, + EmbeddableExpressionType, + EmbeddableExpression, +} from '../../expression_types'; + +import { SearchInput } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter } from '../../../types'; + +interface Arguments { + id: string; +} + +type Return = EmbeddableExpression & { id: SearchInput['id'] }>; + +export function savedSearch(): ExpressionFunction<'savedSearch', Filter | null, Arguments, Return> { + return { + name: 'savedSearch', + help: 'Render a Saved Search Query', + args: { + id: { + types: ['string'], + required: false, + help: 'Id of the saved search', + }, + }, + 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/savedVisualization.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedVisualization.test.ts new file mode 100644 index 0000000000000..5fa4ad3fa4d0d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedVisualization.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. + */ + +import { savedVisualization } from './savedVisualization'; +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/savedVisualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedVisualization.ts new file mode 100644 index 0000000000000..3b72e1fb88daa --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/savedVisualization.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 { + EmbeddableTypes, + EmbeddableExpressionType, + EmbeddableExpression, +} from '../../expression_types'; +// import { VisualizeInput } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable'; +import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter } from '../../../types'; + +interface Arguments { + id: string; +} + +// TODO: Importing from visualize/embeddable chokes type_check script +// Using an any here now until we can get that resolved +type Return = EmbeddableExpression; + +export function savedVisualization(): ExpressionFunction< + 'savedVisualization', + Filter | null, + Arguments, + Return +> { + return { + name: 'savedVisualization', + help: 'Render a Saved Search Query', + args: { + id: { + types: ['string'], + required: false, + help: 'Id of the saved search', + }, + }, + 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 0000000000000..b3d7093548207 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + IEmbeddable, + EmbeddablePanel, + embeddableFactories, + EmbeddableInput, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; + +import { EmbeddableFactoryNotFoundError } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/embeddables'; + +import { EmbeddableExpression } from '../expression_types/embeddable'; + +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 = embeddableFactories.get(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 ec2e863669093..ef1c502aadac9 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 2f86c48925c43..9eed52c28c17d 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 './savedMap'; +import { help as savedSearch } from './savedSearch'; +import { help as savedVisualization } from './savedVisualization'; 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/savedMap.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/savedMap.ts new file mode 100644 index 0000000000000..f172408fb247e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/savedMap.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/savedMap'; +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/savedSearch.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/savedSearch.ts new file mode 100644 index 0000000000000..7776540ef6d74 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/savedSearch.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/savedSearch'; +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/savedVisualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/savedVisualization.ts new file mode 100644 index 0000000000000..938d4db1fe521 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/savedVisualization.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/savedVisualization'; +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/index.js b/x-pack/legacy/plugins/canvas/index.js index 02e9e6ac408b0..d35c920283e62 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 1bb2588fc9038..ad51d7b57d1ee 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 b43f55fea891f..0a467d491e2c6 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 0000000000000..f64689c747283 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -0,0 +1,74 @@ +/* + * 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 { embeddableFactories } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; + +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) => { + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories.entries()).find(([_key, embeddableFactory]) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); + + const foundEmbeddableType = found ? found[0] : 'unknown'; + + this.props.onSelect(id, foundEmbeddableType); + }; + + render() { + const availableSavedObjects = Array.from(embeddableFactories.entries()) + .filter(([key]) => { + return this.props.availableEmbeddables.includes(key); + }) + .map(([_key, { savedObjectMetaData }]) => 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 0000000000000..740f38793e7fd --- /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; +} + +// 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 498d17db4e0dc..5a5167f21c772 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 d3150902fefa7..56ea35a6887ec 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 448e94163d9f8..68f47f35c6fa1 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 0000000000000..e443f7e40879f --- /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 0000000000000..d1632fc3eef28 --- /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 0000000000000..b97d520de5c5a --- /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), + }; +}