[7.x] [Canvas] Enable Embeddable maps (#53971) (#54649)

* [Canvas] Enable Embeddable maps (#53971)

* Enables Embeddable maps in Canvas. Updates expressions as maps are interacted with

* Fix type check errors

* Update imports. Remove filters from initial embed expressions

* Adds hide layer functionality to canvas map embeds

* Fix typecheck error

* Fix Type check

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

* Re-enable embeds in Canvas

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Corey Robertson 2020-01-14 10:16:41 -05:00 committed by GitHub
parent 5dce5b4acc
commit 6c30f40c7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 615 additions and 56 deletions

View file

@ -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<Input extends EmbeddableInput> {
type: typeof EmbeddableExpressionType;

View file

@ -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,

View file

@ -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';
@ -47,8 +48,8 @@ import { rounddate } from './rounddate';
import { rowCount } from './rowCount';
import { repeatImage } from './repeatImage';
import { revealImage } from './revealImage';
import { savedMap } from './saved_map';
// TODO: elastic/kibana#44822 Disabling pending filters work
// import { savedMap } from './saved_map';
// import { savedSearch } from './saved_search';
// import { savedVisualization } from './saved_visualization';
import { seriesStyle } from './seriesStyle';
@ -58,6 +59,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';
@ -92,6 +94,7 @@ export const functions = [
lt,
lte,
joinRows,
mapCenter,
mapColumn,
math,
metric,
@ -107,8 +110,8 @@ export const functions = [
revealImage,
rounddate,
rowCount,
savedMap,
// TODO: elastic/kibana#44822 Disabling pending filters work
// savedMap,
// savedSearch,
// savedVisualization,
seriesStyle,
@ -120,6 +123,7 @@ export const functions = [
tail,
timefilter,
timefilterControl,
timerange,
switchFn,
caseFn,
];

View file

@ -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,
};
},
};
}

View file

@ -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);
});
});

View file

@ -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<SavedMapInput>;
@ -47,21 +64,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,
};

View file

@ -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,
};
},
};
}

View file

@ -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) => (
<SavedObjectFinderUi
@ -73,12 +68,12 @@ const embeddable = () => ({
render: async (
domNode: HTMLElement,
{ input, embeddableType }: EmbeddableExpression<EmbeddableInput>,
handlers: Handlers
handlers: RendererHandlers
) => {
if (!embeddablesRegistry[input.id]) {
const factory = Array.from(start.getEmbeddableFactories()).find(
embeddableFactory => embeddableFactory.type === embeddableType
);
) as EmbeddableFactory<EmbeddableInput>;
if (!factory) {
handlers.done();
@ -86,8 +81,13 @@ const embeddable = () => ({
}
const embeddableObject = await factory.createFromSavedObject(input.id, input);
embeddablesRegistry[input.id] = embeddableObject;
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 {

View file

@ -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);
});
});
});

View file

@ -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(' ');
}

View file

@ -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';

View file

@ -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<FunctionFactory<typeof mapCenter>> = {
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`,
}),
},
};

View file

@ -14,6 +14,20 @@ export const help: FunctionHelp<FunctionFactory<typeof savedMap>> = {
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`,
}),
},
};

View file

@ -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<FunctionFactory<typeof timerange>> = {
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`,
}),
},
};

View file

@ -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,
@ -213,9 +216,8 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
revealImage,
rounddate,
rowCount,
// TODO: elastic/kibana#44822 Disabling pending filters work
// @ts-ignore
savedMap,
// TODO: elastic/kibana#44822 Disabling pending filters work
// @ts-ignore
savedSearch,
// @ts-ignore
@ -231,6 +233,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
timefilter,
timefilterControl,
timelion,
timerange,
to,
urlparam,
});

View file

@ -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 }}
/>
</ElementShareContainer>
</div>

View file

@ -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) {

View file

@ -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 {

View file

@ -13,6 +13,7 @@ import {
EuiFlexGroup,
EuiButtonIcon,
EuiButton,
EuiButtonEmpty,
EuiOverlayMask,
EuiModal,
EuiModalFooter,
@ -193,14 +194,13 @@ export class WorkpadHeader extends React.PureComponent<Props, State> {
<EuiFlexItem grow={false}>
<AssetManager />
</EuiFlexItem>
{/*
TODO: elastic/kibana#44822 Disabling pending filters work
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this._showEmbeddablePanel}>
{strings.getEmbedObjectButtonLabel()}
</EuiButtonEmpty>
</EuiFlexItem>
*/}
<EuiFlexItem grow={false}>
<EuiButton
fill

View file

@ -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) {

View file

@ -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<UpdateEmbeddableExpressionPayload>(
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));
}
}
);

View file

@ -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}`;

View file

@ -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']
);

View file

@ -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, '')
);
});
});

View file

@ -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,
})
),

View file

@ -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', () => {

View file

@ -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 {

View file

@ -69,6 +69,8 @@ export class RenderedElementComponent extends PureComponent<Props> {
onResize: () => {},
setFilter: () => {},
getFilter: () => '',
onEmbeddableInputChange: () => {},
onEmbeddableDestroyed: () => {},
});
} catch (e) {
// eslint-disable-next-line no-console

View file

@ -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;
}

View file

@ -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<RendererConfig = {}> {

View file

@ -137,7 +137,7 @@ export class CustomizeTimeRangeModal extends Component<CustomizeTimeRangeProps,
onClick={this.inheritFromParent}
color="danger"
data-test-subj="removePerPanelTimeRangeButton"
disabled={this.state.inheritTimeRange}
disabled={!this.props.embeddable.parent || this.state.inheritTimeRange}
flush="left"
>
{i18n.translate(