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 index 063e69d1d2141..e728ea25f5504 100644 --- 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 @@ -5,11 +5,11 @@ */ import { ExpressionType } from 'src/plugins/expressions/public'; -import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { EmbeddableInput } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; -export { EmbeddableTypes }; +export { EmbeddableTypes, EmbeddableInput }; export interface EmbeddableExpression { type: typeof EmbeddableExpressionType; 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 index 3669bd3e08201..8f5ad859d28ba 100644 --- 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 @@ -9,7 +9,7 @@ import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize_embeddable/constants'; import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; -export const EmbeddableTypes = { +export const EmbeddableTypes: { map: string; search: string; visualization: string } = { map: MAP_SAVED_OBJECT_TYPE, search: SEARCH_EMBEDDABLE_TYPE, visualization: VISUALIZE_EMBEDDABLE_TYPE, 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 097aef69d4b4c..48b50930d563e 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 @@ -32,6 +32,7 @@ import { image } from './image'; import { joinRows } from './join_rows'; import { lt } from './lt'; import { lte } from './lte'; +import { mapCenter } from './map_center'; import { mapColumn } from './mapColumn'; import { math } from './math'; import { metric } from './metric'; @@ -57,6 +58,7 @@ import { staticColumn } from './staticColumn'; import { string } from './string'; import { table } from './table'; import { tail } from './tail'; +import { timerange } from './time_range'; import { timefilter } from './timefilter'; import { timefilterControl } from './timefilterControl'; import { switchFn } from './switch'; @@ -91,6 +93,7 @@ export const functions = [ lt, lte, joinRows, + mapCenter, mapColumn, math, metric, @@ -118,6 +121,7 @@ export const functions = [ tail, timefilter, timefilterControl, + timerange, switchFn, caseFn, ]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts new file mode 100644 index 0000000000000..21f9e9fe3148d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.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/plugins/expressions/common'; +import { getFunctionHelp } from '../../../i18n/functions'; +import { MapCenter } from '../../../types'; + +interface Args { + lat: number; + lon: number; + zoom: number; +} + +export function mapCenter(): ExpressionFunction<'mapCenter', null, Args, MapCenter> { + const { help, args: argHelp } = getFunctionHelp().mapCenter; + return { + name: 'mapCenter', + help, + type: 'mapCenter', + context: { + types: ['null'], + }, + args: { + lat: { + types: ['number'], + required: true, + help: argHelp.lat, + }, + lon: { + types: ['number'], + required: true, + help: argHelp.lon, + }, + zoom: { + types: ['number'], + required: true, + help: argHelp.zoom, + }, + }, + fn: (context, args) => { + return { + type: 'mapCenter', + ...args, + }; + }, + }; +} 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 index 25f035bbb6d8c..5b95886faa13d 100644 --- 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 @@ -5,7 +5,7 @@ */ jest.mock('ui/new_platform'); import { savedMap } from './saved_map'; -import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../server/lib/build_embeddable_filters'; const filterContext = { and: [ @@ -24,20 +24,22 @@ describe('savedMap', () => { const fn = savedMap().fn; const args = { id: 'some-id', + center: null, + title: null, + timerange: null, + hideLayer: [], }; 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); + const embeddableFilters = getQueryFilters(filterContext.and); - expect(expression.input.filters).toEqual(embeddableFilters.filters); - expect(expression.input.timeRange).toEqual(embeddableFilters.timeRange); + expect(expression.input.filters).toEqual(embeddableFilters); }); }); 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 index 460cb9c34efff..b6d88c06ed06d 100644 --- 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 @@ -7,8 +7,8 @@ import { ExpressionFunction } from 'src/plugins/expressions/common/types'; import { TimeRange } from 'src/plugins/data/public'; 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 { getQueryFilters } from '../../../server/lib/build_embeddable_filters'; +import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -19,19 +19,36 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; interface Arguments { id: string; + center: MapCenter | null; + hideLayer: string[]; + title: string | null; + timerange: TimeRangeArg | null; } // 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 { +export type SavedMapInput = EmbeddableInput & { id: string; + isLayerTOCOpen: boolean; timeRange?: TimeRange; refreshConfig: { isPaused: boolean; interval: number; }; + hideFilterActions: true; filters: esFilters.Filter[]; -} + mapCenter?: { + lat: number; + lon: number; + zoom: number; + }; + hiddenLayers?: string[]; +}; + +const defaultTimeRange = { + from: 'now-15m', + to: 'now', +}; type Return = EmbeddableExpression; @@ -46,21 +63,56 @@ export function savedMap(): ExpressionFunction<'savedMap', Filter | null, Argume required: false, help: argHelp.id, }, + center: { + types: ['mapCenter'], + help: argHelp.center, + required: false, + }, + hideLayer: { + types: ['string'], + help: argHelp.hideLayer, + required: false, + multi: true, + }, + timerange: { + types: ['timerange'], + help: argHelp.timerange, + required: false, + }, + title: { + types: ['string'], + help: argHelp.title, + required: false, + }, }, type: EmbeddableExpressionType, - fn: (context, { id }) => { + fn: (context, args) => { const filters = context ? context.and : []; + const center = args.center + ? { + lat: args.center.lat, + lon: args.center.lon, + zoom: args.center.zoom, + } + : undefined; + return { type: EmbeddableExpressionType, input: { - id, - ...buildEmbeddableFilters(filters), - + id: args.id, + filters: getQueryFilters(filters), + timeRange: args.timerange || defaultTimeRange, refreshConfig: { isPaused: false, interval: 0, }, + + mapCenter: center, + hideFilterActions: true, + title: args.title ? args.title : undefined, + isLayerTOCOpen: false, + hiddenLayers: args.hideLayer || [], }, embeddableType: EmbeddableTypes.map, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts new file mode 100644 index 0000000000000..716026279ccea --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts @@ -0,0 +1,44 @@ +/* + * 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/plugins/expressions/common'; +import { getFunctionHelp } from '../../../i18n/functions'; +import { TimeRange } from '../../../types'; + +interface Args { + from: string; + to: string; +} + +export function timerange(): ExpressionFunction<'timerange', null, Args, TimeRange> { + const { help, args: argHelp } = getFunctionHelp().timerange; + return { + name: 'timerange', + help, + type: 'timerange', + context: { + types: ['null'], + }, + args: { + from: { + types: ['string'], + required: true, + help: argHelp.from, + }, + to: { + types: ['string'], + required: true, + help: argHelp.to, + }, + }, + fn: (context, args) => { + return { + type: 'timerange', + ...args, + }; + }, + }; +} 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/embeddable.tsx similarity index 74% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 5c7ef1a8c1799..8642ebd901bb4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -10,32 +10,27 @@ import { I18nContext } from 'ui/i18n'; import { npStart } from 'ui/new_platform'; import { IEmbeddable, + EmbeddableFactory, 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 { RendererStrings } from '../../i18n'; +} from '../../../../../../../src/plugins/embeddable/public'; +import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { EmbeddableExpression } from '../../expression_types/embeddable'; +import { RendererStrings } from '../../../i18n'; import { SavedObjectFinderProps, SavedObjectFinderUi, -} from '../../../../../../src/plugins/kibana_react/public'; +} from '../../../../../../../src/plugins/kibana_react/public'; const { embeddable: strings } = RendererStrings; +import { embeddableInputToExpression } from './embeddable_input_to_expression'; +import { EmbeddableInput } from '../../expression_types'; +import { RendererHandlers } from '../../../types'; 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) => { const SavedObjectFinder = (props: SavedObjectFinderProps) => ( ({ render: async ( domNode: HTMLElement, { input, embeddableType }: EmbeddableExpression, - handlers: Handlers + handlers: RendererHandlers ) => { if (!embeddablesRegistry[input.id]) { const factory = Array.from(start.getEmbeddableFactories()).find( embeddableFactory => embeddableFactory.type === embeddableType - ); + ) as EmbeddableFactory; if (!factory) { handlers.done(); @@ -86,8 +81,13 @@ const embeddable = () => ({ } const embeddableObject = await factory.createFromSavedObject(input.id, input); + embeddablesRegistry[input.id] = embeddableObject; + ReactDOM.unmountComponentAtNode(domNode); + const subscription = embeddableObject.getInput$().subscribe(function(updatedInput) { + handlers.onEmbeddableInputChange(embeddableInputToExpression(updatedInput, embeddableType)); + }); ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => handlers.done()); handlers.onResize(() => { @@ -97,7 +97,11 @@ const embeddable = () => ({ }); handlers.onDestroy(() => { + subscription.unsubscribe(); + handlers.onEmbeddableDestroyed(); + delete embeddablesRegistry[input.id]; + return ReactDOM.unmountComponentAtNode(domNode); }); } else { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts new file mode 100644 index 0000000000000..93d747537c34c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { embeddableInputToExpression } from './embeddable_input_to_expression'; +import { SavedMapInput } from '../../functions/common/saved_map'; +import { EmbeddableTypes } from '../../expression_types'; +import { fromExpression, Ast } from '@kbn/interpreter/common'; + +const baseSavedMapInput = { + id: 'embeddableId', + filters: [], + isLayerTOCOpen: false, + refreshConfig: { + isPaused: true, + interval: 0, + }, + hideFilterActions: true as true, +}; + +describe('input to expression', () => { + describe('Map Embeddable', () => { + it('converts to a savedMap expression', () => { + const input: SavedMapInput = { + ...baseSavedMapInput, + }; + + const expression = embeddableInputToExpression(input, EmbeddableTypes.map); + const ast = fromExpression(expression); + + 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).not.toHaveProperty('title'); + expect(ast.chain[0].arguments).not.toHaveProperty('center'); + expect(ast.chain[0].arguments).not.toHaveProperty('timerange'); + }); + + it('includes optional input values', () => { + const input: SavedMapInput = { + ...baseSavedMapInput, + mapCenter: { + lat: 1, + lon: 2, + zoom: 3, + }, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = embeddableInputToExpression(input, EmbeddableTypes.map); + const ast = fromExpression(expression); + + const centerExpression = ast.chain[0].arguments.center[0] as Ast; + + expect(centerExpression.chain[0].function).toBe('mapCenter'); + expect(centerExpression.chain[0].arguments.lat[0]).toEqual(input.mapCenter?.lat); + expect(centerExpression.chain[0].arguments.lon[0]).toEqual(input.mapCenter?.lon); + expect(centerExpression.chain[0].arguments.zoom[0]).toEqual(input.mapCenter?.zoom); + + const timerangeExpression = ast.chain[0].arguments.timerange[0] as Ast; + + expect(timerangeExpression.chain[0].function).toBe('timerange'); + expect(timerangeExpression.chain[0].arguments.from[0]).toEqual(input.timeRange?.from); + expect(timerangeExpression.chain[0].arguments.to[0]).toEqual(input.timeRange?.to); + }); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts new file mode 100644 index 0000000000000..a3cb53acebed2 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.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 { EmbeddableTypes, EmbeddableInput } from '../../expression_types'; +import { SavedMapInput } from '../../functions/common/saved_map'; + +/* + Take the input from an embeddable and the type of embeddable and convert it into an expression +*/ +export function embeddableInputToExpression( + input: EmbeddableInput, + embeddableType: string +): string { + const expressionParts: string[] = []; + + if (embeddableType === EmbeddableTypes.map) { + const mapInput = input as SavedMapInput; + + expressionParts.push('savedMap'); + + expressionParts.push(`id="${input.id}"`); + + if (input.title) { + expressionParts.push(`title="${input.title}"`); + } + + if (mapInput.mapCenter) { + expressionParts.push( + `center={mapCenter lat=${mapInput.mapCenter.lat} lon=${mapInput.mapCenter.lon} zoom=${mapInput.mapCenter.zoom}}` + ); + } + + if (mapInput.timeRange) { + expressionParts.push( + `timerange={timerange from="${mapInput.timeRange.from}" to="${mapInput.timeRange.to}"}` + ); + } + + if (mapInput.hiddenLayers && mapInput.hiddenLayers.length) { + for (const layerId of mapInput.hiddenLayers) { + expressionParts.push(`hideLayer="${layerId}"`); + } + } + } + + return expressionParts.join(' '); +} 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 50fa6943fc74a..48364be06e539 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,7 +7,7 @@ import { advancedFilter } from './advanced_filter'; import { debug } from './debug'; import { dropdownFilter } from './dropdown_filter'; -import { embeddable } from './embeddable'; +import { embeddable } from './embeddable/embeddable'; import { error } from './error'; import { image } from './image'; import { markdown } from './markdown'; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/map_center.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/map_center.ts new file mode 100644 index 0000000000000..3022ad07089d2 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/map_center.ts @@ -0,0 +1,27 @@ +/* + * 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 { mapCenter } from '../../../canvas_plugin_src/functions/common/map_center'; +import { FunctionHelp } from '../'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp> = { + help: i18n.translate('xpack.canvas.functions.mapCenterHelpText', { + defaultMessage: `Returns an object with the center coordinates and zoom level of the map`, + }), + args: { + lat: i18n.translate('xpack.canvas.functions.mapCenter.args.latHelpText', { + defaultMessage: `Latitude for the center of the map`, + }), + lon: i18n.translate('xpack.canvas.functions.savedMap.args.lonHelpText', { + defaultMessage: `Longitude for the center of the map`, + }), + zoom: i18n.translate('xpack.canvas.functions.savedMap.args.zoomHelpText', { + defaultMessage: `The zoom level of the map`, + }), + }, +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts index d01b77e1cfd51..53bcd481f185f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts @@ -14,6 +14,20 @@ export const help: FunctionHelp> = { defaultMessage: `Returns an embeddable for a saved map object`, }), args: { - id: 'The id of the saved map object', + id: i18n.translate('xpack.canvas.functions.savedMap.args.idHelpText', { + defaultMessage: `The ID of the Saved Map Object`, + }), + center: i18n.translate('xpack.canvas.functions.savedMap.args.centerHelpText', { + defaultMessage: `The center and zoom level the map should have`, + }), + hideLayer: i18n.translate('xpack.canvas.functions.savedMap.args.hideLayer', { + defaultMessage: `The IDs of map layers that should be hidden`, + }), + timerange: i18n.translate('xpack.canvas.functions.savedMap.args.timerangeHelpText', { + defaultMessage: `The timerange of data that should be included`, + }), + title: i18n.translate('xpack.canvas.functions.savedMap.args.titleHelpText', { + defaultMessage: `The title for the map`, + }), }, }; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/time_range.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/time_range.ts new file mode 100644 index 0000000000000..476a9978800df --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/time_range.ts @@ -0,0 +1,24 @@ +/* + * 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 { timerange } from '../../../canvas_plugin_src/functions/common/time_range'; +import { FunctionHelp } from '../function_help'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp> = { + help: i18n.translate('xpack.canvas.functions.timerangeHelpText', { + defaultMessage: `An object that represents a span of time`, + }), + args: { + from: i18n.translate('xpack.canvas.functions.timerange.args.fromHelpText', { + defaultMessage: `The start of the time range`, + }), + to: i18n.translate('xpack.canvas.functions.timerange.args.toHelpText', { + defaultMessage: `The end of the time range`, + }), + }, +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts b/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts index f6b3c451c6fbb..94d7e6f43326f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts @@ -44,6 +44,7 @@ import { help as joinRows } from './dict/join_rows'; import { help as location } from './dict/location'; import { help as lt } from './dict/lt'; import { help as lte } from './dict/lte'; +import { help as mapCenter } from './dict/map_center'; import { help as mapColumn } from './dict/map_column'; import { help as markdown } from './dict/markdown'; import { help as math } from './dict/math'; @@ -75,6 +76,7 @@ import { help as tail } from './dict/tail'; import { help as timefilter } from './dict/timefilter'; import { help as timefilterControl } from './dict/timefilter_control'; import { help as timelion } from './dict/timelion'; +import { help as timerange } from './dict/time_range'; import { help as to } from './dict/to'; import { help as urlparam } from './dict/urlparam'; @@ -196,6 +198,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ location, lt, lte, + mapCenter, mapColumn, markdown, math, @@ -227,6 +230,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ timefilter, timefilterControl, timelion, + timerange, to, urlparam, }); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js b/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js index 89c0b5b21c581..1926fb4aaa5eb 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js @@ -47,7 +47,14 @@ export const ElementContent = compose( pure, ...branches )(({ renderable, renderFunction, size, handlers }) => { - const { getFilter, setFilter, done, onComplete } = handlers; + const { + getFilter, + setFilter, + done, + onComplete, + onEmbeddableInputChange, + onEmbeddableDestroyed, + } = handlers; return Style.it( renderable.css, @@ -69,7 +76,7 @@ export const ElementContent = compose( config={renderable.value} css={renderable.css} // This is an actual CSS stylesheet string, it will be scoped by RenderElement size={size} // Size is only passed for the purpose of triggering the resize event, it isn't really used otherwise - handlers={{ getFilter, setFilter, done }} + handlers={{ getFilter, setFilter, done, onEmbeddableInputChange, onEmbeddableDestroyed }} /> diff --git a/x-pack/legacy/plugins/canvas/public/components/element_wrapper/lib/handlers.js b/x-pack/legacy/plugins/canvas/public/components/element_wrapper/lib/handlers.js index ce6791f2f88b6..e93cea597901f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_wrapper/lib/handlers.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_wrapper/lib/handlers.js @@ -6,6 +6,10 @@ import { isEqual } from 'lodash'; import { setFilter } from '../../../state/actions/elements'; +import { + updateEmbeddableExpression, + fetchEmbeddableRenderable, +} from '../../../state/actions/embeddable'; export const createHandlers = dispatch => { let isComplete = false; @@ -32,6 +36,14 @@ export const createHandlers = dispatch => { completeFn = fn; }, + onEmbeddableInputChange(embeddableExpression) { + dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); + }, + + onEmbeddableDestroyed() { + dispatch(fetchEmbeddableRenderable(element.id)); + }, + done() { // don't emit if the element is already done if (isComplete) { 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 index c54c56e1561ca..565ca5fa5bbd6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -19,14 +19,15 @@ import { withKibana } from '../../../../../../../src/plugins/kibana_react/public const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { - return `filters | savedMap id="${id}" | render`; + return `savedMap id="${id}" | render`; }, - [EmbeddableTypes.visualization]: (id: string) => { + // FIX: Only currently allow Map embeddables + /* [EmbeddableTypes.visualization]: (id: string) => { return `filters | savedVisualization id="${id}" | render`; }, [EmbeddableTypes.search]: (id: string) => { return `filters | savedSearch id="${id}" | render`; - }, + },*/ }; interface StateProps { 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 4ee3a65172a2e..b775524acf639 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 @@ -73,6 +73,32 @@ function closest(s) { return null; } +// If you interact with an embeddable panel, only the header should be draggable +// This function will determine if an element is an embeddable body or not +const isEmbeddableBody = element => { + const hasClosest = typeof element.closest === 'function'; + + if (hasClosest) { + return element.closest('.embeddable') && !element.closest('.embPanel__header'); + } else { + return closest.call(element, '.embeddable') && !closest.call(element, '.embPanel__header'); + } +}; + +// Some elements in an embeddable may be portaled out of the embeddable container. +// We do not want clicks on those to trigger drags, etc, in the workpad. This function +// will check to make sure the clicked item is actually in the container +const isInWorkpad = element => { + const hasClosest = typeof element.closest === 'function'; + const workpadContainerSelector = '.canvasWorkpadContainer'; + + if (hasClosest) { + return !!element.closest(workpadContainerSelector); + } else { + return !!closest.call(element, workpadContainerSelector); + } +}; + const componentLayoutState = ({ aeroStore, setAeroStore, @@ -209,6 +235,8 @@ export const InteractivePage = compose( withProps((...props) => ({ ...props, canDragElement: element => { + return !isEmbeddableBody(element) && isInWorkpad(element); + const hasClosest = typeof element.closest === 'function'; if (hasClosest) { diff --git a/x-pack/legacy/plugins/canvas/public/state/actions/embeddable.ts b/x-pack/legacy/plugins/canvas/public/state/actions/embeddable.ts new file mode 100644 index 0000000000000..3604d7e3c2141 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/state/actions/embeddable.ts @@ -0,0 +1,36 @@ +/* + * 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 { Dispatch } from 'redux'; +import { createAction } from 'redux-actions'; +// @ts-ignore Untyped +import { createThunk } from 'redux-thunks'; +// @ts-ignore Untyped Local +import { fetchRenderable } from './elements'; +import { State } from '../../../types'; + +export const UpdateEmbeddableExpressionActionType = 'updateEmbeddableExpression'; +export interface UpdateEmbeddableExpressionPayload { + embeddableExpression: string; + elementId: string; +} +export const updateEmbeddableExpression = createAction( + UpdateEmbeddableExpressionActionType +); + +export const fetchEmbeddableRenderable = createThunk( + 'fetchEmbeddableRenderable', + ({ dispatch, getState }: { dispatch: Dispatch; getState: () => State }, elementId: string) => { + const pageWithElement = getState().persistent.workpad.pages.find(page => { + return page.elements.find(element => element.id === elementId) !== undefined; + }); + + if (pageWithElement) { + const element = pageWithElement.elements.find(el => el.id === elementId); + dispatch(fetchRenderable(element)); + } + } +); diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js b/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js index 10a5bdb5998ea..c7e8a5c2ff2d8 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js @@ -28,7 +28,7 @@ function getNodeIndexById(page, nodeId, location) { return page[location].findIndex(node => node.id === nodeId); } -function assignNodeProperties(workpadState, pageId, nodeId, props) { +export function assignNodeProperties(workpadState, pageId, nodeId, props) { const pageIndex = getPageIndexById(workpadState, pageId); const location = getLocationFromIds(workpadState, pageId, nodeId); const nodesPath = `pages.${pageIndex}.${location}`; diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/legacy/plugins/canvas/public/state/reducers/embeddable.ts new file mode 100644 index 0000000000000..9969c38cfa767 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/embeddable.ts @@ -0,0 +1,67 @@ +/* + * 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 { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { handleActions } from 'redux-actions'; +import { State } from '../../../types'; + +import { + UpdateEmbeddableExpressionActionType, + UpdateEmbeddableExpressionPayload, +} from '../actions/embeddable'; + +// @ts-ignore untyped local +import { assignNodeProperties } from './elements'; + +export const embeddableReducer = handleActions< + State['persistent']['workpad'], + UpdateEmbeddableExpressionPayload +>( + { + [UpdateEmbeddableExpressionActionType]: (workpadState, { payload }) => { + if (!payload) { + return workpadState; + } + + const { elementId, embeddableExpression } = payload; + + // Find the element + const pageWithElement = workpadState.pages.find(page => { + return page.elements.find(element => element.id === elementId) !== undefined; + }); + + if (!pageWithElement) { + return workpadState; + } + + const element = pageWithElement.elements.find(elem => elem.id === elementId); + + if (!element) { + return workpadState; + } + + const existingAst = fromExpression(element.expression); + const newAst = fromExpression(embeddableExpression); + const searchForFunction = newAst.chain[0].function; + + // Find the first matching function in the existing ASt + const existingAstFunction = existingAst.chain.find(f => f.function === searchForFunction); + + if (!existingAstFunction) { + return workpadState; + } + + existingAstFunction.arguments = newAst.chain[0].arguments; + + const updatedExpression = toExpression(existingAst); + + return assignNodeProperties(workpadState, pageWithElement.id, elementId, { + expression: updatedExpression, + }); + }, + }, + {} as State['persistent']['workpad'] +); diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/embeddables.test.ts b/x-pack/legacy/plugins/canvas/public/state/reducers/embeddables.test.ts new file mode 100644 index 0000000000000..5b1192630897a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/embeddables.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { State } from '../../../types'; +import { updateEmbeddableExpression } from '../actions/embeddable'; +import { embeddableReducer } from './embeddable'; + +const elementId = 'element-1111'; +const embeddableId = '1234'; +const mockWorkpadState = { + pages: [ + { + elements: [ + { + id: elementId, + expression: `function1 | function2 id="${embeddableId}" change="start value" remove="remove"`, + }, + ], + }, + ], +} as State['persistent']['workpad']; + +describe('embeddables reducer', () => { + it('updates the functions expression', () => { + const updatedValue = 'updated value'; + + const action = updateEmbeddableExpression({ + elementId, + embeddableExpression: `function2 id="${embeddableId}" change="${updatedValue}" add="add"`, + }); + + const newState = embeddableReducer(mockWorkpadState, action); + + expect(newState.pages[0].elements[0].expression.replace(/\s/g, '')).toBe( + `function1 | ${action.payload!.embeddableExpression}`.replace(/\s/g, '') + ); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/index.js b/x-pack/legacy/plugins/canvas/public/state/reducers/index.js index b60a0a3b32656..cec6f9dceef6d 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/index.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/index.js @@ -16,6 +16,7 @@ import { pagesReducer } from './pages'; import { elementsReducer } from './elements'; import { assetsReducer } from './assets'; import { historyReducer } from './history'; +import { embeddableReducer } from './embeddable'; export function getRootReducer(initialState) { return combineReducers({ @@ -25,7 +26,7 @@ export function getRootReducer(initialState) { persistent: reduceReducers( historyReducer, combineReducers({ - workpad: reduceReducers(workpadReducer, pagesReducer, elementsReducer), + workpad: reduceReducers(workpadReducer, pagesReducer, elementsReducer, embeddableReducer), schemaVersion: (state = get(initialState, 'persistent.schemaVersion')) => state, }) ), 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 index d1632fc3eef28..b422a9451293f 100644 --- 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 @@ -23,10 +23,10 @@ const timeFilter: Filter = { }; describe('buildEmbeddableFilters', () => { - it('converts non time Canvas Filters to ES Filters ', () => { + it('converts all Canvas Filters to ES Filters ', () => { const filters = buildEmbeddableFilters([timeFilter, columnFilter, columnFilter]); - expect(filters.filters).toHaveLength(2); + expect(filters.filters).toHaveLength(3); }); it('converts time filter to time range', () => { 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 index 52fcc9813a93d..1a78a1e057016 100644 --- a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts +++ b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts @@ -35,10 +35,8 @@ function getTimeRangeFromFilters(filters: Filter[]): TimeRange | undefined { : undefined; } -function getQueryFilters(filters: Filter[]): esFilters.Filter[] { - return buildBoolArray(filters.filter(filter => filter.type !== 'time')).map( - esFilters.buildQueryFilter - ); +export function getQueryFilters(filters: Filter[]): esFilters.Filter[] { + return buildBoolArray(filters).map(esFilters.buildQueryFilter); } export function buildEmbeddableFilters(filters: Filter[]): EmbeddableFilterInput { diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/rendered_element.tsx index 03b3e0df8a0cf..317a3417841b8 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/rendered_element.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/rendered_element.tsx @@ -69,6 +69,8 @@ export class RenderedElementComponent extends PureComponent { onResize: () => {}, setFilter: () => {}, getFilter: () => '', + onEmbeddableInputChange: () => {}, + onEmbeddableDestroyed: () => {}, }); } catch (e) { // eslint-disable-next-line no-console diff --git a/x-pack/legacy/plugins/canvas/types/functions.ts b/x-pack/legacy/plugins/canvas/types/functions.ts index 6510c018f1ed4..773c9c3020a85 100644 --- a/x-pack/legacy/plugins/canvas/types/functions.ts +++ b/x-pack/legacy/plugins/canvas/types/functions.ts @@ -192,3 +192,16 @@ export interface AxisConfig { */ export const isAxisConfig = (axisConfig: any): axisConfig is AxisConfig => !!axisConfig && axisConfig.type === 'axisConfig'; + +export interface MapCenter { + type: 'mapCenter'; + lat: number; + lon: number; + zoom: number; +} + +export interface TimeRange { + type: 'timerange'; + from: string; + to: string; +} diff --git a/x-pack/legacy/plugins/canvas/types/renderers.ts b/x-pack/legacy/plugins/canvas/types/renderers.ts index 282a1c820e346..af1710e69c257 100644 --- a/x-pack/legacy/plugins/canvas/types/renderers.ts +++ b/x-pack/legacy/plugins/canvas/types/renderers.ts @@ -17,6 +17,10 @@ export interface RendererHandlers { getFilter: () => string; /** Sets the value of the filter property on the element object persisted on the workpad */ setFilter: (filter: string) => void; + /** Handler to invoke when the input to a function has changed internally */ + onEmbeddableInputChange: (expression: string) => void; + /** Handler to invoke when a rendered embeddable is destroyed */ + onEmbeddableDestroyed: () => void; } export interface RendererSpec { diff --git a/x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx b/x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx index 90393f9f4ff6f..9880a2b811f8b 100644 --- a/x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx @@ -137,7 +137,7 @@ export class CustomizeTimeRangeModal extends Component {i18n.translate(