mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* [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:
parent
5dce5b4acc
commit
6c30f40c7b
31 changed files with 615 additions and 56 deletions
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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 {
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(' ');
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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`,
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -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`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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`,
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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}`;
|
||||
|
|
|
@ -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']
|
||||
);
|
|
@ -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, '')
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
})
|
||||
),
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -69,6 +69,8 @@ export class RenderedElementComponent extends PureComponent<Props> {
|
|||
onResize: () => {},
|
||||
setFilter: () => {},
|
||||
getFilter: () => '',
|
||||
onEmbeddableInputChange: () => {},
|
||||
onEmbeddableDestroyed: () => {},
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = {}> {
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue