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(