mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
React map embeddable (#178158)
Closes https://github.com/elastic/kibana/issues/174960 and https://github.com/elastic/kibana/issues/179677 PR refactors Map embeddable from legacy Embeddable class to react embeddable. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl <nickpeihl@gmail.com>
This commit is contained in:
parent
f016398f8b
commit
3d419ec027
58 changed files with 1834 additions and 1919 deletions
|
@ -27,8 +27,8 @@ export const canLinkLegacyEmbeddable = async (embeddable: CommonLegacyEmbeddable
|
|||
return false;
|
||||
}
|
||||
|
||||
const { maps, visualize } = core.application.capabilities;
|
||||
const canSave = embeddable.type === 'map' ? maps.save : visualize.save;
|
||||
const { visualize } = core.application.capabilities;
|
||||
const canSave = visualize.save;
|
||||
|
||||
const { isOfAggregateQueryType } = await import('@kbn/es-query');
|
||||
const query = isFilterableEmbeddable(embeddable) && embeddable.getQuery();
|
||||
|
|
|
@ -6,29 +6,6 @@
|
|||
*/
|
||||
|
||||
import { savedMap } from './saved_map';
|
||||
import { getQueryFilters } from '../../../common/lib/build_embeddable_filters';
|
||||
import { ExpressionValueFilter } from '../../../types';
|
||||
|
||||
const filterContext: ExpressionValueFilter = {
|
||||
type: 'filter',
|
||||
and: [
|
||||
{
|
||||
type: 'filter',
|
||||
and: [],
|
||||
value: 'filter-value',
|
||||
column: 'filter-column',
|
||||
filterType: 'exactly',
|
||||
},
|
||||
{
|
||||
type: 'filter',
|
||||
and: [],
|
||||
column: 'time-column',
|
||||
filterType: 'time',
|
||||
from: '2019-06-04T04:00:00.000Z',
|
||||
to: '2019-06-05T04:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('savedMap', () => {
|
||||
const fn = savedMap().fn;
|
||||
|
@ -43,13 +20,18 @@ describe('savedMap', () => {
|
|||
it('accepts null context', () => {
|
||||
const expression = fn(null, args, {} as any);
|
||||
|
||||
expect(expression.input.filters).toEqual([]);
|
||||
});
|
||||
|
||||
it('accepts filter context', () => {
|
||||
const expression = fn(filterContext, args, {} as any);
|
||||
const embeddableFilters = getQueryFilters(filterContext.and);
|
||||
|
||||
expect(expression.input.filters).toEqual(embeddableFilters);
|
||||
expect(expression.input).toEqual({
|
||||
hiddenLayers: [],
|
||||
hideFilterActions: true,
|
||||
id: 'some-id',
|
||||
isLayerTOCOpen: false,
|
||||
mapCenter: undefined,
|
||||
savedObjectId: 'some-id',
|
||||
timeRange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
title: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
*/
|
||||
|
||||
import { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import type { MapEmbeddableInput } from '@kbn/maps-plugin/public';
|
||||
import type { MapSerializedState } from '@kbn/maps-plugin/public';
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
import { getQueryFilters } from '../../../common/lib/build_embeddable_filters';
|
||||
import { ExpressionValueFilter, MapCenter, TimeRange as TimeRangeArg } from '../../../types';
|
||||
import {
|
||||
EmbeddableTypes,
|
||||
|
@ -30,7 +29,7 @@ const defaultTimeRange = {
|
|||
to: 'now',
|
||||
};
|
||||
|
||||
type Output = EmbeddableExpression<MapEmbeddableInput & { savedObjectId: string }>;
|
||||
type Output = EmbeddableExpression<MapSerializedState & { id: string }>;
|
||||
|
||||
export function savedMap(): ExpressionFunctionDefinition<
|
||||
'savedMap',
|
||||
|
@ -72,30 +71,19 @@ export function savedMap(): ExpressionFunctionDefinition<
|
|||
},
|
||||
type: EmbeddableExpressionType,
|
||||
fn: (input, args) => {
|
||||
const filters = input ? input.and : [];
|
||||
|
||||
const center = args.center
|
||||
? {
|
||||
lat: args.center.lat,
|
||||
lon: args.center.lon,
|
||||
zoom: args.center.zoom,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: EmbeddableExpressionType,
|
||||
input: {
|
||||
id: args.id,
|
||||
attributes: { title: '' },
|
||||
savedObjectId: args.id,
|
||||
filters: getQueryFilters(filters),
|
||||
timeRange: args.timerange || defaultTimeRange,
|
||||
refreshConfig: {
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
mapCenter: center,
|
||||
mapCenter: args.center
|
||||
? {
|
||||
lat: args.center.lat,
|
||||
lon: args.center.lon,
|
||||
zoom: args.center.zoom,
|
||||
}
|
||||
: undefined,
|
||||
hideFilterActions: true,
|
||||
title: args.title === null ? undefined : args.title,
|
||||
isLayerTOCOpen: false,
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MapEmbeddableInput } from '@kbn/maps-plugin/public';
|
||||
import { MapSerializedState } from '@kbn/maps-plugin/public';
|
||||
|
||||
export function toExpression(input: MapEmbeddableInput & { savedObjectId: string }): string {
|
||||
export function toExpression(input: MapSerializedState & { id: string }): string {
|
||||
const expressionParts = [] as string[];
|
||||
|
||||
expressionParts.push('savedMap');
|
||||
|
|
|
@ -13,7 +13,7 @@ test('Should return original state and empty references with by-reference embedd
|
|||
type: 'map',
|
||||
};
|
||||
|
||||
expect(extract!(mapByReferenceInput)).toEqual({
|
||||
expect(extract(mapByReferenceInput)).toEqual({
|
||||
state: mapByReferenceInput,
|
||||
references: [],
|
||||
});
|
||||
|
@ -29,7 +29,7 @@ test('Should update state with refNames with by-value embeddable state', () => {
|
|||
type: 'map',
|
||||
};
|
||||
|
||||
expect(extract!(mapByValueInput)).toEqual({
|
||||
expect(extract(mapByValueInput)).toEqual({
|
||||
references: [
|
||||
{
|
||||
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
|
|
|
@ -10,7 +10,7 @@ import { MapEmbeddablePersistableState } from './types';
|
|||
import type { MapAttributes } from '../content_management';
|
||||
import { extractReferences } from '../migrations/references';
|
||||
|
||||
export const extract: EmbeddableRegistryDefinition['extract'] = (state) => {
|
||||
export const extract: NonNullable<EmbeddableRegistryDefinition['extract']> = (state) => {
|
||||
const typedState = state as MapEmbeddablePersistableState;
|
||||
|
||||
// by-reference embeddable
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type { MapEmbeddablePersistableState } from './types';
|
||||
export { extract } from './extract';
|
||||
export { inject } from './inject';
|
||||
|
|
|
@ -21,7 +21,7 @@ test('Should return original state with by-reference embeddable state', () => {
|
|||
},
|
||||
];
|
||||
|
||||
expect(inject!(mapByReferenceInput, refernces)).toEqual(mapByReferenceInput);
|
||||
expect(inject(mapByReferenceInput, refernces)).toEqual(mapByReferenceInput);
|
||||
});
|
||||
|
||||
test('Should inject refNames with by-value embeddable state', () => {
|
||||
|
@ -41,7 +41,7 @@ test('Should inject refNames with by-value embeddable state', () => {
|
|||
},
|
||||
];
|
||||
|
||||
expect(inject!(mapByValueInput, refernces)).toEqual({
|
||||
expect(inject(mapByValueInput, refernces)).toEqual({
|
||||
id: '8d62c3f0-c61f-4c09-ac24-9b8ee4320e20',
|
||||
attributes: {
|
||||
layerListJSON: '[{"sourceDescriptor":{"indexPatternId":"changed_index_pattern_id"}}]',
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { MapEmbeddablePersistableState } from './types';
|
|||
import type { MapAttributes } from '../content_management';
|
||||
import { extractReferences, injectReferences } from '../migrations/references';
|
||||
|
||||
export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => {
|
||||
export const inject: NonNullable<EmbeddableRegistryDefinition['inject']> = (state, references) => {
|
||||
const typedState = state as MapEmbeddablePersistableState;
|
||||
|
||||
// by-reference embeddable
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"optionalPlugins": [
|
||||
"cloud",
|
||||
"customIntegrations",
|
||||
"embeddableEnhanced",
|
||||
"home",
|
||||
"savedObjectsTagging",
|
||||
"charts",
|
||||
|
|
|
@ -14,4 +14,4 @@
|
|||
@import 'components/index';
|
||||
@import 'classes/index';
|
||||
@import 'animations';
|
||||
@import 'embeddable/index';
|
||||
@import 'react_embeddable/index';
|
|
@ -19,7 +19,6 @@ import {
|
|||
getLayerListRaw,
|
||||
getMapColors,
|
||||
getMapReady,
|
||||
getMapSettings,
|
||||
getSelectedLayerId,
|
||||
} from '../selectors/map_selectors';
|
||||
import { FLYOUT_STATE } from '../reducers/ui';
|
||||
|
@ -44,12 +43,7 @@ import {
|
|||
UPDATE_LAYER_STYLE,
|
||||
UPDATE_SOURCE_PROP,
|
||||
} from './map_action_constants';
|
||||
import {
|
||||
autoFitToBounds,
|
||||
clearDataRequests,
|
||||
syncDataForLayerId,
|
||||
updateStyleMeta,
|
||||
} from './data_request_actions';
|
||||
import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_request_actions';
|
||||
import {
|
||||
Attribution,
|
||||
JoinDescriptor,
|
||||
|
@ -138,19 +132,10 @@ export function replaceLayerList(newLayerList: LayerDescriptor[]) {
|
|||
};
|
||||
}
|
||||
|
||||
export function updateLayerById(layerDescriptor: LayerDescriptor) {
|
||||
return async (
|
||||
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
|
||||
getState: () => MapStoreState
|
||||
) => {
|
||||
dispatch({
|
||||
type: UPDATE_LAYER,
|
||||
layer: layerDescriptor,
|
||||
});
|
||||
await dispatch(syncDataForLayerId(layerDescriptor.id, false));
|
||||
if (getMapSettings(getState()).autoFitToDataBounds) {
|
||||
dispatch(autoFitToBounds());
|
||||
}
|
||||
export function updateLayerDescriptor(layerDescriptor: LayerDescriptor) {
|
||||
return {
|
||||
type: UPDATE_LAYER,
|
||||
layer: layerDescriptor,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { LayerDescriptor } from '../../common/descriptor_types';
|
|||
import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source';
|
||||
import type { SampleValuesConfig, EMSTermJoinConfig } from '../ems_autosuggest';
|
||||
import type { Props as PassiveMapProps } from '../lens/passive_map';
|
||||
import type { Props as MapProps } from '../embeddable/map_component';
|
||||
import type { Props as MapProps } from '../react_embeddable/map_renderer';
|
||||
|
||||
export interface MapsStartApi {
|
||||
createLayerDescriptors: {
|
||||
|
|
|
@ -42,6 +42,10 @@ export class InvalidLayer extends AbstractLayer {
|
|||
};
|
||||
}
|
||||
|
||||
isLayerLoading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasErrors() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './map_embeddable';
|
||||
export * from './types';
|
||||
export * from './map_embeddable_factory';
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
HasParentApi,
|
||||
HasType,
|
||||
PublishesDataViews,
|
||||
PublishesPanelTitle,
|
||||
PublishesUnifiedSearch,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import {
|
||||
apiIsOfType,
|
||||
apiPublishesUnifiedSearch,
|
||||
apiPublishesPanelTitle,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import type { ILayer } from '../classes/layers/layer';
|
||||
|
||||
export type MapApi = HasType<'map'> & {
|
||||
getLayerList: () => ILayer[];
|
||||
reload: () => void;
|
||||
} & PublishesDataViews &
|
||||
PublishesPanelTitle &
|
||||
PublishesUnifiedSearch &
|
||||
Partial<HasParentApi<unknown>>;
|
||||
|
||||
export const isMapApi = (api: unknown): api is MapApi => {
|
||||
return Boolean(
|
||||
api &&
|
||||
apiIsOfType(api, 'map') &&
|
||||
typeof (api as MapApi).getLayerList === 'function' &&
|
||||
apiPublishesPanelTitle(api) &&
|
||||
apiPublishesUnifiedSearch(api)
|
||||
);
|
||||
};
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component, RefObject } from 'react';
|
||||
import { first } from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { LayerDescriptor, MapCenterAndZoom, MapSettings } from '../../common/descriptor_types';
|
||||
import { MapEmbeddable } from './map_embeddable';
|
||||
import { createBasemapLayerDescriptor } from '../classes/layers/create_basemap_layer_descriptor';
|
||||
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
|
||||
import { MapApi } from './map_api';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
timeRange?: TimeRange;
|
||||
layerList: LayerDescriptor[];
|
||||
mapSettings?: Partial<MapSettings>;
|
||||
hideFilterActions?: boolean;
|
||||
isLayerTOCOpen?: boolean;
|
||||
mapCenter?: MapCenterAndZoom;
|
||||
onInitialRenderComplete?: () => void;
|
||||
getTooltipRenderer?: () => RenderToolTipContent;
|
||||
onApiAvailable?: (api: MapApi) => void;
|
||||
/*
|
||||
* Set to false to exclude sharing attributes 'data-*'.
|
||||
*/
|
||||
isSharable?: boolean;
|
||||
}
|
||||
|
||||
export class MapComponent extends Component<Props> {
|
||||
private _prevLayerList: LayerDescriptor[];
|
||||
private _mapEmbeddable: MapEmbeddable;
|
||||
private readonly _embeddableRef: RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this._prevLayerList = this.props.layerList;
|
||||
this._mapEmbeddable = new MapEmbeddable(
|
||||
{
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
attributes: {
|
||||
title: this.props.title ?? '',
|
||||
layerListJSON: JSON.stringify(this.getLayerList()),
|
||||
},
|
||||
hidePanelTitles: !Boolean(this.props.title),
|
||||
viewMode: ViewMode.VIEW,
|
||||
isLayerTOCOpen:
|
||||
typeof this.props.isLayerTOCOpen === 'boolean' ? this.props.isLayerTOCOpen : false,
|
||||
hideFilterActions:
|
||||
typeof this.props.hideFilterActions === 'boolean' ? this.props.hideFilterActions : false,
|
||||
mapCenter: this.props.mapCenter,
|
||||
mapSettings: this.props.mapSettings ?? {},
|
||||
}
|
||||
);
|
||||
this._mapEmbeddable.updateInput({
|
||||
filters: this.props.filters,
|
||||
query: this.props.query,
|
||||
timeRange: this.props.timeRange,
|
||||
});
|
||||
|
||||
if (this.props.getTooltipRenderer) {
|
||||
this._mapEmbeddable.setRenderTooltipContent(this.props.getTooltipRenderer());
|
||||
}
|
||||
if (this.props.onApiAvailable) {
|
||||
this.props.onApiAvailable(this._mapEmbeddable as MapApi);
|
||||
}
|
||||
|
||||
if (this.props.onInitialRenderComplete) {
|
||||
this._mapEmbeddable
|
||||
.getOnRenderComplete$()
|
||||
.pipe(first())
|
||||
.subscribe(() => {
|
||||
if (this.props.onInitialRenderComplete) {
|
||||
this.props.onInitialRenderComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.isSharable !== undefined) {
|
||||
this._mapEmbeddable.setIsSharable(this.props.isSharable);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this._embeddableRef.current) {
|
||||
this._mapEmbeddable.render(this._embeddableRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mapEmbeddable.destroy();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._mapEmbeddable.updateInput({
|
||||
filters: this.props.filters,
|
||||
query: this.props.query,
|
||||
timeRange: this.props.timeRange,
|
||||
});
|
||||
|
||||
if (this._prevLayerList !== this.props.layerList) {
|
||||
this._mapEmbeddable.setLayerList(this.getLayerList());
|
||||
this._prevLayerList = this.props.layerList;
|
||||
}
|
||||
}
|
||||
|
||||
getLayerList(): LayerDescriptor[] {
|
||||
const basemapLayer = createBasemapLayerDescriptor();
|
||||
return basemapLayer ? [basemapLayer, ...this.props.layerList] : this.props.layerList;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="mapEmbeddableContainer" ref={this._embeddableRef} />;
|
||||
}
|
||||
}
|
|
@ -1,304 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getControlledBy, MapEmbeddable } from './map_embeddable';
|
||||
import { buildExistsFilter, disableFilter, pinFilter, toggleFilterNegated } from '@kbn/es-query';
|
||||
import type { DataViewFieldBase, DataViewBase } from '@kbn/es-query';
|
||||
import { MapEmbeddableConfig, MapEmbeddableInput } from './types';
|
||||
import type { MapAttributes } from '../../common/content_management';
|
||||
|
||||
jest.mock('../kibana_services', () => {
|
||||
return {
|
||||
getExecutionContextService() {
|
||||
return {
|
||||
get: () => {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
},
|
||||
getHttp() {
|
||||
return {
|
||||
basePath: {
|
||||
prepend: (url: string) => url,
|
||||
},
|
||||
};
|
||||
},
|
||||
getMapsCapabilities() {
|
||||
return { save: true };
|
||||
},
|
||||
getSearchService() {
|
||||
return {
|
||||
session: {
|
||||
getSearchOptions() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getShowMapsInspectorAdapter() {
|
||||
return false;
|
||||
},
|
||||
getTimeFilter() {
|
||||
return {
|
||||
getTime() {
|
||||
return { from: 'now-7d', to: 'now' };
|
||||
},
|
||||
};
|
||||
},
|
||||
getEMSSettings() {
|
||||
return {
|
||||
isEMSUrlSet() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../connected_components/map_container', () => {
|
||||
return {
|
||||
MapContainer: () => {
|
||||
return <div>mockLayerTOC</div>;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../routes/map_page', () => {
|
||||
class MockSavedMap {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
private _store = require('../reducers/store').createMapStore();
|
||||
private _attributes: MapAttributes = {
|
||||
title: 'myMap',
|
||||
};
|
||||
|
||||
whenReady = async function () {};
|
||||
|
||||
getStore() {
|
||||
return this._store;
|
||||
}
|
||||
getAttributes() {
|
||||
return this._attributes;
|
||||
}
|
||||
getAutoFitToBounds() {
|
||||
return true;
|
||||
}
|
||||
getSharingSavedObjectProps() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return { SavedMap: MockSavedMap };
|
||||
});
|
||||
|
||||
function untilInitialized(mapEmbeddable: MapEmbeddable): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// @ts-expect-error setInitializationFinished is protected but we are overriding it to know when embeddable is initialized
|
||||
mapEmbeddable.setInitializationFinished = () => {
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function onNextTick(): Promise<void> {
|
||||
// wait one tick to give observables time to fire
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
describe('shouldFetch$', () => {
|
||||
test('should not fetch when search context does not change', async () => {
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: 'map1',
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
title: 'updated map title',
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('on filters change', () => {
|
||||
test('should fetch on filter change', async () => {
|
||||
const existsFilter = buildExistsFilter(
|
||||
{
|
||||
name: 'myFieldName',
|
||||
} as DataViewFieldBase,
|
||||
{
|
||||
id: 'myDataViewId',
|
||||
} as DataViewBase
|
||||
);
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: 'map1',
|
||||
filters: [existsFilter],
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
filters: [toggleFilterNegated(existsFilter)],
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not fetch on disabled filter change', async () => {
|
||||
const disabledFilter = disableFilter(
|
||||
buildExistsFilter(
|
||||
{
|
||||
name: 'myFieldName',
|
||||
} as DataViewFieldBase,
|
||||
{
|
||||
id: 'myDataViewId',
|
||||
} as DataViewBase
|
||||
)
|
||||
);
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: 'map1',
|
||||
filters: [disabledFilter],
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
filters: [toggleFilterNegated(disabledFilter)],
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not fetch when unpinned filter is pinned', async () => {
|
||||
const unpinnedFilter = buildExistsFilter(
|
||||
{
|
||||
name: 'myFieldName',
|
||||
} as DataViewFieldBase,
|
||||
{
|
||||
id: 'myDataViewId',
|
||||
} as DataViewBase
|
||||
);
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: 'map1',
|
||||
filters: [unpinnedFilter],
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
filters: [pinFilter(unpinnedFilter)],
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not fetch on filter controlled by map embeddable change', async () => {
|
||||
const embeddableId = 'map1';
|
||||
const filter = buildExistsFilter(
|
||||
{
|
||||
name: 'myFieldName',
|
||||
} as DataViewFieldBase,
|
||||
{
|
||||
id: 'myDataViewId',
|
||||
} as DataViewBase
|
||||
);
|
||||
const controlledByFilter = {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
controlledBy: getControlledBy(embeddableId),
|
||||
},
|
||||
};
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: embeddableId,
|
||||
filters: [controlledByFilter],
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
filters: [toggleFilterNegated(controlledByFilter)],
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on searchSessionId change', () => {
|
||||
test('should fetch when filterByMapExtent is false', async () => {
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: 'map1',
|
||||
filterByMapExtent: false,
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
searchSessionId: uuidv4(),
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not fetch when filterByMapExtent is true', async () => {
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{} as unknown as MapEmbeddableConfig,
|
||||
{
|
||||
id: 'map1',
|
||||
filterByMapExtent: true,
|
||||
} as unknown as MapEmbeddableInput
|
||||
);
|
||||
await untilInitialized(mapEmbeddable);
|
||||
|
||||
const fetchSpy = jest.spyOn(mapEmbeddable, '_dispatchSetQuery');
|
||||
|
||||
mapEmbeddable.updateInput({
|
||||
searchSessionId: uuidv4(),
|
||||
});
|
||||
|
||||
await onNextTick();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,878 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter as filterOperator,
|
||||
map,
|
||||
skip,
|
||||
startWith,
|
||||
} from 'rxjs';
|
||||
import { Unsubscribe } from 'redux';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { Query, type Filter } from '@kbn/es-query';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import {
|
||||
Embeddable,
|
||||
IContainer,
|
||||
ReferenceOrValueEmbeddable,
|
||||
genericEmbeddableInputIsEqual,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
omitGenericEmbeddableInput,
|
||||
FilterableEmbeddable,
|
||||
shouldFetch$,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '@kbn/unified-search-plugin/public';
|
||||
import { createExtentFilter } from '../../common/elasticsearch_util';
|
||||
import {
|
||||
replaceLayerList,
|
||||
setMapSettings,
|
||||
setQuery,
|
||||
setReadOnly,
|
||||
updateLayerById,
|
||||
setGotoWithCenter,
|
||||
setEmbeddableSearchContext,
|
||||
setExecutionContext,
|
||||
} from '../actions';
|
||||
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors';
|
||||
import {
|
||||
getInspectorAdapters,
|
||||
setChartsPaletteServiceGetColor,
|
||||
setEventHandlers,
|
||||
setOnMapMove,
|
||||
EventHandlers,
|
||||
} from '../reducers/non_serializable_instances';
|
||||
import {
|
||||
isMapLoading,
|
||||
getGeoFieldNames,
|
||||
getEmbeddableSearchContext,
|
||||
getLayerList,
|
||||
getGoto,
|
||||
getMapCenter,
|
||||
getMapBuffer,
|
||||
getMapExtent,
|
||||
getMapReady,
|
||||
getMapSettings,
|
||||
getMapZoom,
|
||||
getHiddenLayerIds,
|
||||
getQueryableUniqueIndexPatternIds,
|
||||
} from '../selectors/map_selectors';
|
||||
import {
|
||||
APP_ID,
|
||||
getEditPath,
|
||||
getFullPath,
|
||||
MAP_EMBEDDABLE_NAME,
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
RawValue,
|
||||
RENDER_TIMEOUT,
|
||||
} from '../../common/constants';
|
||||
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
|
||||
import {
|
||||
getAnalytics,
|
||||
getCharts,
|
||||
getCoreI18n,
|
||||
getCoreOverlays,
|
||||
getExecutionContextService,
|
||||
getHttp,
|
||||
getSearchService,
|
||||
getSpacesApi,
|
||||
getTheme,
|
||||
getUiActions,
|
||||
} from '../kibana_services';
|
||||
import { LayerDescriptor, MapExtent } from '../../common/descriptor_types';
|
||||
import { extractReferences } from '../../common/migrations/references';
|
||||
import { MapContainer } from '../connected_components/map_container';
|
||||
import { SavedMap } from '../routes/map_page';
|
||||
import { getIndexPatternsFromIds } from '../index_pattern_util';
|
||||
import { getMapAttributeService } from '../map_attribute_service';
|
||||
import { isUrlDrilldown, toValueClickDataFormat } from '../trigger_actions/trigger_utils';
|
||||
import { waitUntilTimeLayersLoad$ } from '../routes/map_page/map_app/wait_until_time_layers_load';
|
||||
import { mapEmbeddablesSingleton } from './map_embeddables_singleton';
|
||||
import { getGeoFieldsLabel } from './get_geo_fields_label';
|
||||
import { checkForDuplicateTitle, getMapClient } from '../content_management';
|
||||
|
||||
import {
|
||||
MapByValueInput,
|
||||
MapByReferenceInput,
|
||||
MapEmbeddableConfig,
|
||||
MapEmbeddableInput,
|
||||
MapEmbeddableOutput,
|
||||
} from './types';
|
||||
|
||||
async function getChartsPaletteServiceGetColor(): Promise<((value: string) => string) | null> {
|
||||
const chartsService = getCharts();
|
||||
const paletteRegistry: PaletteRegistry | null = chartsService
|
||||
? await chartsService.palettes.getPalettes()
|
||||
: null;
|
||||
if (!paletteRegistry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const paletteDefinition = paletteRegistry.get('default');
|
||||
const chartConfiguration = { syncColors: true };
|
||||
return (value: string) => {
|
||||
const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }];
|
||||
const color = paletteDefinition.getCategoricalColor(series, chartConfiguration);
|
||||
return color ? color : '#3d3d3d';
|
||||
};
|
||||
}
|
||||
|
||||
function getIsRestore(searchSessionId?: string) {
|
||||
if (!searchSessionId) {
|
||||
return false;
|
||||
}
|
||||
const searchSessionOptions = getSearchService().session.getSearchOptions(searchSessionId);
|
||||
return searchSessionOptions ? searchSessionOptions.isRestore : false;
|
||||
}
|
||||
|
||||
export function getControlledBy(id: string) {
|
||||
return `mapEmbeddablePanel${id}`;
|
||||
}
|
||||
|
||||
export class MapEmbeddable
|
||||
extends Embeddable<MapEmbeddableInput, MapEmbeddableOutput>
|
||||
implements ReferenceOrValueEmbeddable<MapByValueInput, MapByReferenceInput>, FilterableEmbeddable
|
||||
{
|
||||
type = MAP_SAVED_OBJECT_TYPE;
|
||||
deferEmbeddableLoad = true;
|
||||
|
||||
private _isActive: boolean;
|
||||
private _savedMap: SavedMap;
|
||||
private _renderTooltipContent?: RenderToolTipContent;
|
||||
private _subscriptions: Subscription[] = [];
|
||||
private _prevIsRestore: boolean = false;
|
||||
private _prevMapExtent?: MapExtent;
|
||||
private _prevSyncColors?: boolean;
|
||||
private _domNode?: HTMLElement;
|
||||
private _unsubscribeFromStore?: Unsubscribe;
|
||||
private _isInitialized = false;
|
||||
private _controlledBy: string;
|
||||
private _isSharable = true;
|
||||
private readonly _onRenderComplete$;
|
||||
|
||||
constructor(config: MapEmbeddableConfig, initialInput: MapEmbeddableInput, parent?: IContainer) {
|
||||
super(
|
||||
initialInput,
|
||||
{
|
||||
editApp: APP_ID,
|
||||
editable: config.editable,
|
||||
indexPatterns: [],
|
||||
},
|
||||
parent
|
||||
);
|
||||
|
||||
this._isActive = true;
|
||||
this._savedMap = new SavedMap({ mapEmbeddableInput: initialInput });
|
||||
this._initializeSaveMap();
|
||||
this._subscriptions.push(this.getUpdated$().subscribe(() => this.onUpdate()));
|
||||
this._controlledBy = getControlledBy(this.id);
|
||||
|
||||
this._onRenderComplete$ = this.getOutput$().pipe(
|
||||
// wrapping distinctUntilChanged with startWith and skip to prime distinctUntilChanged with an initial value.
|
||||
startWith(this.getOutput()),
|
||||
distinctUntilChanged((a, b) => a.loading === b.loading),
|
||||
skip(1),
|
||||
debounceTime(RENDER_TIMEOUT),
|
||||
filterOperator((output) => !output.loading),
|
||||
map(() => {
|
||||
// Observable notifies subscriber when rendering is complete
|
||||
// Return void to not expose internal implemenation details of observabale
|
||||
return;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getOnRenderComplete$() {
|
||||
return this._onRenderComplete$;
|
||||
}
|
||||
|
||||
public reportsEmbeddableLoad() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _initializeSaveMap() {
|
||||
try {
|
||||
await this._savedMap.whenReady();
|
||||
} catch (e) {
|
||||
this.onFatalError(e);
|
||||
return;
|
||||
}
|
||||
this._initializeStore();
|
||||
try {
|
||||
await this._initializeOutput();
|
||||
} catch (e) {
|
||||
this.onFatalError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this._savedMap.getStore().dispatch(setExecutionContext(this.getExecutionContext()));
|
||||
|
||||
// deferred loading of this embeddable is complete
|
||||
this.setInitializationFinished();
|
||||
|
||||
this._isInitialized = true;
|
||||
if (this._domNode) {
|
||||
this.render(this._domNode);
|
||||
}
|
||||
}
|
||||
|
||||
private getExecutionContext() {
|
||||
const parentContext = getExecutionContextService().get();
|
||||
const mapContext: KibanaExecutionContext = {
|
||||
type: APP_ID,
|
||||
name: APP_ID,
|
||||
id: this.id,
|
||||
url: this.output.editPath,
|
||||
};
|
||||
|
||||
return parentContext
|
||||
? {
|
||||
...parentContext,
|
||||
child: mapContext,
|
||||
}
|
||||
: mapContext;
|
||||
}
|
||||
|
||||
private _initializeStore() {
|
||||
this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors);
|
||||
|
||||
const store = this._savedMap.getStore();
|
||||
|
||||
store.dispatch(setReadOnly(true));
|
||||
store.dispatch(
|
||||
setMapSettings({
|
||||
keydownScrollZoom: true,
|
||||
showTimesliderToggleButton: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Passing callback into redux store instead of regular pattern of getting redux state changes for performance reasons
|
||||
store.dispatch(setOnMapMove(this._propogateMapMovement));
|
||||
|
||||
this._dispatchSetQuery({ forceRefresh: false });
|
||||
this._subscriptions.push(
|
||||
shouldFetch$<MapEmbeddableInput>(this.getUpdated$(), () => {
|
||||
return {
|
||||
...this.getInput(),
|
||||
filters: this._getInputFilters(),
|
||||
searchSessionId: this._getSearchSessionId(),
|
||||
};
|
||||
}).subscribe(() => {
|
||||
this._dispatchSetQuery({
|
||||
forceRefresh: false,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const mapStateJSON = this._savedMap.getAttributes().mapStateJSON;
|
||||
if (mapStateJSON) {
|
||||
try {
|
||||
const mapState = JSON.parse(mapStateJSON);
|
||||
store.dispatch(
|
||||
setEmbeddableSearchContext({
|
||||
filters: mapState.filters ? mapState.filters : [],
|
||||
query: mapState.query,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults
|
||||
}
|
||||
}
|
||||
|
||||
this._unsubscribeFromStore = store.subscribe(() => {
|
||||
this._handleStoreChanges();
|
||||
});
|
||||
}
|
||||
|
||||
private async _initializeOutput() {
|
||||
const { title: savedMapTitle, description: savedMapDescription } =
|
||||
this._savedMap.getAttributes();
|
||||
const input = this.getInput();
|
||||
const title = input.hidePanelTitles ? '' : input.title ?? savedMapTitle;
|
||||
const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined;
|
||||
this.updateOutput({
|
||||
defaultTitle: savedMapTitle,
|
||||
defaultDescription: savedMapDescription,
|
||||
title,
|
||||
editPath: getEditPath(savedObjectId),
|
||||
editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)),
|
||||
indexPatterns: await this._getIndexPatterns(),
|
||||
});
|
||||
}
|
||||
|
||||
public inputIsRefType(
|
||||
input: MapByValueInput | MapByReferenceInput
|
||||
): input is MapByReferenceInput {
|
||||
return getMapAttributeService().inputIsRefType(input);
|
||||
}
|
||||
|
||||
public async getInputAsRefType(): Promise<MapByReferenceInput> {
|
||||
return getMapAttributeService().getInputAsRefType(this.getExplicitInput(), {
|
||||
showSaveModal: true,
|
||||
saveModalTitle: this.getTitle(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getExplicitInputIsEqual(
|
||||
lastExplicitInput: Partial<MapByValueInput | MapByReferenceInput>
|
||||
): Promise<boolean> {
|
||||
const currentExplicitInput = this.getExplicitInput();
|
||||
if (!genericEmbeddableInputIsEqual(lastExplicitInput, currentExplicitInput)) return false;
|
||||
|
||||
// generic embeddable input is equal, now we compare map specific input elements, ignoring 'mapBuffer'.
|
||||
const lastMapInput = omitGenericEmbeddableInput(_.omit(lastExplicitInput, 'mapBuffer'));
|
||||
const currentMapInput = omitGenericEmbeddableInput(_.omit(currentExplicitInput, 'mapBuffer'));
|
||||
return fastIsEqual(lastMapInput, currentMapInput);
|
||||
}
|
||||
|
||||
public async getInputAsValueType(): Promise<MapByValueInput> {
|
||||
return getMapAttributeService().getInputAsValueType(this.getExplicitInput());
|
||||
}
|
||||
|
||||
public getLayerList() {
|
||||
return getLayerList(this._savedMap.getStore().getState());
|
||||
}
|
||||
|
||||
public getFilters() {
|
||||
const embeddableSearchContext = getEmbeddableSearchContext(
|
||||
this._savedMap.getStore().getState()
|
||||
);
|
||||
return embeddableSearchContext ? embeddableSearchContext.filters : [];
|
||||
}
|
||||
|
||||
public getQuery(): Query | undefined {
|
||||
const embeddableSearchContext = getEmbeddableSearchContext(
|
||||
this._savedMap.getStore().getState()
|
||||
);
|
||||
return embeddableSearchContext?.query;
|
||||
}
|
||||
|
||||
public supportedTriggers(): string[] {
|
||||
return [APPLY_FILTER_TRIGGER, VALUE_CLICK_TRIGGER];
|
||||
}
|
||||
|
||||
setRenderTooltipContent = (renderTooltipContent: RenderToolTipContent) => {
|
||||
this._renderTooltipContent = renderTooltipContent;
|
||||
};
|
||||
|
||||
setEventHandlers = (eventHandlers: EventHandlers) => {
|
||||
this._savedMap.getStore().dispatch(setEventHandlers(eventHandlers));
|
||||
};
|
||||
|
||||
/*
|
||||
* Set to false to exclude sharing attributes 'data-*'.
|
||||
*/
|
||||
public setIsSharable(isSharable: boolean): void {
|
||||
this._isSharable = isSharable;
|
||||
}
|
||||
|
||||
getInspectorAdapters() {
|
||||
return getInspectorAdapters(this._savedMap.getStore().getState());
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
if (this.input.syncColors !== this._prevSyncColors) {
|
||||
this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors);
|
||||
}
|
||||
|
||||
const isRestore = getIsRestore(this._getSearchSessionId());
|
||||
if (isRestore !== this._prevIsRestore) {
|
||||
this._prevIsRestore = isRestore;
|
||||
this._savedMap.getStore().dispatch(
|
||||
setMapSettings({
|
||||
disableInteractive: isRestore,
|
||||
hideToolbarOverlay: isRestore,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_getIsMovementSynchronized = () => {
|
||||
return this.input.isMovementSynchronized === undefined
|
||||
? true
|
||||
: this.input.isMovementSynchronized;
|
||||
};
|
||||
|
||||
_getIsFilterByMapExtent = () => {
|
||||
return this.input.filterByMapExtent === undefined ? false : this.input.filterByMapExtent;
|
||||
};
|
||||
|
||||
_gotoSynchronizedLocation() {
|
||||
const syncedLocation = mapEmbeddablesSingleton.getLocation();
|
||||
if (syncedLocation) {
|
||||
// set map to synchronized view
|
||||
this._mapSyncHandler(syncedLocation.lat, syncedLocation.lon, syncedLocation.zoom);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getMapReady(this._savedMap.getStore().getState())) {
|
||||
// Initialize synchronized view to map's goto
|
||||
// Use goto because un-rendered map will not have accurate mapCenter and mapZoom.
|
||||
const goto = getGoto(this._savedMap.getStore().getState());
|
||||
if (goto && goto.center) {
|
||||
mapEmbeddablesSingleton.setLocation(
|
||||
this.input.id,
|
||||
goto.center.lat,
|
||||
goto.center.lon,
|
||||
goto.center.zoom
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize synchronized view to map's view
|
||||
const center = getMapCenter(this._savedMap.getStore().getState());
|
||||
const zoom = getMapZoom(this._savedMap.getStore().getState());
|
||||
mapEmbeddablesSingleton.setLocation(this.input.id, center.lat, center.lon, zoom);
|
||||
}
|
||||
|
||||
_propogateMapMovement = (lat: number, lon: number, zoom: number) => {
|
||||
if (this._getIsMovementSynchronized()) {
|
||||
mapEmbeddablesSingleton.setLocation(this.input.id, lat, lon, zoom);
|
||||
}
|
||||
};
|
||||
|
||||
_getInputFilters() {
|
||||
return this.input.filters
|
||||
? this.input.filters.filter(
|
||||
(filter) => !filter.meta.disabled && filter.meta.controlledBy !== this._controlledBy
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
_getSearchSessionId() {
|
||||
// New search session id causes all layers from elasticsearch to refetch data.
|
||||
// Dashboard provides a new search session id anytime filters change.
|
||||
// Thus, filtering embeddable container by map extent causes a new search session id any time the map is moved.
|
||||
// Disabling search session when filtering embeddable container by map extent.
|
||||
// The use case for search sessions (restoring results because of slow responses) does not match the use case of
|
||||
// filtering by map extent (rapid responses as users explore their map).
|
||||
return this.input.filterByMapExtent ? undefined : this.input.searchSessionId;
|
||||
}
|
||||
|
||||
_dispatchSetQuery({ forceRefresh }: { forceRefresh: boolean }) {
|
||||
this._savedMap.getStore().dispatch<any>(
|
||||
setQuery({
|
||||
filters: this._getInputFilters(),
|
||||
query: this.input.query,
|
||||
timeFilters: this.input.timeRange,
|
||||
timeslice: this.input.timeslice
|
||||
? { from: this.input.timeslice[0], to: this.input.timeslice[1] }
|
||||
: undefined,
|
||||
clearTimeslice: this.input.timeslice === undefined,
|
||||
forceRefresh,
|
||||
searchSessionId: this._getSearchSessionId(),
|
||||
searchSessionMapBuffer: getIsRestore(this._getSearchSessionId())
|
||||
? this.input.mapBuffer
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async _dispatchSetChartsPaletteServiceGetColor(syncColors?: boolean) {
|
||||
this._prevSyncColors = syncColors;
|
||||
const chartsPaletteServiceGetColor = syncColors
|
||||
? await getChartsPaletteServiceGetColor()
|
||||
: null;
|
||||
if (syncColors !== this._prevSyncColors) {
|
||||
return;
|
||||
}
|
||||
this._savedMap
|
||||
.getStore()
|
||||
.dispatch(setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} domNode
|
||||
* @param {ContainerState} containerState
|
||||
*/
|
||||
render(domNode: HTMLElement) {
|
||||
this._domNode = domNode;
|
||||
if (!this._isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapEmbeddablesSingleton.register(this.input.id, {
|
||||
getTitle: () => {
|
||||
const output = this.getOutput();
|
||||
if (output.title) {
|
||||
return output.title;
|
||||
}
|
||||
|
||||
if (output.defaultTitle) {
|
||||
return output.defaultTitle;
|
||||
}
|
||||
|
||||
return this.input.id;
|
||||
},
|
||||
onLocationChange: this._mapSyncHandler,
|
||||
getIsMovementSynchronized: this._getIsMovementSynchronized,
|
||||
setIsMovementSynchronized: (isMovementSynchronized: boolean) => {
|
||||
this.updateInput({ isMovementSynchronized });
|
||||
if (isMovementSynchronized) {
|
||||
this._gotoSynchronizedLocation();
|
||||
} else if (!isMovementSynchronized && this._savedMap.getAutoFitToBounds()) {
|
||||
// restore autoFitToBounds when isMovementSynchronized disabled
|
||||
this._savedMap.getStore().dispatch(setMapSettings({ autoFitToDataBounds: true }));
|
||||
}
|
||||
},
|
||||
getIsFilterByMapExtent: this._getIsFilterByMapExtent,
|
||||
setIsFilterByMapExtent: (isFilterByMapExtent: boolean) => {
|
||||
this.updateInput({ filterByMapExtent: isFilterByMapExtent });
|
||||
if (isFilterByMapExtent) {
|
||||
this._setMapExtentFilter();
|
||||
} else {
|
||||
this._clearMapExtentFilter();
|
||||
}
|
||||
},
|
||||
getGeoFieldNames: () => {
|
||||
return getGeoFieldNames(this._savedMap.getStore().getState());
|
||||
},
|
||||
});
|
||||
if (this._getIsMovementSynchronized()) {
|
||||
this._gotoSynchronizedLocation();
|
||||
}
|
||||
|
||||
const sharingSavedObjectProps = this._savedMap.getSharingSavedObjectProps();
|
||||
const spaces = getSpacesApi();
|
||||
const content =
|
||||
sharingSavedObjectProps && spaces && sharingSavedObjectProps?.outcome === 'conflict' ? (
|
||||
<div className="mapEmbeddedError">
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
iconColor="danger"
|
||||
data-test-subj="embeddable-maps-failure"
|
||||
body={spaces.ui.components.getEmbeddableLegacyUrlConflict({
|
||||
targetType: MAP_SAVED_OBJECT_TYPE,
|
||||
sourceId: sharingSavedObjectProps.sourceId!,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<MapContainer
|
||||
onSingleValueTrigger={this.onSingleValueTrigger}
|
||||
addFilters={
|
||||
this.input.hideFilterActions || this.input.disableTriggers ? null : this.addFilters
|
||||
}
|
||||
getFilterActions={this.getFilterActions}
|
||||
getActionContext={this.getActionContext}
|
||||
renderTooltipContent={this._renderTooltipContent}
|
||||
title={this.getTitle()}
|
||||
description={this.getDescription()}
|
||||
waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this._savedMap.getStore())}
|
||||
isSharable={this._isSharable}
|
||||
/>
|
||||
);
|
||||
|
||||
render(
|
||||
<KibanaRenderContextProvider
|
||||
analytics={getAnalytics()}
|
||||
i18n={getCoreI18n()}
|
||||
theme={getTheme()}
|
||||
>
|
||||
<Provider store={this._savedMap.getStore()}>{content}</Provider>
|
||||
</KibanaRenderContextProvider>,
|
||||
this._domNode
|
||||
);
|
||||
}
|
||||
|
||||
setLayerList(layerList: LayerDescriptor[]) {
|
||||
this._savedMap.getStore().dispatch<any>(replaceLayerList(layerList));
|
||||
this._getIndexPatterns().then((indexPatterns) => {
|
||||
this.updateOutput({
|
||||
indexPatterns,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateLayerById(layerDescriptor: LayerDescriptor) {
|
||||
this._savedMap.getStore().dispatch<any>(updateLayerById(layerDescriptor));
|
||||
}
|
||||
|
||||
private async _getIndexPatterns() {
|
||||
const queryableIndexPatternIds = getQueryableUniqueIndexPatternIds(
|
||||
this._savedMap.getStore().getState()
|
||||
);
|
||||
return await getIndexPatternsFromIds(queryableIndexPatternIds);
|
||||
}
|
||||
|
||||
onSingleValueTrigger = (actionId: string, key: string, value: RawValue) => {
|
||||
const action = getUiActions().getAction(actionId);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply action, could not locate action');
|
||||
}
|
||||
const executeContext = {
|
||||
...this.getActionContext(),
|
||||
data: {
|
||||
data: toValueClickDataFormat(key, value),
|
||||
},
|
||||
};
|
||||
action.execute(executeContext);
|
||||
};
|
||||
|
||||
addFilters = async (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => {
|
||||
const executeContext = {
|
||||
...this.getActionContext(),
|
||||
filters,
|
||||
};
|
||||
const action = getUiActions().getAction(actionId);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
};
|
||||
|
||||
getFilterActions = async () => {
|
||||
const filterActions = await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, {
|
||||
embeddable: this,
|
||||
filters: [],
|
||||
});
|
||||
const valueClickActions = await getUiActions().getTriggerCompatibleActions(
|
||||
VALUE_CLICK_TRIGGER,
|
||||
{
|
||||
embeddable: this,
|
||||
data: {
|
||||
// uiActions.getTriggerCompatibleActions validates action with provided context
|
||||
// so if event.key and event.value are used in the URL template but can not be parsed from context
|
||||
// then the action is filtered out.
|
||||
// To prevent filtering out actions, provide dummy context when initially fetching actions.
|
||||
data: toValueClickDataFormat('anyfield', 'anyvalue'),
|
||||
},
|
||||
}
|
||||
);
|
||||
return [...filterActions, ...valueClickActions.filter(isUrlDrilldown)];
|
||||
};
|
||||
|
||||
getActionContext = () => {
|
||||
const trigger = getUiActions().getTrigger(APPLY_FILTER_TRIGGER);
|
||||
if (!trigger) {
|
||||
throw new Error('Unable to get context, could not locate trigger');
|
||||
}
|
||||
return {
|
||||
embeddable: this,
|
||||
trigger,
|
||||
} as ActionExecutionContext;
|
||||
};
|
||||
|
||||
// remove legacy library tranform methods
|
||||
linkToLibrary = undefined;
|
||||
unlinkFromLibrary = undefined;
|
||||
// add implemenation for library transform methods
|
||||
checkForDuplicateTitle = async (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => {
|
||||
await checkForDuplicateTitle(
|
||||
{
|
||||
title: newTitle,
|
||||
copyOnSave: false,
|
||||
lastSavedTitle: '',
|
||||
isTitleDuplicateConfirmed,
|
||||
getDisplayName: () => MAP_EMBEDDABLE_NAME,
|
||||
onTitleDuplicate,
|
||||
},
|
||||
{
|
||||
overlays: getCoreOverlays(),
|
||||
}
|
||||
);
|
||||
};
|
||||
saveToLibrary = async (title: string) => {
|
||||
const { attributes, references } = extractReferences({
|
||||
attributes: this._savedMap.getAttributes(),
|
||||
});
|
||||
|
||||
const {
|
||||
item: { id: savedObjectId },
|
||||
} = await getMapClient().create({
|
||||
data: {
|
||||
...attributes,
|
||||
title,
|
||||
},
|
||||
options: { references },
|
||||
});
|
||||
return savedObjectId;
|
||||
};
|
||||
getByReferenceState = (libraryId: string) => {
|
||||
return {
|
||||
..._.omit(this.getExplicitInput(), 'attributes'),
|
||||
savedObjectId: libraryId,
|
||||
};
|
||||
};
|
||||
getByValueState = () => {
|
||||
return {
|
||||
..._.omit(this.getExplicitInput(), 'savedObjectId'),
|
||||
attributes: this._savedMap.getAttributes(),
|
||||
};
|
||||
};
|
||||
|
||||
// Timing bug for dashboard with multiple maps with synchronized movement and filter by map extent enabled
|
||||
// When moving map with filterByMapExtent:false, previous map extent filter(s) does not get removed
|
||||
// Cuased by syncDashboardContainerInput applyContainerChangesToState.
|
||||
// 1) _setMapExtentFilter executes ACTION_GLOBAL_APPLY_FILTER action,
|
||||
// removing previous map extent filter and adding new map extent filter
|
||||
// 2) applyContainerChangesToState then re-adds stale input.filters (which contains previous map extent filter)
|
||||
// Add debounce to fix timing issue.
|
||||
// 1) applyContainerChangesToState now runs first and does its thing
|
||||
// 2) _setMapExtentFilter executes ACTION_GLOBAL_APPLY_FILTER action,
|
||||
// removing previous map extent filter and adding new map extent filter
|
||||
_setMapExtentFilter = _.debounce(() => {
|
||||
const mapExtent = getMapExtent(this._savedMap.getStore().getState());
|
||||
const geoFieldNames = mapEmbeddablesSingleton.getGeoFieldNames();
|
||||
|
||||
if (mapExtent === undefined || geoFieldNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._prevMapExtent = mapExtent;
|
||||
|
||||
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
|
||||
mapExtentFilter.meta.controlledBy = this._controlledBy;
|
||||
mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
|
||||
defaultMessage: '{geoFieldsLabel} within map bounds',
|
||||
values: { geoFieldsLabel: getGeoFieldsLabel(geoFieldNames) },
|
||||
});
|
||||
|
||||
const executeContext = {
|
||||
...this.getActionContext(),
|
||||
filters: [mapExtentFilter],
|
||||
controlledBy: this._controlledBy,
|
||||
};
|
||||
const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply map extent filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
}, 100);
|
||||
|
||||
_clearMapExtentFilter() {
|
||||
this._prevMapExtent = undefined;
|
||||
const executeContext = {
|
||||
...this.getActionContext(),
|
||||
filters: [],
|
||||
controlledBy: this._controlledBy,
|
||||
};
|
||||
const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply map extent filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
mapEmbeddablesSingleton.unregister(this.input.id);
|
||||
this._isActive = false;
|
||||
if (this._unsubscribeFromStore) {
|
||||
this._unsubscribeFromStore();
|
||||
}
|
||||
|
||||
if (this._domNode) {
|
||||
unmountComponentAtNode(this._domNode);
|
||||
}
|
||||
|
||||
this._subscriptions.forEach((subscription) => {
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
this._dispatchSetQuery({
|
||||
forceRefresh: true,
|
||||
});
|
||||
}
|
||||
|
||||
_mapSyncHandler = (lat: number, lon: number, zoom: number) => {
|
||||
// auto fit to bounds is not compatable with map synchronization
|
||||
// auto fit to bounds may cause map location to never stablize and bound back and forth between bounds on different maps
|
||||
if (getMapSettings(this._savedMap.getStore().getState()).autoFitToDataBounds) {
|
||||
this._savedMap.getStore().dispatch(setMapSettings({ autoFitToDataBounds: false }));
|
||||
}
|
||||
this._savedMap.getStore().dispatch(setGotoWithCenter({ lat, lon, zoom }));
|
||||
};
|
||||
|
||||
_handleStoreChanges() {
|
||||
if (!this._isActive || !getMapReady(this._savedMap.getStore().getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapExtent = getMapExtent(this._savedMap.getStore().getState());
|
||||
if (this._getIsFilterByMapExtent() && !_.isEqual(this._prevMapExtent, mapExtent)) {
|
||||
this._setMapExtentFilter();
|
||||
}
|
||||
|
||||
const center = getMapCenter(this._savedMap.getStore().getState());
|
||||
const zoom = getMapZoom(this._savedMap.getStore().getState());
|
||||
|
||||
const mapCenter = this.input.mapCenter || undefined;
|
||||
if (
|
||||
!mapCenter ||
|
||||
mapCenter.lat !== center.lat ||
|
||||
mapCenter.lon !== center.lon ||
|
||||
mapCenter.zoom !== zoom
|
||||
) {
|
||||
this.updateInput({
|
||||
mapCenter: {
|
||||
lat: center.lat,
|
||||
lon: center.lon,
|
||||
zoom,
|
||||
},
|
||||
mapBuffer: getMapBuffer(this._savedMap.getStore().getState()),
|
||||
});
|
||||
}
|
||||
|
||||
const isLayerTOCOpen = getIsLayerTOCOpen(this._savedMap.getStore().getState());
|
||||
if (this.input.isLayerTOCOpen !== isLayerTOCOpen) {
|
||||
this.updateInput({
|
||||
isLayerTOCOpen,
|
||||
});
|
||||
}
|
||||
|
||||
const openTOCDetails = getOpenTOCDetails(this._savedMap.getStore().getState());
|
||||
if (!_.isEqual(this.input.openTOCDetails, openTOCDetails)) {
|
||||
this.updateInput({
|
||||
openTOCDetails,
|
||||
});
|
||||
}
|
||||
|
||||
const hiddenLayerIds = getHiddenLayerIds(this._savedMap.getStore().getState());
|
||||
if (!_.isEqual(this.input.hiddenLayers, hiddenLayerIds)) {
|
||||
this.updateInput({
|
||||
hiddenLayers: hiddenLayerIds,
|
||||
});
|
||||
}
|
||||
|
||||
const isLoading = isMapLoading(this._savedMap.getStore().getState());
|
||||
if (this.getOutput().loading !== isLoading) {
|
||||
/**
|
||||
* Maps emit rendered when the data is loaded, as we don't have feedback from the maps rendering library atm.
|
||||
* This means that the DASHBOARD_LOADED_EVENT event might be fired while a map is still rendering in some cases.
|
||||
* For more details please contact the maps team.
|
||||
*/
|
||||
this.updateOutput({
|
||||
loading: isLoading,
|
||||
rendered: !isLoading,
|
||||
// do not surface layer errors as output.error
|
||||
// output.error blocks entire embeddable display and prevents map from displaying
|
||||
// layer errors are better surfaced in legend while still keeping the map usable
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { first } from 'rxjs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { MAP_SAVED_OBJECT_TYPE, APP_ICON, MAP_EMBEDDABLE_NAME } from '../../common/constants';
|
||||
import { extract, inject } from '../../common/embeddable';
|
||||
import { MapByReferenceInput, MapEmbeddableInput } from './types';
|
||||
import { getApplication, getMapsCapabilities, getUsageCollection } from '../kibana_services';
|
||||
|
||||
export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
|
||||
type = MAP_SAVED_OBJECT_TYPE;
|
||||
savedObjectMetaData = {
|
||||
name: i18n.translate('xpack.maps.mapSavedObjectLabel', {
|
||||
defaultMessage: 'Map',
|
||||
}),
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
getIconForSavedObject: () => APP_ICON,
|
||||
};
|
||||
|
||||
async isEditable() {
|
||||
return getMapsCapabilities().save as boolean;
|
||||
}
|
||||
|
||||
// Not supported yet for maps types.
|
||||
canCreateNew() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
return MAP_EMBEDDABLE_NAME;
|
||||
}
|
||||
|
||||
createFromSavedObject = async (
|
||||
savedObjectId: string,
|
||||
input: MapEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) => {
|
||||
if (!(input as MapByReferenceInput).savedObjectId) {
|
||||
(input as MapByReferenceInput).savedObjectId = savedObjectId;
|
||||
}
|
||||
return this.create(input, parent);
|
||||
};
|
||||
|
||||
create = async (input: MapEmbeddableInput, parent?: IContainer) => {
|
||||
const { MapEmbeddable } = await import('./map_embeddable');
|
||||
const usageCollection = getUsageCollection();
|
||||
if (usageCollection) {
|
||||
// currentAppId$ is a BehaviorSubject exposed as an observable so subscription gets last value upon subscribe
|
||||
getApplication()
|
||||
.currentAppId$.pipe(first())
|
||||
.subscribe((appId) => {
|
||||
if (appId) usageCollection.reportUiCounter('map', 'loaded', `open_maps_vis_${appId}`);
|
||||
});
|
||||
}
|
||||
return new MapEmbeddable(
|
||||
{
|
||||
editable: await this.isEditable(),
|
||||
},
|
||||
input,
|
||||
parent
|
||||
);
|
||||
};
|
||||
|
||||
inject = inject;
|
||||
|
||||
extract = extract;
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import {
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
SavedObjectEmbeddableInput,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { MapCenterAndZoom, MapExtent, MapSettings } from '../../common/descriptor_types';
|
||||
import type { MapAttributes } from '../../common/content_management';
|
||||
|
||||
export interface MapEmbeddableConfig {
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
interface MapEmbeddableState {
|
||||
isLayerTOCOpen?: boolean;
|
||||
openTOCDetails?: string[];
|
||||
mapCenter?: MapCenterAndZoom;
|
||||
mapBuffer?: MapExtent;
|
||||
mapSettings?: Partial<MapSettings>;
|
||||
hiddenLayers?: string[];
|
||||
hideFilterActions?: boolean;
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
timeRange?: TimeRange;
|
||||
timeslice?: [number, number];
|
||||
filterByMapExtent?: boolean;
|
||||
isMovementSynchronized?: boolean;
|
||||
}
|
||||
export type MapByValueInput = {
|
||||
attributes: MapAttributes;
|
||||
} & EmbeddableInput &
|
||||
MapEmbeddableState;
|
||||
export type MapByReferenceInput = SavedObjectEmbeddableInput & MapEmbeddableState;
|
||||
export type MapEmbeddableInput = MapByValueInput | MapByReferenceInput;
|
||||
|
||||
export type MapEmbeddableOutput = EmbeddableOutput & {
|
||||
indexPatterns: DataView[];
|
||||
};
|
||||
|
||||
export type MapEmbeddableType = Embeddable<MapEmbeddableInput, MapEmbeddableOutput> & {
|
||||
getOnRenderComplete$(): Observable<void>;
|
||||
setIsSharable(isSharable: boolean): void;
|
||||
};
|
|
@ -28,8 +28,7 @@ export type {
|
|||
export type { MapsSetupApi, MapsStartApi } from './api';
|
||||
export type { CreateLayerDescriptorParams } from './classes/sources/es_search_source/create_layer_descriptor';
|
||||
|
||||
export type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from './embeddable';
|
||||
export { type MapApi, isMapApi } from './embeddable/map_api';
|
||||
export { type MapApi, type MapSerializedState, isMapApi } from './react_embeddable/types';
|
||||
|
||||
export type { EMSTermJoinConfig, SampleValuesConfig } from './ems_autosuggest';
|
||||
|
||||
|
|
|
@ -8,9 +8,23 @@
|
|||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { EMSSettings } from '@kbn/maps-ems-plugin/common/ems_settings';
|
||||
import { MapsEmsPluginPublicStart } from '@kbn/maps-ems-plugin/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { MapsConfigType } from '../config';
|
||||
import type { MapsPluginStartDependencies } from './plugin';
|
||||
|
||||
const servicesReady$ = new BehaviorSubject(false);
|
||||
export const untilPluginStartServicesReady = () => {
|
||||
if (servicesReady$.value) return Promise.resolve();
|
||||
return new Promise<void>((resolve) => {
|
||||
const subscription = servicesReady$.subscribe((isInitialized) => {
|
||||
if (isInitialized) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let isDarkMode = false;
|
||||
let coreStart: CoreStart;
|
||||
let pluginsStart: MapsPluginStartDependencies;
|
||||
|
@ -25,6 +39,8 @@ export function setStartServices(core: CoreStart, plugins: MapsPluginStartDepend
|
|||
core.theme.theme$.subscribe(({ darkMode }) => {
|
||||
isDarkMode = darkMode;
|
||||
});
|
||||
|
||||
servicesReady$.next(true);
|
||||
}
|
||||
|
||||
let isCloudEnabled = false;
|
||||
|
@ -84,6 +100,7 @@ export const isScreenshotMode = () => {
|
|||
return pluginsStart.screenshotMode ? pluginsStart.screenshotMode.isScreenshotMode() : false;
|
||||
};
|
||||
export const getServerless = () => pluginsStart.serverless;
|
||||
export const getEmbeddableEnhanced = () => pluginsStart.embeddableEnhanced;
|
||||
|
||||
// xpack.maps.* kibana.yml settings from this plugin
|
||||
let mapAppConfig: MapsConfigType;
|
||||
|
|
|
@ -9,8 +9,10 @@ import React from 'react';
|
|||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import type { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import type { RegionMapVisRenderValue } from './region_map_fn';
|
||||
import { REGION_MAP_RENDER } from './types';
|
||||
import { getAnalytics, getCoreI18n, getTheme } from '../../kibana_services';
|
||||
|
||||
const Component = dynamic(async () => {
|
||||
const { RegionMapVisualization } = await import('./region_map_visualization');
|
||||
|
@ -37,6 +39,15 @@ export const regionMapRenderer = {
|
|||
visConfig,
|
||||
};
|
||||
|
||||
render(<Component {...props} />, domNode);
|
||||
render(
|
||||
<KibanaRenderContextProvider
|
||||
analytics={getAnalytics()}
|
||||
i18n={getCoreI18n()}
|
||||
theme={getTheme()}
|
||||
>
|
||||
<Component {...props} />
|
||||
</KibanaRenderContextProvider>,
|
||||
domNode
|
||||
);
|
||||
},
|
||||
} as ExpressionRenderDefinition<RegionMapVisRenderValue>;
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { first } from 'rxjs';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
import { RegionMapVisConfig } from './types';
|
||||
import { MapComponent } from '../../embeddable/map_component';
|
||||
import { MapRenderer } from '../../react_embeddable/map_renderer';
|
||||
import { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor';
|
||||
|
||||
interface Props {
|
||||
|
@ -21,6 +23,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function RegionMapVisualization(props: Props) {
|
||||
const isMounted = useMountedState();
|
||||
const initialMapCenter = useMemo(() => {
|
||||
return {
|
||||
lat: props.visConfig.mapCenter[0],
|
||||
|
@ -37,7 +40,7 @@ export function RegionMapVisualization(props: Props) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return (
|
||||
<MapComponent
|
||||
<MapRenderer
|
||||
title={props.visConfig.layerDescriptorParams.label}
|
||||
filters={props.filters}
|
||||
query={props.query}
|
||||
|
@ -45,7 +48,13 @@ export function RegionMapVisualization(props: Props) {
|
|||
mapCenter={initialMapCenter}
|
||||
isLayerTOCOpen={true}
|
||||
layerList={initialLayerList}
|
||||
onInitialRenderComplete={props.onInitialRenderComplete}
|
||||
onApiAvailable={(api) => {
|
||||
api.onRenderComplete$.pipe(first()).subscribe(() => {
|
||||
if (isMounted()) {
|
||||
props.onInitialRenderComplete();
|
||||
}
|
||||
});
|
||||
}}
|
||||
isSharable={false}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -9,8 +9,10 @@ import React from 'react';
|
|||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import type { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import type { TileMapVisRenderValue } from './tile_map_fn';
|
||||
import { TILE_MAP_RENDER } from './types';
|
||||
import { getAnalytics, getCoreI18n, getTheme } from '../../kibana_services';
|
||||
|
||||
const Component = dynamic(async () => {
|
||||
const { TileMapVisualization } = await import('./tile_map_visualization');
|
||||
|
@ -37,6 +39,15 @@ export const tileMapRenderer = {
|
|||
visConfig,
|
||||
};
|
||||
|
||||
render(<Component {...props} />, domNode);
|
||||
render(
|
||||
<KibanaRenderContextProvider
|
||||
analytics={getAnalytics()}
|
||||
i18n={getCoreI18n()}
|
||||
theme={getTheme()}
|
||||
>
|
||||
<Component {...props} />
|
||||
</KibanaRenderContextProvider>,
|
||||
domNode
|
||||
);
|
||||
},
|
||||
} as ExpressionRenderDefinition<TileMapVisRenderValue>;
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { first } from 'rxjs';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
import type { TileMapVisConfig } from './types';
|
||||
import { MapComponent } from '../../embeddable/map_component';
|
||||
import { MapRenderer } from '../../react_embeddable/map_renderer';
|
||||
import { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_map_layer_descriptor';
|
||||
|
||||
interface Props {
|
||||
|
@ -21,6 +23,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function TileMapVisualization(props: Props) {
|
||||
const isMounted = useMountedState();
|
||||
const initialMapCenter = useMemo(() => {
|
||||
return {
|
||||
lat: props.visConfig.mapCenter[0],
|
||||
|
@ -37,7 +40,7 @@ export function TileMapVisualization(props: Props) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return (
|
||||
<MapComponent
|
||||
<MapRenderer
|
||||
title={props.visConfig.layerDescriptorParams.label}
|
||||
filters={props.filters}
|
||||
query={props.query}
|
||||
|
@ -45,7 +48,13 @@ export function TileMapVisualization(props: Props) {
|
|||
mapCenter={initialMapCenter}
|
||||
isLayerTOCOpen={true}
|
||||
layerList={initialLayerList}
|
||||
onInitialRenderComplete={props.onInitialRenderComplete}
|
||||
onApiAvailable={(api) => {
|
||||
api.onRenderComplete$.pipe(first()).subscribe(() => {
|
||||
if (isMounted()) {
|
||||
props.onInitialRenderComplete();
|
||||
}
|
||||
});
|
||||
}}
|
||||
isSharable={false}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -5,25 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Component, RefObject } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { LayerDescriptor } from '../../common/descriptor_types';
|
||||
import { INITIAL_LOCATION } from '../../common';
|
||||
import { MapEmbeddable } from '../embeddable';
|
||||
import { INITIAL_LOCATION, MAP_SAVED_OBJECT_TYPE } from '../../common';
|
||||
import { createBasemapLayerDescriptor } from '../classes/layers/create_basemap_layer_descriptor';
|
||||
import { MapApi, MapSerializedState } from '../react_embeddable/types';
|
||||
|
||||
export interface Props {
|
||||
passiveLayer: LayerDescriptor;
|
||||
onRenderComplete?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
mapEmbeddable: MapEmbeddable | null;
|
||||
}
|
||||
|
||||
/*
|
||||
* PassiveMap compoment is a wrapper around a map embeddable where passive layer descriptor provides features
|
||||
* and layer does not auto-fetch features based on changes to pan, zoom, filter, query, timeRange, and other state changes.
|
||||
|
@ -31,88 +26,76 @@ interface State {
|
|||
* Contrast with traditional map (active map), where layers independently auto-fetch features
|
||||
* based on changes to pan, zoom, filter, query, timeRange, and other state changes
|
||||
*/
|
||||
export class PassiveMap extends Component<Props, State> {
|
||||
private _isMounted = false;
|
||||
private _prevPassiveLayer = this.props.passiveLayer;
|
||||
private readonly _embeddableRef: RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
|
||||
private _onRenderSubscription: Subscription | undefined;
|
||||
export function PassiveMap(props: Props) {
|
||||
const isMounted = useMountedState();
|
||||
const mapApiRef = useRef<MapApi | undefined>(undefined);
|
||||
const beforeApiReadyPassiveLayerRef = useRef<LayerDescriptor | undefined>(undefined);
|
||||
const onRenderCompleteSubscriptionRef = useRef<Subscription | undefined>(undefined);
|
||||
|
||||
state: State = { mapEmbeddable: null };
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this._setupEmbeddable();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
if (this.state.mapEmbeddable) {
|
||||
this.state.mapEmbeddable.destroy();
|
||||
useEffect(() => {
|
||||
if (mapApiRef.current) {
|
||||
mapApiRef.current.updateLayerById(props.passiveLayer);
|
||||
} else {
|
||||
beforeApiReadyPassiveLayerRef.current = props.passiveLayer;
|
||||
}
|
||||
if (this._onRenderSubscription) {
|
||||
this._onRenderSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}, [props.passiveLayer]);
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.mapEmbeddable && this._prevPassiveLayer !== this.props.passiveLayer) {
|
||||
this.state.mapEmbeddable.updateLayerById(this.props.passiveLayer);
|
||||
this._prevPassiveLayer = this.props.passiveLayer;
|
||||
}
|
||||
}
|
||||
|
||||
async _setupEmbeddable() {
|
||||
const basemapLayerDescriptor = createBasemapLayerDescriptor();
|
||||
const intialLayers = basemapLayerDescriptor ? [basemapLayerDescriptor] : [];
|
||||
const mapEmbeddable = new MapEmbeddable(
|
||||
{
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
attributes: {
|
||||
title: '',
|
||||
layerListJSON: JSON.stringify([...intialLayers, this.props.passiveLayer]),
|
||||
},
|
||||
filters: [],
|
||||
hidePanelTitles: true,
|
||||
viewMode: ViewMode.VIEW,
|
||||
isLayerTOCOpen: false,
|
||||
hideFilterActions: true,
|
||||
mapSettings: {
|
||||
disableInteractive: false,
|
||||
hideToolbarOverlay: false,
|
||||
hideLayerControl: false,
|
||||
hideViewControl: false,
|
||||
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent
|
||||
autoFitToDataBounds: true, // this will auto-fit when there are changes to the filter and/or query
|
||||
},
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (onRenderCompleteSubscriptionRef.current) {
|
||||
onRenderCompleteSubscriptionRef.current.unsubscribe();
|
||||
}
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (this.props.onRenderComplete) {
|
||||
this._onRenderSubscription = mapEmbeddable.getOnRenderComplete$().subscribe(() => {
|
||||
if (this._isMounted && this.props.onRenderComplete) {
|
||||
this.props.onRenderComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this._isMounted) {
|
||||
mapEmbeddable.setIsSharable(false);
|
||||
this.setState({ mapEmbeddable }, () => {
|
||||
if (this.state.mapEmbeddable && this._embeddableRef.current) {
|
||||
this.state.mapEmbeddable.render(this._embeddableRef.current);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.mapEmbeddable) {
|
||||
return <EuiLoadingChart mono size="l" />;
|
||||
}
|
||||
|
||||
return <div className="mapEmbeddableContainer" ref={this._embeddableRef} />;
|
||||
}
|
||||
return (
|
||||
<div className="mapEmbeddableContainer">
|
||||
<ReactEmbeddableRenderer<MapSerializedState, MapApi>
|
||||
type={MAP_SAVED_OBJECT_TYPE}
|
||||
getParentApi={() => ({
|
||||
getSerializedStateForChild: () => {
|
||||
const basemapLayerDescriptor = createBasemapLayerDescriptor();
|
||||
const intialLayers = basemapLayerDescriptor ? [basemapLayerDescriptor] : [];
|
||||
return {
|
||||
rawState: {
|
||||
attributes: {
|
||||
title: '',
|
||||
layerListJSON: JSON.stringify([...intialLayers, props.passiveLayer]),
|
||||
},
|
||||
filters: [],
|
||||
hidePanelTitles: true,
|
||||
viewMode: ViewMode.VIEW,
|
||||
isLayerTOCOpen: false,
|
||||
hideFilterActions: true,
|
||||
mapSettings: {
|
||||
disableInteractive: false,
|
||||
hideToolbarOverlay: false,
|
||||
hideLayerControl: false,
|
||||
hideViewControl: false,
|
||||
initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent
|
||||
autoFitToDataBounds: true, // this will auto-fit when there are changes to the filter and/or query
|
||||
},
|
||||
isSharable: false,
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
},
|
||||
})}
|
||||
onApiAvailable={(api) => {
|
||||
mapApiRef.current = api;
|
||||
if (beforeApiReadyPassiveLayerRef.current) {
|
||||
api.updateLayerById(beforeApiReadyPassiveLayerRef.current);
|
||||
}
|
||||
if (props.onRenderComplete) {
|
||||
onRenderCompleteSubscriptionRef.current = api.onRenderComplete$.subscribe(() => {
|
||||
if (isMounted() && props.onRenderComplete) {
|
||||
props.onRenderComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
hidePanelChrome={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
import type { ResolvedSimpleSavedObject } from '@kbn/core/public';
|
||||
import { AttributeService } from '@kbn/embeddable-plugin/public';
|
||||
import type { OnSaveProps } from '@kbn/saved-objects-plugin/public';
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import type { MapAttributes } from '../common/content_management';
|
||||
import { MAP_EMBEDDABLE_NAME, MAP_SAVED_OBJECT_TYPE } from '../common/constants';
|
||||
import { getCoreOverlays, getEmbeddableService } from './kibana_services';
|
||||
import { extractReferences, injectReferences } from '../common/migrations/references';
|
||||
import { getMapClient, checkForDuplicateTitle } from './content_management';
|
||||
import { MapByValueInput, MapByReferenceInput } from './embeddable/types';
|
||||
|
||||
export interface SharingSavedObjectProps {
|
||||
outcome?: ResolvedSimpleSavedObject['outcome'];
|
||||
aliasTargetId?: ResolvedSimpleSavedObject['alias_target_id'];
|
||||
aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose'];
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
type MapDoc = MapAttributes & {
|
||||
references?: SavedObjectReference[];
|
||||
};
|
||||
export interface MapUnwrapMetaInfo {
|
||||
sharingSavedObjectProps: SharingSavedObjectProps;
|
||||
// Is this map managed by the system?
|
||||
managed: boolean;
|
||||
}
|
||||
|
||||
export type MapAttributeService = AttributeService<
|
||||
MapDoc,
|
||||
MapByValueInput,
|
||||
MapByReferenceInput,
|
||||
MapUnwrapMetaInfo
|
||||
>;
|
||||
|
||||
export const savedObjectToEmbeddableAttributes = (
|
||||
savedObject: SavedObjectCommon<MapAttributes>
|
||||
) => {
|
||||
const { attributes } = injectReferences(savedObject);
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
references: savedObject.references,
|
||||
};
|
||||
};
|
||||
|
||||
let mapAttributeService: MapAttributeService | null = null;
|
||||
|
||||
export function getMapAttributeService(): MapAttributeService {
|
||||
if (mapAttributeService) {
|
||||
return mapAttributeService;
|
||||
}
|
||||
|
||||
mapAttributeService = getEmbeddableService().getAttributeService<
|
||||
MapDoc,
|
||||
MapByValueInput,
|
||||
MapByReferenceInput,
|
||||
MapUnwrapMetaInfo
|
||||
>(MAP_SAVED_OBJECT_TYPE, {
|
||||
saveMethod: async (attributes: MapDoc, savedObjectId?: string) => {
|
||||
// AttributeService "attributes" contains "references" as a child.
|
||||
// SavedObjectClient "attributes" uses "references" as a sibling.
|
||||
// https://github.com/elastic/kibana/issues/83133
|
||||
const savedObjectClientReferences = attributes.references;
|
||||
const savedObjectClientAttributes = { ...attributes };
|
||||
delete savedObjectClientAttributes.references;
|
||||
const { attributes: updatedAttributes, references } = extractReferences({
|
||||
attributes: savedObjectClientAttributes,
|
||||
references: savedObjectClientReferences,
|
||||
});
|
||||
|
||||
const {
|
||||
item: { id },
|
||||
} = await (savedObjectId
|
||||
? getMapClient().update({
|
||||
id: savedObjectId,
|
||||
data: updatedAttributes,
|
||||
options: { references },
|
||||
})
|
||||
: getMapClient().create({ data: updatedAttributes, options: { references } }));
|
||||
return { id };
|
||||
},
|
||||
unwrapMethod: async (
|
||||
savedObjectId: string
|
||||
): Promise<{
|
||||
attributes: MapDoc;
|
||||
metaInfo: MapUnwrapMetaInfo;
|
||||
}> => {
|
||||
const {
|
||||
item: savedObject,
|
||||
meta: { outcome, aliasPurpose, aliasTargetId },
|
||||
} = await getMapClient<MapAttributes>().get(savedObjectId);
|
||||
|
||||
if (savedObject.error) {
|
||||
throw savedObject.error;
|
||||
}
|
||||
|
||||
return {
|
||||
attributes: savedObjectToEmbeddableAttributes(savedObject),
|
||||
metaInfo: {
|
||||
sharingSavedObjectProps: {
|
||||
aliasTargetId,
|
||||
outcome,
|
||||
aliasPurpose,
|
||||
sourceId: savedObjectId,
|
||||
},
|
||||
managed: Boolean(savedObject.managed),
|
||||
},
|
||||
};
|
||||
},
|
||||
checkForDuplicateTitle: (props: OnSaveProps) => {
|
||||
return checkForDuplicateTitle(
|
||||
{
|
||||
title: props.newTitle,
|
||||
copyOnSave: false,
|
||||
lastSavedTitle: '',
|
||||
isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed,
|
||||
getDisplayName: () => MAP_EMBEDDABLE_NAME,
|
||||
onTitleDuplicate: props.onTitleDuplicate,
|
||||
},
|
||||
{
|
||||
overlays: getCoreOverlays(),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return mapAttributeService;
|
||||
}
|
|
@ -25,6 +25,7 @@ import type { VisualizationsSetup, VisualizationsStart } from '@kbn/visualizatio
|
|||
import type { Plugin as ExpressionsPublicPlugin } from '@kbn/expressions-plugin/public';
|
||||
import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { MapsEmsPluginPublicStart } from '@kbn/maps-ems-plugin/public';
|
||||
|
@ -69,11 +70,10 @@ import {
|
|||
suggestEMSTermJoinConfig,
|
||||
} from './api';
|
||||
import { MapsXPackConfig, MapsConfigType } from '../config';
|
||||
import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory';
|
||||
import { filterByMapExtentAction } from './trigger_actions/filter_by_map_extent/action';
|
||||
import { synchronizeMovementAction } from './trigger_actions/synchronize_movement/action';
|
||||
import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action';
|
||||
import { APP_NAME, APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants';
|
||||
import { APP_NAME, APP_ICON_SOLUTION, APP_ID } from '../common/constants';
|
||||
import { getMapsVisTypeAlias } from './maps_vis_type_alias';
|
||||
import { featureCatalogueEntry } from './feature_catalogue_entry';
|
||||
import {
|
||||
|
@ -81,15 +81,15 @@ import {
|
|||
setMapAppConfig,
|
||||
setSpaceId,
|
||||
setStartServices,
|
||||
untilPluginStartServicesReady,
|
||||
} from './kibana_services';
|
||||
import { MapInspectorView } from './inspector/map_adapter/map_inspector_view';
|
||||
import { VectorTileInspectorView } from './inspector/vector_tile_adapter/vector_tile_inspector_view';
|
||||
|
||||
import { PassiveMapLazy, setupLensChoroplethChart } from './lens';
|
||||
import { CONTENT_ID, LATEST_VERSION, MapAttributes } from '../common/content_management';
|
||||
import { savedObjectToEmbeddableAttributes } from './map_attribute_service';
|
||||
import { MapByValueInput } from './embeddable';
|
||||
import { MapComponentLazy } from './embeddable/map_component_lazy';
|
||||
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
|
||||
import { setupMapEmbeddable } from './react_embeddable/setup_map_embeddable';
|
||||
import { MapRendererLazy } from './react_embeddable/map_renderer_lazy';
|
||||
|
||||
export interface MapsPluginSetupDependencies {
|
||||
cloud?: CloudSetup;
|
||||
|
@ -112,6 +112,7 @@ export interface MapsPluginStartDependencies {
|
|||
data: DataPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
embeddable: EmbeddableStart;
|
||||
embeddableEnhanced?: EmbeddableEnhancedPluginStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
fileUpload: FileUploadPluginStart;
|
||||
inspector: InspectorStartContract;
|
||||
|
@ -194,7 +195,6 @@ export class MapsPlugin
|
|||
plugins.home.featureCatalogue.register(featureCatalogueEntry);
|
||||
}
|
||||
plugins.visualizations.registerAlias(getMapsVisTypeAlias());
|
||||
plugins.embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory());
|
||||
|
||||
core.application.register({
|
||||
id: APP_ID,
|
||||
|
@ -204,14 +204,18 @@ export class MapsPlugin
|
|||
euiIconType: APP_ICON_SOLUTION,
|
||||
category: DEFAULT_APP_CATEGORIES.kibana,
|
||||
async mount(params: AppMountParameters) {
|
||||
const [coreStart, { savedObjectsTagging, spaces }] = await core.getStartServices();
|
||||
const [, startServices, { renderApp }] = await Promise.all([
|
||||
untilPluginStartServicesReady(),
|
||||
core.getStartServices(),
|
||||
import('./render_app'),
|
||||
]);
|
||||
const [coreStart, { savedObjectsTagging, spaces }] = startServices;
|
||||
const UsageTracker =
|
||||
plugins.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
|
||||
const activeSpace = await spaces?.getActiveSpace();
|
||||
if (activeSpace) {
|
||||
setSpaceId(activeSpace.id);
|
||||
}
|
||||
const { renderApp } = await import('./render_app');
|
||||
return renderApp(params, { coreStart, AppUsageTracker: UsageTracker, savedObjectsTagging });
|
||||
},
|
||||
});
|
||||
|
@ -224,18 +228,7 @@ export class MapsPlugin
|
|||
name: APP_NAME,
|
||||
});
|
||||
|
||||
plugins.embeddable.registerSavedObjectToPanelMethod<MapAttributes, MapByValueInput>(
|
||||
CONTENT_ID,
|
||||
(savedObject) => {
|
||||
if (!savedObject.managed) {
|
||||
return { savedObjectId: savedObject.id };
|
||||
}
|
||||
|
||||
return {
|
||||
attributes: savedObjectToEmbeddableAttributes(savedObject),
|
||||
};
|
||||
}
|
||||
);
|
||||
setupMapEmbeddable(plugins.embeddable);
|
||||
|
||||
setupLensChoroplethChart(core, plugins.expressions, plugins.lens);
|
||||
|
||||
|
@ -273,7 +266,7 @@ export class MapsPlugin
|
|||
return {
|
||||
createLayerDescriptors,
|
||||
suggestEMSTermJoinConfig,
|
||||
Map: MapComponentLazy,
|
||||
Map: MapRendererLazy,
|
||||
PassiveMap: PassiveMapLazy,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '@kbn/unified-search-plugin/public';
|
||||
import { VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { RawValue } from '../../common/constants';
|
||||
import type { MapApi } from './types';
|
||||
import { getUiActions } from '../kibana_services';
|
||||
import { isUrlDrilldown, toValueClickDataFormat } from '../trigger_actions/trigger_utils';
|
||||
|
||||
export function initializeActionHandlers(getApi: () => MapApi | undefined) {
|
||||
function getActionContext() {
|
||||
const trigger = getUiActions().getTrigger(APPLY_FILTER_TRIGGER);
|
||||
if (!trigger) {
|
||||
throw new Error('Unable to get context, could not locate trigger');
|
||||
}
|
||||
return {
|
||||
embeddable: getApi(),
|
||||
trigger,
|
||||
} as ActionExecutionContext;
|
||||
}
|
||||
|
||||
return {
|
||||
addFilters: async (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => {
|
||||
const executeContext = {
|
||||
...getActionContext(),
|
||||
filters,
|
||||
};
|
||||
const action = getUiActions().getAction(actionId);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
},
|
||||
getActionContext,
|
||||
getFilterActions: async () => {
|
||||
const filterActions = await getUiActions().getTriggerCompatibleActions(APPLY_FILTER_TRIGGER, {
|
||||
embeddable: getApi(),
|
||||
filters: [],
|
||||
});
|
||||
const valueClickActions = await getUiActions().getTriggerCompatibleActions(
|
||||
VALUE_CLICK_TRIGGER,
|
||||
{
|
||||
embeddable: getApi(),
|
||||
data: {
|
||||
// uiActions.getTriggerCompatibleActions validates action with provided context
|
||||
// so if event.key and event.value are used in the URL template but can not be parsed from context
|
||||
// then the action is filtered out.
|
||||
// To prevent filtering out actions, provide dummy context when initially fetching actions.
|
||||
data: toValueClickDataFormat('anyfield', 'anyvalue'),
|
||||
},
|
||||
}
|
||||
);
|
||||
return [...filterActions, ...valueClickActions.filter(isUrlDrilldown)];
|
||||
},
|
||||
onSingleValueTrigger: (actionId: string, key: string, value: RawValue) => {
|
||||
const action = getUiActions().getAction(actionId);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply action, could not locate action');
|
||||
}
|
||||
const executeContext = {
|
||||
...getActionContext(),
|
||||
data: {
|
||||
data: toValueClickDataFormat(key, value),
|
||||
},
|
||||
};
|
||||
action.execute(executeContext);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '@kbn/unified-search-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { getPanelTitle, StateComparators } from '@kbn/presentation-publishing';
|
||||
import { createExtentFilter } from '../../common/elasticsearch_util';
|
||||
import { SavedMap } from '../routes/map_page';
|
||||
import { mapEmbeddablesSingleton } from './map_embeddables_singleton';
|
||||
import {
|
||||
getGeoFieldNames,
|
||||
getGoto,
|
||||
getMapCenter,
|
||||
getMapExtent,
|
||||
getMapReady,
|
||||
getMapSettings,
|
||||
getMapZoom,
|
||||
} from '../selectors/map_selectors';
|
||||
import { setGotoWithCenter, setMapSettings } from '../actions';
|
||||
import { MapExtent } from '../../common/descriptor_types';
|
||||
import { getUiActions } from '../kibana_services';
|
||||
import { getGeoFieldsLabel } from './get_geo_fields_label';
|
||||
import { MapApi, MapSerializedState } from './types';
|
||||
import { setOnMapMove } from '../reducers/non_serializable_instances';
|
||||
|
||||
export function initializeCrossPanelActions({
|
||||
controlledBy,
|
||||
getActionContext,
|
||||
getApi,
|
||||
savedMap,
|
||||
state,
|
||||
uuid,
|
||||
}: {
|
||||
controlledBy: string;
|
||||
getActionContext: () => ActionExecutionContext;
|
||||
getApi: () => MapApi | undefined;
|
||||
savedMap: SavedMap;
|
||||
state: MapSerializedState;
|
||||
uuid: string;
|
||||
}) {
|
||||
const isMovementSynchronized$ = new BehaviorSubject<boolean | undefined>(
|
||||
state.isMovementSynchronized
|
||||
);
|
||||
function getIsMovementSynchronized() {
|
||||
return isMovementSynchronized$.value ?? true;
|
||||
}
|
||||
function setIsMovementSynchronized(next: boolean) {
|
||||
isMovementSynchronized$.next(next);
|
||||
}
|
||||
|
||||
const isFilterByMapExtent$ = new BehaviorSubject<boolean | undefined>(state.filterByMapExtent);
|
||||
function getIsFilterByMapExtent() {
|
||||
return isFilterByMapExtent$.value ?? false;
|
||||
}
|
||||
function setIsFilterByMapExtent(next: boolean) {
|
||||
isFilterByMapExtent$.next(next);
|
||||
}
|
||||
|
||||
let prevMapExtent: MapExtent | undefined;
|
||||
|
||||
function mapSyncHandler(lat: number, lon: number, zoom: number) {
|
||||
// auto fit to bounds is not compatable with map synchronization
|
||||
// auto fit to bounds may cause map location to never stablize and bound back and forth between bounds on different maps
|
||||
if (getMapSettings(savedMap.getStore().getState()).autoFitToDataBounds) {
|
||||
savedMap.getStore().dispatch(setMapSettings({ autoFitToDataBounds: false }));
|
||||
}
|
||||
savedMap.getStore().dispatch(setGotoWithCenter({ lat, lon, zoom }));
|
||||
}
|
||||
|
||||
function gotoSynchronizedLocation() {
|
||||
const syncedLocation = mapEmbeddablesSingleton.getLocation();
|
||||
if (syncedLocation) {
|
||||
// set map to synchronized view
|
||||
mapSyncHandler(syncedLocation.lat, syncedLocation.lon, syncedLocation.zoom);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getMapReady(savedMap.getStore().getState())) {
|
||||
// Initialize synchronized view to map's goto
|
||||
// Use goto because un-rendered map will not have accurate mapCenter and mapZoom.
|
||||
const goto = getGoto(savedMap.getStore().getState());
|
||||
if (goto && goto.center) {
|
||||
mapEmbeddablesSingleton.setLocation(
|
||||
uuid,
|
||||
goto.center.lat,
|
||||
goto.center.lon,
|
||||
goto.center.zoom
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize synchronized view to map's view
|
||||
const center = getMapCenter(savedMap.getStore().getState());
|
||||
const zoom = getMapZoom(savedMap.getStore().getState());
|
||||
mapEmbeddablesSingleton.setLocation(uuid, center.lat, center.lon, zoom);
|
||||
}
|
||||
|
||||
// debounce to fix timing issue for dashboard with multiple maps with synchronized movement and filter by map extent enabled
|
||||
const setMapExtentFilter = _.debounce(() => {
|
||||
const mapExtent = getMapExtent(savedMap.getStore().getState());
|
||||
const geoFieldNames = mapEmbeddablesSingleton.getGeoFieldNames();
|
||||
|
||||
if (mapExtent === undefined || geoFieldNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevMapExtent = mapExtent;
|
||||
|
||||
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
|
||||
mapExtentFilter.meta.controlledBy = controlledBy;
|
||||
mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
|
||||
defaultMessage: '{geoFieldsLabel} within map bounds',
|
||||
values: { geoFieldsLabel: getGeoFieldsLabel(geoFieldNames) },
|
||||
});
|
||||
|
||||
const executeContext = {
|
||||
...getActionContext(),
|
||||
filters: [mapExtentFilter],
|
||||
controlledBy,
|
||||
};
|
||||
const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply map extent filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
}, 100);
|
||||
|
||||
function clearMapExtentFilter() {
|
||||
prevMapExtent = undefined;
|
||||
const executeContext = {
|
||||
...getActionContext(),
|
||||
filters: [],
|
||||
controlledBy,
|
||||
};
|
||||
const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER);
|
||||
if (!action) {
|
||||
throw new Error('Unable to apply map extent filter, could not locate action');
|
||||
}
|
||||
action.execute(executeContext);
|
||||
}
|
||||
|
||||
mapEmbeddablesSingleton.register(uuid, {
|
||||
getTitle: () => {
|
||||
const mapApi = getApi();
|
||||
const title = mapApi ? getPanelTitle(mapApi) : undefined;
|
||||
return title
|
||||
? title
|
||||
: i18n.translate('xpack.maps.embeddable.untitleMap', {
|
||||
defaultMessage: 'Untitled map',
|
||||
});
|
||||
},
|
||||
onLocationChange: mapSyncHandler,
|
||||
getIsMovementSynchronized,
|
||||
setIsMovementSynchronized: (isMovementSynchronized: boolean) => {
|
||||
setIsMovementSynchronized(isMovementSynchronized);
|
||||
if (isMovementSynchronized) {
|
||||
gotoSynchronizedLocation();
|
||||
} else if (!isMovementSynchronized && savedMap.getAutoFitToBounds()) {
|
||||
// restore autoFitToBounds when isMovementSynchronized disabled
|
||||
savedMap.getStore().dispatch(setMapSettings({ autoFitToDataBounds: true }));
|
||||
}
|
||||
},
|
||||
getIsFilterByMapExtent,
|
||||
setIsFilterByMapExtent: (isFilterByMapExtent: boolean) => {
|
||||
setIsFilterByMapExtent(isFilterByMapExtent);
|
||||
if (isFilterByMapExtent) {
|
||||
setMapExtentFilter();
|
||||
} else {
|
||||
clearMapExtentFilter();
|
||||
}
|
||||
},
|
||||
getGeoFieldNames: () => {
|
||||
return getGeoFieldNames(savedMap.getStore().getState());
|
||||
},
|
||||
});
|
||||
|
||||
if (getIsMovementSynchronized()) {
|
||||
gotoSynchronizedLocation();
|
||||
}
|
||||
|
||||
// Passing callback into redux store instead of regular pattern of getting redux state changes for performance reasons
|
||||
savedMap.getStore().dispatch(
|
||||
setOnMapMove((lat: number, lon: number, zoom: number) => {
|
||||
if (getIsMovementSynchronized()) {
|
||||
mapEmbeddablesSingleton.setLocation(uuid, lat, lon, zoom);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const unsubscribeFromStore = savedMap.getStore().subscribe(() => {
|
||||
if (!getMapReady(savedMap.getStore().getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
getIsFilterByMapExtent() &&
|
||||
!_.isEqual(prevMapExtent, getMapExtent(savedMap.getStore().getState()))
|
||||
) {
|
||||
setMapExtentFilter();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
mapEmbeddablesSingleton.unregister(uuid);
|
||||
unsubscribeFromStore();
|
||||
},
|
||||
comparators: {
|
||||
isMovementSynchronized: [isMovementSynchronized$, setIsMovementSynchronized],
|
||||
filterByMapExtent: [isFilterByMapExtent$, setIsFilterByMapExtent],
|
||||
} as StateComparators<Pick<MapSerializedState, 'isMovementSynchronized' | 'filterByMapExtent'>>,
|
||||
getIsFilterByMapExtent,
|
||||
serialize: () => {
|
||||
return {
|
||||
isMovementSynchronized: isMovementSynchronized$.value,
|
||||
filterByMapExtent: isFilterByMapExtent$.value,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import { createMapStore } from '../reducers/store';
|
||||
import { initializeDataViews } from './initialize_data_views';
|
||||
import { createLayerDescriptor } from '../classes/sources/es_search_source';
|
||||
import { ES_GEO_FIELD_TYPE } from '../../common/constants';
|
||||
import { skip } from 'rxjs';
|
||||
|
||||
jest.mock('../kibana_services', () => {
|
||||
return {
|
||||
getIsDarkMode() {
|
||||
return false;
|
||||
},
|
||||
getMapsCapabilities() {
|
||||
return { save: true };
|
||||
},
|
||||
getShowMapsInspectorAdapter() {
|
||||
return false;
|
||||
},
|
||||
getEMSSettings() {
|
||||
return {
|
||||
isEMSUrlSet() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../index_pattern_util', () => {
|
||||
return {
|
||||
getIndexPatternsFromIds: async (ids: string[]) => {
|
||||
return ids.length
|
||||
? ids.map(
|
||||
(id) =>
|
||||
({
|
||||
id,
|
||||
} as unknown as DataView)
|
||||
)
|
||||
: [];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('dataViews$', () => {
|
||||
const onEmitMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onEmitMock.mockReset();
|
||||
});
|
||||
|
||||
test('Should emit when data view added', async () => {
|
||||
const dataViewApi = initializeDataViews(createMapStore());
|
||||
const subscription = dataViewApi.dataViews.pipe(skip(1)).subscribe(onEmitMock);
|
||||
|
||||
dataViewApi.setLayerList([
|
||||
createLayerDescriptor({
|
||||
indexPatternId: '1234',
|
||||
geoFieldName: 'location',
|
||||
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
|
||||
}),
|
||||
]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(onEmitMock.mock.calls).toHaveLength(1);
|
||||
expect(onEmitMock.mock.calls[0][0]).toEqual([
|
||||
{
|
||||
id: '1234',
|
||||
},
|
||||
]);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
test('Should emit when data view removed', async () => {
|
||||
const dataViewApi = initializeDataViews(createMapStore());
|
||||
dataViewApi.setLayerList([
|
||||
createLayerDescriptor({
|
||||
indexPatternId: '1234',
|
||||
geoFieldName: 'location',
|
||||
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
|
||||
}),
|
||||
]);
|
||||
const subscription = dataViewApi.dataViews.pipe(skip(1)).subscribe(onEmitMock);
|
||||
|
||||
dataViewApi.setLayerList([]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(onEmitMock.mock.calls).toHaveLength(1);
|
||||
expect(onEmitMock.mock.calls[0][0]).toEqual([]);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
test('Should emit not emit when data view ids do not change', async () => {
|
||||
const dataViewApi = initializeDataViews(createMapStore());
|
||||
dataViewApi.setLayerList([
|
||||
createLayerDescriptor({
|
||||
indexPatternId: '1234',
|
||||
geoFieldName: 'location',
|
||||
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
|
||||
}),
|
||||
createLayerDescriptor({
|
||||
indexPatternId: '4567',
|
||||
geoFieldName: 'location',
|
||||
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
|
||||
}),
|
||||
]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const subscription = dataViewApi.dataViews.pipe(skip(1)).subscribe(onEmitMock);
|
||||
|
||||
dataViewApi.setLayerList([
|
||||
createLayerDescriptor({
|
||||
indexPatternId: '4567',
|
||||
geoFieldName: 'location',
|
||||
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
|
||||
}),
|
||||
createLayerDescriptor({
|
||||
indexPatternId: '1234',
|
||||
geoFieldName: 'location',
|
||||
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
|
||||
}),
|
||||
]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(onEmitMock).not.toHaveBeenCalled();
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import { LayerDescriptor } from '../../common';
|
||||
import { replaceLayerList, updateLayerDescriptor } from '../actions';
|
||||
import { MapStore } from '../reducers/store';
|
||||
import { getIndexPatternsFromIds } from '../index_pattern_util';
|
||||
import { getMapSettings, getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors';
|
||||
import { autoFitToBounds, syncDataForLayerId } from '../actions/data_request_actions';
|
||||
|
||||
export function initializeDataViews(store: MapStore) {
|
||||
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(undefined);
|
||||
let dataViewsFetchToken: symbol | undefined;
|
||||
|
||||
async function updateDataViews() {
|
||||
const queryableDataViewIds = getQueryableUniqueIndexPatternIds(store.getState());
|
||||
const prevDataViewIds = dataViews$.getValue()?.map((dataView) => {
|
||||
return dataView.id;
|
||||
});
|
||||
if (isEqual(queryableDataViewIds.sort(), prevDataViewIds?.sort())) {
|
||||
return;
|
||||
}
|
||||
const currentDataViewsFetchToken = Symbol();
|
||||
dataViewsFetchToken = currentDataViewsFetchToken;
|
||||
const dataViews = await getIndexPatternsFromIds(queryableDataViewIds);
|
||||
// ignore responses from obsolete requests
|
||||
if (currentDataViewsFetchToken !== dataViewsFetchToken) {
|
||||
return;
|
||||
}
|
||||
dataViews$.next(dataViews);
|
||||
}
|
||||
|
||||
updateDataViews();
|
||||
|
||||
const syncLayerTokens: Record<string, symbol> = {};
|
||||
|
||||
return {
|
||||
dataViews: dataViews$,
|
||||
setLayerList(layerList: LayerDescriptor[]) {
|
||||
store.dispatch<any>(replaceLayerList(layerList));
|
||||
updateDataViews();
|
||||
},
|
||||
updateLayerById: (layerDescriptor: LayerDescriptor) => {
|
||||
store.dispatch<any>(updateLayerDescriptor(layerDescriptor));
|
||||
updateDataViews();
|
||||
(async () => {
|
||||
const currentSyncLayerToken = Symbol();
|
||||
syncLayerTokens[layerDescriptor.id] = currentSyncLayerToken;
|
||||
await store.dispatch<any>(syncDataForLayerId(layerDescriptor.id, false));
|
||||
// stop processing responses from obsolete requests
|
||||
if (currentSyncLayerToken !== syncLayerTokens[layerDescriptor.id]) {
|
||||
return;
|
||||
}
|
||||
if (getMapSettings(store.getState()).autoFitToDataBounds) {
|
||||
store.dispatch<any>(autoFitToBounds());
|
||||
}
|
||||
})();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { apiHasAppContext } from '@kbn/presentation-publishing';
|
||||
import { APP_ID, getEditPath, getFullPath, MAP_EMBEDDABLE_NAME } from '../../common/constants';
|
||||
import { getEmbeddableService, getHttp, getMapsCapabilities } from '../kibana_services';
|
||||
import { MapSerializedState } from './types';
|
||||
|
||||
export function initializeEditApi(
|
||||
uuid: string,
|
||||
getState: () => MapSerializedState,
|
||||
parentApi?: unknown,
|
||||
savedObjectId?: string
|
||||
) {
|
||||
if (!parentApi || !apiHasAppContext(parentApi)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parentApiContext = parentApi.getAppContext();
|
||||
|
||||
return {
|
||||
getTypeDisplayName: () => {
|
||||
return MAP_EMBEDDABLE_NAME;
|
||||
},
|
||||
onEdit: async () => {
|
||||
const stateTransfer = getEmbeddableService().getStateTransfer();
|
||||
await stateTransfer.navigateToEditor(APP_ID, {
|
||||
path: getEditPath(savedObjectId),
|
||||
state: {
|
||||
embeddableId: uuid,
|
||||
valueInput: getState(),
|
||||
originatingApp: parentApiContext.currentAppId,
|
||||
originatingPath: parentApiContext.getCurrentPath?.(),
|
||||
},
|
||||
});
|
||||
},
|
||||
isEditingEnabled: () => {
|
||||
return getMapsCapabilities().save as boolean;
|
||||
},
|
||||
getEditHref: async () => {
|
||||
return getHttp().basePath.prepend(getFullPath(savedObjectId));
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FetchContext, fetch$ } from '@kbn/presentation-publishing';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { MapExtent } from '../../common/descriptor_types';
|
||||
import { getSearchService } from '../kibana_services';
|
||||
import { MapStore } from '../reducers/store';
|
||||
import { MapApi } from './types';
|
||||
import { setMapSettings, setQuery } from '../actions';
|
||||
|
||||
function getIsRestore(searchSessionId?: string) {
|
||||
if (!searchSessionId) {
|
||||
return false;
|
||||
}
|
||||
const searchSessionOptions = getSearchService().session.getSearchOptions(searchSessionId);
|
||||
return searchSessionOptions ? searchSessionOptions.isRestore : false;
|
||||
}
|
||||
|
||||
export function initializeFetch({
|
||||
api,
|
||||
controlledBy,
|
||||
getIsFilterByMapExtent,
|
||||
searchSessionMapBuffer,
|
||||
store,
|
||||
}: {
|
||||
api: MapApi;
|
||||
controlledBy: string;
|
||||
getIsFilterByMapExtent: () => boolean;
|
||||
searchSessionMapBuffer?: MapExtent;
|
||||
store: MapStore;
|
||||
}) {
|
||||
let prevIsRestore: boolean | undefined;
|
||||
const fetchSubscription = fetch$(api).subscribe((fetchContext: FetchContext) => {
|
||||
// New search session id causes all layers from elasticsearch to refetch data.
|
||||
// Dashboard provides a new search session id anytime filters change.
|
||||
// Thus, filtering embeddable container by map extent causes a new search session id any time the map is moved.
|
||||
// Disabling search session when filtering embeddable container by map extent.
|
||||
// The use case for search sessions (restoring results because of slow responses) does not match the use case of
|
||||
// filtering by map extent (rapid responses as users explore their map).
|
||||
const searchSessionId = getIsFilterByMapExtent() ? undefined : fetchContext.searchSessionId;
|
||||
const isRestore = getIsRestore(searchSessionId);
|
||||
|
||||
// Map can not be interacted with when viewing session restore.
|
||||
// Session restore only show data for cached extent and new data can not be fetch
|
||||
if (isRestore !== prevIsRestore) {
|
||||
prevIsRestore = isRestore;
|
||||
store.dispatch(
|
||||
setMapSettings({
|
||||
disableInteractive: isRestore,
|
||||
hideToolbarOverlay: isRestore,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
store.dispatch<any>(
|
||||
setQuery({
|
||||
filters: fetchContext.filters
|
||||
? fetchContext.filters.filter(
|
||||
(filter) => !filter.meta.disabled && filter.meta.controlledBy !== controlledBy
|
||||
)
|
||||
: [],
|
||||
query: fetchContext.query as Query | undefined,
|
||||
timeFilters: fetchContext.timeRange,
|
||||
timeslice: fetchContext.timeslice
|
||||
? { from: fetchContext.timeslice[0], to: fetchContext.timeslice[1] }
|
||||
: undefined,
|
||||
clearTimeslice: fetchContext.timeslice === undefined,
|
||||
forceRefresh: fetchContext.isReload,
|
||||
searchSessionId,
|
||||
searchSessionMapBuffer: isRestore ? searchSessionMapBuffer : undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
fetchSubscription.unsubscribe();
|
||||
};
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, debounceTime, filter, map, Subscription } from 'rxjs';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { PublishingSubject, StateComparators } from '@kbn/presentation-publishing';
|
||||
import { KibanaExecutionContext } from '@kbn/core-execution-context-common';
|
||||
import { PaletteRegistry } from '@kbn/coloring';
|
||||
import { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import { MapCenterAndZoom } from '../../common/descriptor_types';
|
||||
import { APP_ID, getEditPath, RENDER_TIMEOUT } from '../../common/constants';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors';
|
||||
import {
|
||||
getLayerList,
|
||||
getLayerListRaw,
|
||||
getMapBuffer,
|
||||
getMapCenter,
|
||||
getMapReady,
|
||||
getMapZoom,
|
||||
isMapLoading,
|
||||
} from '../selectors/map_selectors';
|
||||
import {
|
||||
setEmbeddableSearchContext,
|
||||
setExecutionContext,
|
||||
setGotoWithCenter,
|
||||
setHiddenLayers,
|
||||
setIsLayerTOCOpen,
|
||||
setMapSettings,
|
||||
setOpenTOCDetails,
|
||||
setQuery,
|
||||
setReadOnly,
|
||||
} from '../actions';
|
||||
import type { MapSerializedState } from './types';
|
||||
import { getCharts, getExecutionContextService } from '../kibana_services';
|
||||
import {
|
||||
EventHandlers,
|
||||
getInspectorAdapters,
|
||||
setChartsPaletteServiceGetColor,
|
||||
setEventHandlers,
|
||||
} from '../reducers/non_serializable_instances';
|
||||
import { SavedMap } from '../routes';
|
||||
|
||||
function getMapCenterAndZoom(state: MapStoreState) {
|
||||
return {
|
||||
...getMapCenter(state),
|
||||
zoom: getMapZoom(state),
|
||||
};
|
||||
}
|
||||
|
||||
function getHiddenLayerIds(state: MapStoreState) {
|
||||
return getLayerListRaw(state)
|
||||
.filter((layer) => !layer.visible)
|
||||
.map((layer) => layer.id);
|
||||
}
|
||||
|
||||
export function initializeReduxSync({
|
||||
savedMap,
|
||||
state,
|
||||
syncColors$,
|
||||
uuid,
|
||||
}: {
|
||||
savedMap: SavedMap;
|
||||
state: MapSerializedState;
|
||||
syncColors$?: PublishingSubject<boolean | undefined>;
|
||||
uuid: string;
|
||||
}) {
|
||||
const store = savedMap.getStore();
|
||||
|
||||
// initializing comparitor publishing subjects to state instead of store state values
|
||||
// because store is not settled until map is rendered and mapReady is true
|
||||
const hiddenLayers$ = new BehaviorSubject<string[]>(
|
||||
state.hiddenLayers ?? getHiddenLayerIds(store.getState())
|
||||
);
|
||||
const isLayerTOCOpen$ = new BehaviorSubject<boolean>(
|
||||
state.isLayerTOCOpen ?? getIsLayerTOCOpen(store.getState())
|
||||
);
|
||||
const mapCenterAndZoom$ = new BehaviorSubject<MapCenterAndZoom>(
|
||||
state.mapCenter ?? getMapCenterAndZoom(store.getState())
|
||||
);
|
||||
const openTOCDetails$ = new BehaviorSubject<string[]>(
|
||||
state.openTOCDetails ?? getOpenTOCDetails(store.getState())
|
||||
);
|
||||
const dataLoading$ = new BehaviorSubject<boolean | undefined>(undefined);
|
||||
|
||||
const unsubscribeFromStore = store.subscribe(() => {
|
||||
if (!getMapReady(store.getState())) {
|
||||
return;
|
||||
}
|
||||
const nextHiddenLayers = getHiddenLayerIds(store.getState());
|
||||
if (!fastIsEqual(hiddenLayers$.value, nextHiddenLayers)) {
|
||||
hiddenLayers$.next(nextHiddenLayers);
|
||||
}
|
||||
|
||||
const nextIsLayerTOCOpen = getIsLayerTOCOpen(store.getState());
|
||||
if (isLayerTOCOpen$.value !== nextIsLayerTOCOpen) {
|
||||
isLayerTOCOpen$.next(nextIsLayerTOCOpen);
|
||||
}
|
||||
|
||||
const nextMapCenterAndZoom = getMapCenterAndZoom(store.getState());
|
||||
if (!fastIsEqual(mapCenterAndZoom$.value, nextMapCenterAndZoom)) {
|
||||
mapCenterAndZoom$.next(nextMapCenterAndZoom);
|
||||
}
|
||||
|
||||
const nextOpenTOCDetails = getOpenTOCDetails(store.getState());
|
||||
if (!fastIsEqual(openTOCDetails$.value, nextOpenTOCDetails)) {
|
||||
openTOCDetails$.next(nextOpenTOCDetails);
|
||||
}
|
||||
|
||||
const nextIsMapLoading = isMapLoading(store.getState());
|
||||
if (nextIsMapLoading !== dataLoading$.value) {
|
||||
dataLoading$.next(nextIsMapLoading);
|
||||
}
|
||||
});
|
||||
|
||||
store.dispatch(setReadOnly(true));
|
||||
store.dispatch(
|
||||
setMapSettings({
|
||||
keydownScrollZoom: true,
|
||||
showTimesliderToggleButton: false,
|
||||
})
|
||||
);
|
||||
store.dispatch(setExecutionContext(getExecutionContext(uuid, state.savedObjectId)));
|
||||
|
||||
const filters$ = new BehaviorSubject<Filter[] | undefined>(undefined);
|
||||
const query$ = new BehaviorSubject<AggregateQuery | Query | undefined>(undefined);
|
||||
const mapStateJSON = savedMap.getAttributes().mapStateJSON;
|
||||
if (mapStateJSON) {
|
||||
try {
|
||||
const mapState = JSON.parse(mapStateJSON);
|
||||
if (mapState.filters) {
|
||||
filters$.next(mapState.filters);
|
||||
}
|
||||
if (mapState.query) {
|
||||
query$.next(mapState.query);
|
||||
}
|
||||
store.dispatch(
|
||||
setEmbeddableSearchContext({
|
||||
filters: mapState.filters,
|
||||
query: mapState.query,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults
|
||||
}
|
||||
}
|
||||
|
||||
let syncColorsSubscription: Subscription | undefined;
|
||||
let syncColorsSymbol: symbol | undefined;
|
||||
if (syncColors$) {
|
||||
syncColorsSubscription = syncColors$.subscribe(async (syncColors: boolean | undefined) => {
|
||||
const currentSyncColorsSymbol = Symbol();
|
||||
syncColorsSymbol = currentSyncColorsSymbol;
|
||||
const chartsPaletteServiceGetColor = syncColors
|
||||
? await getChartsPaletteServiceGetColor()
|
||||
: null;
|
||||
if (syncColorsSymbol === currentSyncColorsSymbol) {
|
||||
store.dispatch(setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
if (syncColorsSubscription) syncColorsSubscription.unsubscribe();
|
||||
unsubscribeFromStore();
|
||||
},
|
||||
api: {
|
||||
dataLoading: dataLoading$,
|
||||
filters$,
|
||||
getInspectorAdapters: () => {
|
||||
return getInspectorAdapters(store.getState());
|
||||
},
|
||||
getLayerList: () => {
|
||||
return getLayerList(store.getState());
|
||||
},
|
||||
onRenderComplete$: dataLoading$.pipe(
|
||||
filter((isDataLoading) => typeof isDataLoading === 'boolean' && !isDataLoading),
|
||||
debounceTime(RENDER_TIMEOUT),
|
||||
map(() => {
|
||||
// Observable notifies subscriber when rendering is complete
|
||||
// Return void to not expose internal implemenation details of observabale
|
||||
return;
|
||||
})
|
||||
),
|
||||
query$,
|
||||
reload: () => {
|
||||
store.dispatch<any>(
|
||||
setQuery({
|
||||
forceRefresh: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
setEventHandlers: (eventHandlers: EventHandlers) => {
|
||||
store.dispatch(setEventHandlers(eventHandlers));
|
||||
},
|
||||
},
|
||||
comparators: {
|
||||
// mapBuffer comparator intentionally omitted and is not part of unsaved changes check
|
||||
hiddenLayers: [
|
||||
hiddenLayers$,
|
||||
(nextValue: string[]) => {
|
||||
store.dispatch<any>(setHiddenLayers(nextValue));
|
||||
},
|
||||
fastIsEqual,
|
||||
],
|
||||
isLayerTOCOpen: [
|
||||
isLayerTOCOpen$,
|
||||
(nextValue: boolean) => {
|
||||
store.dispatch(setIsLayerTOCOpen(nextValue));
|
||||
},
|
||||
],
|
||||
mapCenter: [
|
||||
mapCenterAndZoom$,
|
||||
(nextValue: MapCenterAndZoom) => {
|
||||
store.dispatch(setGotoWithCenter(nextValue));
|
||||
},
|
||||
fastIsEqual,
|
||||
],
|
||||
openTOCDetails: [
|
||||
openTOCDetails$,
|
||||
(nextValue: string[]) => {
|
||||
store.dispatch(setOpenTOCDetails(nextValue));
|
||||
},
|
||||
fastIsEqual,
|
||||
],
|
||||
} as StateComparators<
|
||||
Pick<MapSerializedState, 'hiddenLayers' | 'isLayerTOCOpen' | 'mapCenter' | 'openTOCDetails'>
|
||||
>,
|
||||
serialize: () => {
|
||||
return {
|
||||
hiddenLayers: getHiddenLayerIds(store.getState()),
|
||||
isLayerTOCOpen: getIsLayerTOCOpen(store.getState()),
|
||||
mapBuffer: getMapBuffer(store.getState()),
|
||||
mapCenter: getMapCenterAndZoom(store.getState()),
|
||||
openTOCDetails: getOpenTOCDetails(store.getState()),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getExecutionContext(uuid: string, savedObjectId: string | undefined) {
|
||||
const parentContext = getExecutionContextService().get();
|
||||
const mapContext: KibanaExecutionContext = {
|
||||
type: APP_ID,
|
||||
name: APP_ID,
|
||||
id: uuid,
|
||||
url: getEditPath(savedObjectId),
|
||||
};
|
||||
|
||||
return parentContext
|
||||
? {
|
||||
...parentContext,
|
||||
child: mapContext,
|
||||
}
|
||||
: mapContext;
|
||||
}
|
||||
|
||||
async function getChartsPaletteServiceGetColor(): Promise<((value: string) => string) | null> {
|
||||
const chartsService = getCharts();
|
||||
const paletteRegistry: PaletteRegistry | null = chartsService
|
||||
? await chartsService.palettes.getPalettes()
|
||||
: null;
|
||||
if (!paletteRegistry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const paletteDefinition = paletteRegistry.get('default');
|
||||
const chartConfiguration = { syncColors: true };
|
||||
return (value: string) => {
|
||||
const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }];
|
||||
const color = paletteDefinition.getCategoricalColor(series, chartConfiguration);
|
||||
return color ? color : '#3d3d3d';
|
||||
};
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SerializedPanelState } from '@kbn/presentation-containers';
|
||||
import { HasLibraryTransforms } from '@kbn/presentation-publishing';
|
||||
import { getCore, getCoreOverlays } from '../kibana_services';
|
||||
import type { MapAttributes } from '../../common/content_management';
|
||||
import { SavedMap } from '../routes/map_page';
|
||||
import { checkForDuplicateTitle, getMapClient } from '../content_management';
|
||||
import { MAP_EMBEDDABLE_NAME } from '../../common/constants';
|
||||
import { MapSerializedState } from './types';
|
||||
|
||||
export function getByReferenceState(state: MapSerializedState | undefined, savedObjectId: string) {
|
||||
const { attributes, ...byRefState } = state ?? {};
|
||||
return {
|
||||
...byRefState,
|
||||
savedObjectId,
|
||||
};
|
||||
}
|
||||
|
||||
export function getByValueState(state: MapSerializedState | undefined, attributes: MapAttributes) {
|
||||
const { savedObjectId, ...byValueState } = state ?? {};
|
||||
return {
|
||||
...byValueState,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
|
||||
export function initializeLibraryTransforms(
|
||||
savedMap: SavedMap,
|
||||
serializeState: () => SerializedPanelState<MapSerializedState>
|
||||
): HasLibraryTransforms<MapSerializedState> {
|
||||
return {
|
||||
canLinkToLibrary: async () => {
|
||||
const { maps } = getCore().application.capabilities;
|
||||
return maps.save && savedMap.getSavedObjectId() === undefined;
|
||||
},
|
||||
saveToLibrary: async (title: string) => {
|
||||
const state = serializeState();
|
||||
const {
|
||||
item: { id: savedObjectId },
|
||||
} = await getMapClient().create({
|
||||
data: {
|
||||
...(state.rawState?.attributes ?? {}),
|
||||
title,
|
||||
},
|
||||
options: { references: state.references ?? [] },
|
||||
});
|
||||
return savedObjectId;
|
||||
},
|
||||
getByReferenceState: (libraryId: string) => {
|
||||
return getByReferenceState(serializeState().rawState, libraryId);
|
||||
},
|
||||
checkForDuplicateTitle: async (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => {
|
||||
await checkForDuplicateTitle(
|
||||
{
|
||||
title: newTitle,
|
||||
copyOnSave: false,
|
||||
lastSavedTitle: '',
|
||||
isTitleDuplicateConfirmed,
|
||||
getDisplayName: () => MAP_EMBEDDABLE_NAME,
|
||||
onTitleDuplicate,
|
||||
},
|
||||
{
|
||||
overlays: getCoreOverlays(),
|
||||
}
|
||||
);
|
||||
},
|
||||
canUnlinkFromLibrary: async () => {
|
||||
return savedMap.getSavedObjectId() !== undefined;
|
||||
},
|
||||
getByValueState: () => {
|
||||
return getByValueState(serializeState().rawState, savedMap.getAttributes());
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
|
||||
import { ReactEmbeddableFactory, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import {
|
||||
areTriggersDisabled,
|
||||
getUnchangingComparator,
|
||||
initializeTimeRange,
|
||||
initializeTitles,
|
||||
useBatchedPublishingSubjects,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { apiPublishesSettings } from '@kbn/presentation-containers/interfaces/publishes_settings';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import { inject } from '../../common/embeddable';
|
||||
import type { MapApi, MapSerializedState } from './types';
|
||||
import { SavedMap } from '../routes/map_page';
|
||||
import { initializeReduxSync } from './initialize_redux_sync';
|
||||
import {
|
||||
getByReferenceState,
|
||||
getByValueState,
|
||||
initializeLibraryTransforms,
|
||||
} from './library_transforms';
|
||||
import { getEmbeddableEnhanced, getSpacesApi } from '../kibana_services';
|
||||
import { initializeActionHandlers } from './initialize_action_handlers';
|
||||
import { MapContainer } from '../connected_components/map_container';
|
||||
import { waitUntilTimeLayersLoad$ } from '../routes/map_page/map_app/wait_until_time_layers_load';
|
||||
import { initializeCrossPanelActions } from './initialize_cross_panel_actions';
|
||||
import { initializeDataViews } from './initialize_data_views';
|
||||
import { initializeFetch } from './initialize_fetch';
|
||||
import { initializeEditApi } from './initialize_edit_api';
|
||||
import { extractReferences } from '../../common/migrations/references';
|
||||
|
||||
export function getControlledBy(id: string) {
|
||||
return `mapEmbeddablePanel${id}`;
|
||||
}
|
||||
|
||||
export const mapEmbeddableFactory: ReactEmbeddableFactory<MapSerializedState, MapApi> = {
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
deserializeState: (state) => {
|
||||
return state.rawState
|
||||
? (inject(
|
||||
state.rawState as EmbeddableStateWithType,
|
||||
state.references ?? []
|
||||
) as unknown as MapSerializedState)
|
||||
: {};
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const savedMap = new SavedMap({
|
||||
mapSerializedState: state,
|
||||
});
|
||||
await savedMap.whenReady();
|
||||
|
||||
// eslint bug, eslint thinks api is never reassigned even though it is
|
||||
// eslint-disable-next-line prefer-const
|
||||
let api: MapApi | undefined;
|
||||
const getApi = () => api;
|
||||
const sharingSavedObjectProps = savedMap.getSharingSavedObjectProps();
|
||||
const spaces = getSpacesApi();
|
||||
const controlledBy = getControlledBy(uuid);
|
||||
const title = initializeTitles(state);
|
||||
const timeRange = initializeTimeRange(state);
|
||||
const dynamicActionsApi = getEmbeddableEnhanced()?.initializeReactEmbeddableDynamicActions(
|
||||
uuid,
|
||||
() => title.titlesApi.panelTitle.getValue(),
|
||||
state
|
||||
);
|
||||
const maybeStopDynamicActions = dynamicActionsApi?.startDynamicActions();
|
||||
|
||||
const defaultPanelTitle$ = new BehaviorSubject<string | undefined>(
|
||||
savedMap.getAttributes().title
|
||||
);
|
||||
const defaultPanelDescription$ = new BehaviorSubject<string | undefined>(
|
||||
savedMap.getAttributes().description
|
||||
);
|
||||
const reduxSync = initializeReduxSync({
|
||||
savedMap,
|
||||
state,
|
||||
syncColors$: apiPublishesSettings(parentApi) ? parentApi.settings.syncColors$ : undefined,
|
||||
uuid,
|
||||
});
|
||||
const actionHandlers = initializeActionHandlers(getApi);
|
||||
const crossPanelActions = initializeCrossPanelActions({
|
||||
controlledBy,
|
||||
getActionContext: actionHandlers.getActionContext,
|
||||
getApi,
|
||||
state,
|
||||
savedMap,
|
||||
uuid,
|
||||
});
|
||||
|
||||
function getState() {
|
||||
return {
|
||||
...state,
|
||||
...timeRange.serialize(),
|
||||
...title.serializeTitles(),
|
||||
...(dynamicActionsApi?.serializeDynamicActions() ?? {}),
|
||||
...crossPanelActions.serialize(),
|
||||
...reduxSync.serialize(),
|
||||
};
|
||||
}
|
||||
|
||||
function serializeState() {
|
||||
const rawState = getState();
|
||||
|
||||
// by-reference embeddable
|
||||
if (rawState.savedObjectId) {
|
||||
// No references to extract for by-reference embeddable since all references are stored with by-reference saved object
|
||||
return {
|
||||
rawState: getByReferenceState(rawState, rawState.savedObjectId),
|
||||
references: [],
|
||||
};
|
||||
}
|
||||
|
||||
// by-value embeddable
|
||||
const { attributes, references } = extractReferences({
|
||||
attributes: savedMap.getAttributes(),
|
||||
});
|
||||
|
||||
return {
|
||||
rawState: getByValueState(rawState, attributes),
|
||||
references,
|
||||
};
|
||||
}
|
||||
|
||||
api = buildApi(
|
||||
{
|
||||
defaultPanelTitle: defaultPanelTitle$,
|
||||
defaultPanelDescription: defaultPanelDescription$,
|
||||
...timeRange.api,
|
||||
...(dynamicActionsApi?.dynamicActionsApi ?? {}),
|
||||
...title.titlesApi,
|
||||
...reduxSync.api,
|
||||
...initializeEditApi(uuid, getState, parentApi, state.savedObjectId),
|
||||
...initializeLibraryTransforms(savedMap, serializeState),
|
||||
...initializeDataViews(savedMap.getStore()),
|
||||
serializeState,
|
||||
supportedTriggers: () => {
|
||||
return [APPLY_FILTER_TRIGGER, VALUE_CLICK_TRIGGER];
|
||||
},
|
||||
},
|
||||
{
|
||||
...timeRange.comparators,
|
||||
...title.titleComparators,
|
||||
...(dynamicActionsApi?.dynamicActionsComparator ?? {
|
||||
enhancements: getUnchangingComparator(),
|
||||
}),
|
||||
...crossPanelActions.comparators,
|
||||
...reduxSync.comparators,
|
||||
// readonly comparators
|
||||
attributes: getUnchangingComparator(),
|
||||
mapBuffer: getUnchangingComparator(),
|
||||
savedObjectId: getUnchangingComparator(),
|
||||
mapSettings: getUnchangingComparator(),
|
||||
hideFilterActions: getUnchangingComparator(),
|
||||
isSharable: getUnchangingComparator(),
|
||||
tooltipRenderer: getUnchangingComparator(),
|
||||
}
|
||||
);
|
||||
|
||||
const unsubscribeFromFetch = initializeFetch({
|
||||
api,
|
||||
controlledBy,
|
||||
getIsFilterByMapExtent: crossPanelActions.getIsFilterByMapExtent,
|
||||
searchSessionMapBuffer: state.mapBuffer,
|
||||
store: savedMap.getStore(),
|
||||
});
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [defaultPanelTitle, panelTitle, defaultPanelDescription, panelDescription] =
|
||||
useBatchedPublishingSubjects(
|
||||
defaultPanelTitle$,
|
||||
title.titlesApi.panelTitle,
|
||||
defaultPanelDescription$,
|
||||
title.titlesApi.panelDescription
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
crossPanelActions.cleanup();
|
||||
reduxSync.cleanup();
|
||||
unsubscribeFromFetch();
|
||||
maybeStopDynamicActions?.stopDynamicActions();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return sharingSavedObjectProps &&
|
||||
spaces &&
|
||||
sharingSavedObjectProps?.outcome === 'conflict' ? (
|
||||
<div className="mapEmbeddedError">
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
iconColor="danger"
|
||||
data-test-subj="embeddable-maps-failure"
|
||||
body={spaces.ui.components.getEmbeddableLegacyUrlConflict({
|
||||
targetType: MAP_SAVED_OBJECT_TYPE,
|
||||
sourceId: sharingSavedObjectProps.sourceId!,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Provider store={savedMap.getStore()}>
|
||||
<MapContainer
|
||||
onSingleValueTrigger={actionHandlers.onSingleValueTrigger}
|
||||
addFilters={
|
||||
state.hideFilterActions || areTriggersDisabled(api)
|
||||
? null
|
||||
: actionHandlers.addFilters
|
||||
}
|
||||
getFilterActions={actionHandlers.getFilterActions}
|
||||
getActionContext={actionHandlers.getActionContext}
|
||||
renderTooltipContent={state.tooltipRenderer}
|
||||
title={panelTitle ?? defaultPanelTitle}
|
||||
description={panelDescription ?? defaultPanelDescription}
|
||||
waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(savedMap.getStore())}
|
||||
isSharable={state.isSharable ?? true}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
103
x-pack/plugins/maps/public/react_embeddable/map_renderer.tsx
Normal file
103
x-pack/plugins/maps/public/react_embeddable/map_renderer.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
|
||||
import { useSearchApi } from '@kbn/presentation-publishing';
|
||||
import type { LayerDescriptor, MapCenterAndZoom, MapSettings } from '../../common/descriptor_types';
|
||||
import { createBasemapLayerDescriptor } from '../classes/layers/create_basemap_layer_descriptor';
|
||||
import { MapApi, MapSerializedState } from './types';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
|
||||
|
||||
function getLayers(layerList: LayerDescriptor[]) {
|
||||
const basemapLayer = createBasemapLayerDescriptor();
|
||||
return basemapLayer ? [basemapLayer, ...layerList] : layerList;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
timeRange?: TimeRange;
|
||||
layerList: LayerDescriptor[];
|
||||
mapSettings?: Partial<MapSettings>;
|
||||
hideFilterActions?: boolean;
|
||||
isLayerTOCOpen?: boolean;
|
||||
mapCenter?: MapCenterAndZoom;
|
||||
getTooltipRenderer?: () => RenderToolTipContent;
|
||||
onApiAvailable?: (api: MapApi) => void;
|
||||
/*
|
||||
* Set to false to exclude sharing attributes 'data-*'.
|
||||
*/
|
||||
isSharable?: boolean;
|
||||
}
|
||||
|
||||
export function MapRenderer(props: Props) {
|
||||
const mapApiRef = useRef<MapApi | undefined>(undefined);
|
||||
const beforeApiReadyLayerListRef = useRef<LayerDescriptor[] | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (mapApiRef.current) {
|
||||
mapApiRef.current.setLayerList(getLayers(props.layerList));
|
||||
} else {
|
||||
beforeApiReadyLayerListRef.current = getLayers(props.layerList);
|
||||
}
|
||||
}, [props.layerList]);
|
||||
|
||||
const searchApi = useSearchApi({
|
||||
filters: props.filters,
|
||||
query: props.query,
|
||||
timeRange: props.timeRange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mapEmbeddableContainer">
|
||||
<ReactEmbeddableRenderer<MapSerializedState, MapApi>
|
||||
type={MAP_SAVED_OBJECT_TYPE}
|
||||
getParentApi={() => ({
|
||||
...searchApi,
|
||||
getSerializedStateForChild: () => {
|
||||
const rawState: MapSerializedState = {
|
||||
attributes: {
|
||||
title: props.title ?? '',
|
||||
layerListJSON: JSON.stringify(getLayers(props.layerList)),
|
||||
},
|
||||
hidePanelTitles: !Boolean(props.title),
|
||||
isLayerTOCOpen:
|
||||
typeof props.isLayerTOCOpen === 'boolean' ? props.isLayerTOCOpen : false,
|
||||
hideFilterActions:
|
||||
typeof props.hideFilterActions === 'boolean' ? props.hideFilterActions : false,
|
||||
mapCenter: props.mapCenter,
|
||||
mapSettings: props.mapSettings ?? {},
|
||||
isSharable: props.isSharable,
|
||||
};
|
||||
if (props.getTooltipRenderer) {
|
||||
rawState.tooltipRenderer = props.getTooltipRenderer();
|
||||
}
|
||||
return {
|
||||
rawState,
|
||||
references: [],
|
||||
};
|
||||
},
|
||||
})}
|
||||
onApiAvailable={(api) => {
|
||||
mapApiRef.current = api;
|
||||
if (beforeApiReadyLayerListRef.current) {
|
||||
api.setLayerList(beforeApiReadyLayerListRef.current);
|
||||
}
|
||||
|
||||
if (props.onApiAvailable) {
|
||||
props.onApiAvailable(api);
|
||||
}
|
||||
}}
|
||||
hidePanelChrome={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -7,15 +7,15 @@
|
|||
|
||||
import React from 'react';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import type { Props } from './map_component';
|
||||
import type { Props } from './map_renderer';
|
||||
|
||||
const Component = dynamic(async () => {
|
||||
const { MapComponent } = await import('./map_component');
|
||||
const { MapRenderer } = await import('./map_renderer');
|
||||
return {
|
||||
default: MapComponent,
|
||||
default: MapRenderer,
|
||||
};
|
||||
});
|
||||
|
||||
export function MapComponentLazy(props: Props) {
|
||||
export function MapRendererLazy(props: Props) {
|
||||
return <Component {...props} />;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MapAttributes } from '../../common/content_management';
|
||||
import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants';
|
||||
import { untilPluginStartServicesReady } from '../kibana_services';
|
||||
|
||||
export function setupMapEmbeddable(embeddableSetup: EmbeddableSetup) {
|
||||
embeddableSetup.registerReactEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, async () => {
|
||||
const startServicesPromise = untilPluginStartServicesReady();
|
||||
const [, { mapEmbeddableFactory }] = await Promise.all([
|
||||
startServicesPromise,
|
||||
import('./map_react_embeddable'),
|
||||
]);
|
||||
|
||||
return mapEmbeddableFactory;
|
||||
});
|
||||
|
||||
embeddableSetup.registerReactEmbeddableSavedObject<MapAttributes>({
|
||||
onAdd: (container, savedObject) => {
|
||||
container.addNewPanel({
|
||||
panelType: MAP_SAVED_OBJECT_TYPE,
|
||||
initialState: { savedObjectId: savedObject.id },
|
||||
});
|
||||
},
|
||||
embeddableType: MAP_SAVED_OBJECT_TYPE,
|
||||
savedObjectType: MAP_SAVED_OBJECT_TYPE,
|
||||
savedObjectName: i18n.translate('xpack.maps.mapSavedObjectLabel', {
|
||||
defaultMessage: 'Map',
|
||||
}),
|
||||
getIconForSavedObject: () => APP_ICON,
|
||||
});
|
||||
}
|
86
x-pack/plugins/maps/public/react_embeddable/types.ts
Normal file
86
x-pack/plugins/maps/public/react_embeddable/types.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { HasInspectorAdapters } from '@kbn/inspector-plugin/public';
|
||||
import {
|
||||
apiIsOfType,
|
||||
apiPublishesPanelTitle,
|
||||
apiPublishesUnifiedSearch,
|
||||
HasEditCapabilities,
|
||||
HasLibraryTransforms,
|
||||
HasSupportedTriggers,
|
||||
PublishesDataLoading,
|
||||
PublishesDataViews,
|
||||
PublishesUnifiedSearch,
|
||||
SerializedTitles,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { HasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin';
|
||||
import { Observable } from 'rxjs';
|
||||
import { MapAttributes } from '../../common/content_management';
|
||||
import {
|
||||
LayerDescriptor,
|
||||
MapCenterAndZoom,
|
||||
MapExtent,
|
||||
MapSettings,
|
||||
} from '../../common/descriptor_types';
|
||||
import { ILayer } from '../classes/layers/layer';
|
||||
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
|
||||
import { EventHandlers } from '../reducers/non_serializable_instances';
|
||||
|
||||
export type MapSerializedState = SerializedTitles &
|
||||
Partial<DynamicActionsSerializedState> & {
|
||||
// by-valye
|
||||
attributes?: MapAttributes;
|
||||
// by-reference
|
||||
savedObjectId?: string;
|
||||
|
||||
isLayerTOCOpen?: boolean;
|
||||
openTOCDetails?: string[];
|
||||
mapCenter?: MapCenterAndZoom;
|
||||
mapBuffer?: MapExtent;
|
||||
mapSettings?: Partial<MapSettings>;
|
||||
hiddenLayers?: string[];
|
||||
hideFilterActions?: boolean;
|
||||
timeRange?: TimeRange;
|
||||
filterByMapExtent?: boolean;
|
||||
isMovementSynchronized?: boolean;
|
||||
|
||||
// Configuration item that are never persisted
|
||||
// Putting in state as a temporary work around
|
||||
isSharable?: boolean;
|
||||
tooltipRenderer?: RenderToolTipContent;
|
||||
};
|
||||
|
||||
export type MapApi = DefaultEmbeddableApi<MapSerializedState> &
|
||||
HasDynamicActions &
|
||||
Partial<HasEditCapabilities> &
|
||||
HasInspectorAdapters &
|
||||
HasSupportedTriggers &
|
||||
PublishesDataLoading &
|
||||
PublishesDataViews &
|
||||
PublishesUnifiedSearch &
|
||||
HasLibraryTransforms<MapSerializedState> & {
|
||||
getLayerList: () => ILayer[];
|
||||
reload: () => void;
|
||||
setEventHandlers: (eventHandlers: EventHandlers) => void;
|
||||
setLayerList: (layerList: LayerDescriptor[]) => void;
|
||||
updateLayerById: (layerDescriptor: LayerDescriptor) => void;
|
||||
onRenderComplete$: Observable<void>;
|
||||
};
|
||||
|
||||
export const isMapApi = (api: unknown): api is MapApi => {
|
||||
return Boolean(
|
||||
api &&
|
||||
apiIsOfType(api, 'map') &&
|
||||
typeof (api as MapApi).getLayerList === 'function' &&
|
||||
apiPublishesPanelTitle(api) &&
|
||||
apiPublishesUnifiedSearch(api)
|
||||
);
|
||||
};
|
|
@ -27,9 +27,9 @@ import {
|
|||
getCore,
|
||||
} from './kibana_services';
|
||||
import { ListPage, MapPage } from './routes';
|
||||
import { MapByValueInput, MapByReferenceInput } from './embeddable/types';
|
||||
import { APP_ID } from '../common/constants';
|
||||
import { registerLayerWizards } from './classes/layers/wizards/load_layer_wizards';
|
||||
import { MapSerializedState } from './react_embeddable/types';
|
||||
|
||||
function setAppChrome() {
|
||||
if (!getMapsCapabilities().save) {
|
||||
|
@ -83,19 +83,19 @@ export async function renderApp(
|
|||
const { embeddableId, originatingApp, valueInput, originatingPath } =
|
||||
stateTransfer.getIncomingEditorState(APP_ID) || {};
|
||||
|
||||
let mapEmbeddableInput;
|
||||
let mapSerializedState: MapSerializedState | undefined;
|
||||
if (routeProps.match.params.savedMapId) {
|
||||
mapEmbeddableInput = {
|
||||
mapSerializedState = {
|
||||
savedObjectId: routeProps.match.params.savedMapId,
|
||||
} as MapByReferenceInput;
|
||||
};
|
||||
} else if (valueInput) {
|
||||
mapEmbeddableInput = valueInput as MapByValueInput;
|
||||
mapSerializedState = valueInput as MapSerializedState;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExitFullScreenButtonKibanaProvider coreStart={getCore()}>
|
||||
<MapPage
|
||||
mapEmbeddableInput={mapEmbeddableInput}
|
||||
mapSerializedState={mapSerializedState}
|
||||
embeddableId={embeddableId}
|
||||
onAppLeave={onAppLeave}
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
|
|
|
@ -15,10 +15,10 @@ import {
|
|||
getInitialLayersFromUrlParam,
|
||||
getOpenLayerWizardFromUrlParam,
|
||||
} from './saved_map';
|
||||
import { MapEmbeddableInput } from '../../embeddable/types';
|
||||
import type { MapSerializedState } from '../../react_embeddable/types';
|
||||
|
||||
interface Props {
|
||||
mapEmbeddableInput?: MapEmbeddableInput;
|
||||
mapSerializedState?: MapSerializedState;
|
||||
embeddableId?: string;
|
||||
onAppLeave: AppMountParameters['onAppLeave'];
|
||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
||||
|
@ -45,7 +45,7 @@ export class MapPage extends Component<Props, State> {
|
|||
this.state = {
|
||||
savedMap: new SavedMap({
|
||||
defaultLayers: getInitialLayersFromUrlParam(),
|
||||
mapEmbeddableInput: props.mapEmbeddableInput,
|
||||
mapSerializedState: props.mapSerializedState,
|
||||
embeddableId: props.embeddableId,
|
||||
originatingApp: props.originatingApp,
|
||||
originatingPath: props.originatingPath,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ResolvedSimpleSavedObject, SavedObjectReference } from '@kbn/core/public';
|
||||
import type { MapAttributes } from '../../../../common/content_management';
|
||||
import { getMapClient } from '../../../content_management';
|
||||
import { injectReferences } from '../../../../common/migrations/references';
|
||||
|
||||
export interface SharingSavedObjectProps {
|
||||
outcome?: ResolvedSimpleSavedObject['outcome'];
|
||||
aliasTargetId?: ResolvedSimpleSavedObject['alias_target_id'];
|
||||
aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose'];
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
export async function loadFromLibrary(savedObjectId: string): Promise<{
|
||||
attributes: MapAttributes;
|
||||
sharingSavedObjectProps: SharingSavedObjectProps;
|
||||
managed: boolean;
|
||||
references?: SavedObjectReference[];
|
||||
}> {
|
||||
const {
|
||||
item: savedObject,
|
||||
meta: { outcome, aliasPurpose, aliasTargetId },
|
||||
} = await getMapClient<MapAttributes>().get(savedObjectId);
|
||||
|
||||
if (savedObject.error) {
|
||||
throw savedObject.error;
|
||||
}
|
||||
|
||||
const { attributes } = injectReferences(savedObject);
|
||||
|
||||
return {
|
||||
attributes,
|
||||
sharingSavedObjectProps: {
|
||||
aliasTargetId,
|
||||
outcome,
|
||||
aliasPurpose,
|
||||
sourceId: savedObjectId,
|
||||
},
|
||||
managed: Boolean(savedObject.managed),
|
||||
references: savedObject.references,
|
||||
};
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import type { MapAttributes } from '../../../../common/content_management';
|
||||
import { getMapClient } from '../../../content_management';
|
||||
|
||||
export async function saveToLibrary(
|
||||
attributes: MapAttributes,
|
||||
references: SavedObjectReference[],
|
||||
savedObjectId?: string
|
||||
) {
|
||||
const {
|
||||
item: { id },
|
||||
} = await (savedObjectId
|
||||
? getMapClient().update({
|
||||
id: savedObjectId,
|
||||
data: attributes,
|
||||
options: { references },
|
||||
})
|
||||
: getMapClient().create({ data: attributes, options: { references } }));
|
||||
return { id };
|
||||
}
|
|
@ -35,8 +35,9 @@ import {
|
|||
setHiddenLayers,
|
||||
} from '../../../actions';
|
||||
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors';
|
||||
import { getMapAttributeService, SharingSavedObjectProps } from '../../../map_attribute_service';
|
||||
import { MapByReferenceInput, MapEmbeddableInput } from '../../../embeddable/types';
|
||||
import { loadFromLibrary, SharingSavedObjectProps } from './load_from_library';
|
||||
import { saveToLibrary } from './save_to_library';
|
||||
import { MapSerializedState } from '../../../react_embeddable/types';
|
||||
import {
|
||||
getCoreChrome,
|
||||
getIndexPatternService,
|
||||
|
@ -56,6 +57,8 @@ import { ParsedMapStateJSON, ParsedUiStateJSON } from './types';
|
|||
import { setAutoOpenLayerWizardId } from '../../../actions/ui_actions';
|
||||
import { LayerStatsCollector, MapSettingsCollector } from '../../../../common/telemetry';
|
||||
import { getIndexPatternsFromIds } from '../../../index_pattern_util';
|
||||
import { extractReferences } from '../../../../common/migrations/references';
|
||||
import { getByReferenceState, getByValueState } from '../../../react_embeddable/library_transforms';
|
||||
|
||||
function setMapSettingsFromEncodedState(settings: Partial<MapSettings>) {
|
||||
const decodedCustomIcons = settings.customIcons
|
||||
|
@ -76,7 +79,7 @@ export class SavedMap {
|
|||
private readonly _defaultLayers: LayerDescriptor[];
|
||||
private readonly _embeddableId?: string;
|
||||
private _initialLayerListConfig: LayerDescriptor[] = [];
|
||||
private _mapEmbeddableInput?: MapEmbeddableInput;
|
||||
private _mapSerializedState?: MapSerializedState;
|
||||
private readonly _onSaveCallback?: () => void;
|
||||
private _originatingApp?: string;
|
||||
private _originatingPath?: string;
|
||||
|
@ -88,7 +91,7 @@ export class SavedMap {
|
|||
|
||||
constructor({
|
||||
defaultLayers = [],
|
||||
mapEmbeddableInput,
|
||||
mapSerializedState,
|
||||
embeddableId,
|
||||
onSaveCallback,
|
||||
originatingApp,
|
||||
|
@ -97,7 +100,7 @@ export class SavedMap {
|
|||
defaultLayerWizard,
|
||||
}: {
|
||||
defaultLayers?: LayerDescriptor[];
|
||||
mapEmbeddableInput?: MapEmbeddableInput;
|
||||
mapSerializedState?: MapSerializedState;
|
||||
embeddableId?: string;
|
||||
onSaveCallback?: () => void;
|
||||
originatingApp?: string;
|
||||
|
@ -106,7 +109,7 @@ export class SavedMap {
|
|||
defaultLayerWizard?: string;
|
||||
}) {
|
||||
this._defaultLayers = defaultLayers;
|
||||
this._mapEmbeddableInput = mapEmbeddableInput;
|
||||
this._mapSerializedState = mapSerializedState;
|
||||
this._embeddableId = embeddableId;
|
||||
this._onSaveCallback = onSaveCallback;
|
||||
this._originatingApp = originatingApp;
|
||||
|
@ -124,25 +127,25 @@ export class SavedMap {
|
|||
async whenReady() {
|
||||
await whenLicenseInitialized();
|
||||
|
||||
if (!this._mapEmbeddableInput) {
|
||||
this._attributes = {
|
||||
title: '',
|
||||
description: '',
|
||||
};
|
||||
} else {
|
||||
const { attributes: doc, metaInfo } = await getMapAttributeService().unwrapAttributes(
|
||||
this._mapEmbeddableInput
|
||||
if (this._mapSerializedState?.savedObjectId) {
|
||||
const { attributes, managed, references, sharingSavedObjectProps } = await loadFromLibrary(
|
||||
this._mapSerializedState.savedObjectId
|
||||
);
|
||||
const { references, ...savedObjectAttributes } = doc;
|
||||
this._attributes = savedObjectAttributes;
|
||||
if (metaInfo?.sharingSavedObjectProps) {
|
||||
this._sharingSavedObjectProps = metaInfo.sharingSavedObjectProps;
|
||||
this._attributes = attributes;
|
||||
if (sharingSavedObjectProps) {
|
||||
this._sharingSavedObjectProps = sharingSavedObjectProps;
|
||||
}
|
||||
this._managed = Boolean(metaInfo?.managed);
|
||||
this._managed = managed;
|
||||
const savedObjectsTagging = getSavedObjectsTagging();
|
||||
if (savedObjectsTagging && references && references.length) {
|
||||
this._tags = savedObjectsTagging.ui.getTagIdsFromReferences(references);
|
||||
}
|
||||
} else {
|
||||
this._attributes = this._mapSerializedState?.attributes
|
||||
? this._mapSerializedState.attributes
|
||||
: {
|
||||
title: '',
|
||||
};
|
||||
}
|
||||
|
||||
this._reportUsage();
|
||||
|
@ -162,8 +165,8 @@ export class SavedMap {
|
|||
}
|
||||
}
|
||||
|
||||
if (this._mapEmbeddableInput && this._mapEmbeddableInput.mapSettings !== undefined) {
|
||||
this._store.dispatch(setMapSettingsFromEncodedState(this._mapEmbeddableInput.mapSettings));
|
||||
if (this._mapSerializedState?.mapSettings !== undefined) {
|
||||
this._store.dispatch(setMapSettingsFromEncodedState(this._mapSerializedState.mapSettings));
|
||||
} else if (this._attributes?.mapStateJSON) {
|
||||
try {
|
||||
const mapState = JSON.parse(this._attributes.mapStateJSON) as ParsedMapStateJSON;
|
||||
|
@ -176,8 +179,8 @@ export class SavedMap {
|
|||
}
|
||||
|
||||
let isLayerTOCOpen = DEFAULT_IS_LAYER_TOC_OPEN;
|
||||
if (this._mapEmbeddableInput && this._mapEmbeddableInput.isLayerTOCOpen !== undefined) {
|
||||
isLayerTOCOpen = this._mapEmbeddableInput.isLayerTOCOpen;
|
||||
if (this._mapSerializedState?.isLayerTOCOpen !== undefined) {
|
||||
isLayerTOCOpen = this._mapSerializedState.isLayerTOCOpen;
|
||||
} else if (this._attributes?.uiStateJSON) {
|
||||
try {
|
||||
const uiState = JSON.parse(this._attributes.uiStateJSON) as ParsedUiStateJSON;
|
||||
|
@ -191,8 +194,8 @@ export class SavedMap {
|
|||
this._store.dispatch(setIsLayerTOCOpen(isLayerTOCOpen));
|
||||
|
||||
let openTOCDetails: string[] = [];
|
||||
if (this._mapEmbeddableInput && this._mapEmbeddableInput.openTOCDetails !== undefined) {
|
||||
openTOCDetails = this._mapEmbeddableInput.openTOCDetails;
|
||||
if (this._mapSerializedState?.openTOCDetails !== undefined) {
|
||||
openTOCDetails = this._mapSerializedState.openTOCDetails;
|
||||
} else if (this._attributes?.uiStateJSON) {
|
||||
try {
|
||||
const uiState = JSON.parse(this._attributes.uiStateJSON) as ParsedUiStateJSON;
|
||||
|
@ -205,12 +208,12 @@ export class SavedMap {
|
|||
}
|
||||
this._store.dispatch(setOpenTOCDetails(openTOCDetails));
|
||||
|
||||
if (this._mapEmbeddableInput && this._mapEmbeddableInput.mapCenter !== undefined) {
|
||||
if (this._mapSerializedState?.mapCenter !== undefined) {
|
||||
this._store.dispatch(
|
||||
setGotoWithCenter({
|
||||
lat: this._mapEmbeddableInput.mapCenter.lat,
|
||||
lon: this._mapEmbeddableInput.mapCenter.lon,
|
||||
zoom: this._mapEmbeddableInput.mapCenter.zoom,
|
||||
lat: this._mapSerializedState.mapCenter.lat,
|
||||
lon: this._mapSerializedState.mapCenter.lon,
|
||||
zoom: this._mapSerializedState.mapCenter.zoom,
|
||||
})
|
||||
);
|
||||
} else if (this._attributes?.mapStateJSON) {
|
||||
|
@ -245,8 +248,8 @@ export class SavedMap {
|
|||
}
|
||||
}
|
||||
this._store.dispatch<any>(replaceLayerList(layerList));
|
||||
if (this._mapEmbeddableInput && this._mapEmbeddableInput.hiddenLayers !== undefined) {
|
||||
this._store.dispatch<any>(setHiddenLayers(this._mapEmbeddableInput.hiddenLayers));
|
||||
if (this._mapSerializedState?.hiddenLayers !== undefined) {
|
||||
this._store.dispatch<any>(setHiddenLayers(this._mapSerializedState.hiddenLayers));
|
||||
}
|
||||
this._initialLayerListConfig = copyPersistentState(layerList);
|
||||
|
||||
|
@ -283,7 +286,7 @@ export class SavedMap {
|
|||
}
|
||||
|
||||
private _getPageTitle(): string {
|
||||
if (!this._mapEmbeddableInput) {
|
||||
if (!this._mapSerializedState) {
|
||||
return i18n.translate('xpack.maps.breadcrumbsCreate', {
|
||||
defaultMessage: 'Create',
|
||||
});
|
||||
|
@ -354,8 +357,8 @@ export class SavedMap {
|
|||
}
|
||||
|
||||
public getSavedObjectId(): string | undefined {
|
||||
return this._mapEmbeddableInput && 'savedObjectId' in this._mapEmbeddableInput
|
||||
? (this._mapEmbeddableInput as MapByReferenceInput).savedObjectId
|
||||
return this._mapSerializedState?.savedObjectId
|
||||
? this._mapSerializedState.savedObjectId
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
@ -404,11 +407,8 @@ export class SavedMap {
|
|||
}
|
||||
|
||||
public getAutoFitToBounds(): boolean {
|
||||
if (
|
||||
this._mapEmbeddableInput &&
|
||||
this._mapEmbeddableInput?.mapSettings?.autoFitToDataBounds !== undefined
|
||||
) {
|
||||
return this._mapEmbeddableInput.mapSettings.autoFitToDataBounds;
|
||||
if (this._mapSerializedState?.mapSettings?.autoFitToDataBounds !== undefined) {
|
||||
return this._mapSerializedState.mapSettings.autoFitToDataBounds;
|
||||
}
|
||||
|
||||
if (!this._attributes || !this._attributes.mapStateJSON) {
|
||||
|
@ -445,13 +445,13 @@ export class SavedMap {
|
|||
newTitle,
|
||||
newCopyOnSave,
|
||||
returnToOrigin,
|
||||
newTags,
|
||||
tags,
|
||||
saveByReference,
|
||||
dashboardId,
|
||||
history,
|
||||
}: OnSaveProps & {
|
||||
returnToOrigin?: boolean;
|
||||
newTags?: string[];
|
||||
tags?: string[];
|
||||
saveByReference: boolean;
|
||||
dashboardId?: string | null;
|
||||
history: ScopedHistory;
|
||||
|
@ -462,36 +462,44 @@ export class SavedMap {
|
|||
|
||||
const prevTitle = this._attributes.title;
|
||||
const prevDescription = this._attributes.description;
|
||||
const prevTags = this._tags;
|
||||
this._attributes.title = newTitle;
|
||||
this._attributes.description = newDescription;
|
||||
if (newTags) {
|
||||
this._tags = newTags;
|
||||
}
|
||||
await this._syncAttributesWithStore();
|
||||
|
||||
let updatedMapEmbeddableInput: MapEmbeddableInput;
|
||||
try {
|
||||
const savedObjectsTagging = getSavedObjectsTagging();
|
||||
// Attribute service deviates from Saved Object client by including references as a child to attributes in stead of a sibling
|
||||
const attributes =
|
||||
savedObjectsTagging && newTags
|
||||
? {
|
||||
...this._attributes,
|
||||
references: savedObjectsTagging.ui.updateTagsReferences([], newTags),
|
||||
}
|
||||
: this._attributes;
|
||||
updatedMapEmbeddableInput = (await getMapAttributeService().wrapAttributes(
|
||||
attributes,
|
||||
saveByReference,
|
||||
newCopyOnSave ? undefined : this._mapEmbeddableInput
|
||||
)) as MapEmbeddableInput;
|
||||
} catch (e) {
|
||||
// Error toast displayed by wrapAttributes
|
||||
this._attributes.title = prevTitle;
|
||||
this._attributes.description = prevDescription;
|
||||
this._tags = prevTags;
|
||||
return;
|
||||
let mapSerializedState: MapSerializedState | undefined;
|
||||
if (saveByReference) {
|
||||
try {
|
||||
const { attributes, references } = extractReferences({
|
||||
attributes: this._attributes,
|
||||
});
|
||||
const savedObjectsTagging = getSavedObjectsTagging();
|
||||
const tagReferences =
|
||||
savedObjectsTagging && tags ? savedObjectsTagging.ui.updateTagsReferences([], tags) : [];
|
||||
const { id: savedObjectId } = await saveToLibrary(
|
||||
attributes,
|
||||
[...references, ...tagReferences],
|
||||
newCopyOnSave ? undefined : this._mapSerializedState?.savedObjectId
|
||||
);
|
||||
mapSerializedState = getByReferenceState(this._mapSerializedState, savedObjectId);
|
||||
} catch (e) {
|
||||
this._attributes.title = prevTitle;
|
||||
this._attributes.description = prevDescription;
|
||||
getToasts().addDanger({
|
||||
title: i18n.translate('xpack.maps.saveToLibraryError', {
|
||||
defaultMessage: `An error occurred while saving. Error: {errorMessage}`,
|
||||
values: {
|
||||
errorMessage: e.message,
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
mapSerializedState = getByValueState(this._mapSerializedState, this._attributes);
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
this._tags = tags;
|
||||
}
|
||||
|
||||
if (returnToOrigin) {
|
||||
|
@ -511,7 +519,7 @@ export class SavedMap {
|
|||
state: {
|
||||
embeddableId: newCopyOnSave ? undefined : this._embeddableId,
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
input: updatedMapEmbeddableInput,
|
||||
input: mapSerializedState,
|
||||
},
|
||||
path: this._originatingPath,
|
||||
});
|
||||
|
@ -520,14 +528,14 @@ export class SavedMap {
|
|||
await this._getStateTransfer().navigateToWithEmbeddablePackage('dashboards', {
|
||||
state: {
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
input: updatedMapEmbeddableInput,
|
||||
input: mapSerializedState,
|
||||
},
|
||||
path: dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._mapEmbeddableInput = updatedMapEmbeddableInput;
|
||||
this._mapSerializedState = mapSerializedState;
|
||||
// break connection to originating application
|
||||
this._originatingApp = undefined;
|
||||
|
||||
|
|
|
@ -150,15 +150,15 @@ export function getTopNavConfig({
|
|||
}
|
||||
},
|
||||
run: () => {
|
||||
let selectedTags = savedMap.getTags();
|
||||
function onTagsSelected(newTags: string[]) {
|
||||
selectedTags = newTags;
|
||||
let tags = savedMap.getTags();
|
||||
function onTagsSelected(nextTags: string[]) {
|
||||
tags = nextTags;
|
||||
}
|
||||
|
||||
const savedObjectsTagging = getSavedObjectsTagging();
|
||||
const tagSelector = savedObjectsTagging ? (
|
||||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={selectedTags}
|
||||
initialSelection={tags}
|
||||
onTagsSelected={onTagsSelected}
|
||||
markOptional
|
||||
/>
|
||||
|
@ -193,7 +193,7 @@ export function getTopNavConfig({
|
|||
|
||||
await savedMap.save({
|
||||
...props,
|
||||
newTags: selectedTags,
|
||||
tags,
|
||||
saveByReference: props.addToLibrary,
|
||||
history,
|
||||
});
|
||||
|
|
|
@ -373,10 +373,6 @@ export function getLayerById(layerId: string | null, state: MapStoreState): ILay
|
|||
});
|
||||
}
|
||||
|
||||
export const getHiddenLayerIds = createSelector(getLayerListRaw, (layers) =>
|
||||
layers.filter((layer) => !layer.visible).map((layer) => layer.id)
|
||||
);
|
||||
|
||||
export const getSelectedLayer = createSelector(
|
||||
getSelectedLayerId,
|
||||
getLayerList,
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
import { createReactOverlays } from '@kbn/kibana-react-plugin/public';
|
||||
import { mapEmbeddablesSingleton } from '../../embeddable/map_embeddables_singleton';
|
||||
import { mapEmbeddablesSingleton } from '../../react_embeddable/map_embeddables_singleton';
|
||||
import { getCore } from '../../kibana_services';
|
||||
|
||||
export function openModal(title: string) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { apiHasVisualizeConfig } from '@kbn/visualizations-plugin/public';
|
|||
import { isLensApi } from '@kbn/lens-plugin/public';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants';
|
||||
import { isLegacyMapApi } from '../../legacy_visualizations/is_legacy_map';
|
||||
import { mapEmbeddablesSingleton } from '../../embeddable/map_embeddables_singleton';
|
||||
import { mapEmbeddablesSingleton } from '../../react_embeddable/map_embeddables_singleton';
|
||||
import type { SynchronizeMovementActionApi } from './types';
|
||||
|
||||
export function isCompatible(api: SynchronizeMovementActionApi) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
import { createReactOverlays } from '@kbn/kibana-react-plugin/public';
|
||||
import { mapEmbeddablesSingleton } from '../../embeddable/map_embeddables_singleton';
|
||||
import { mapEmbeddablesSingleton } from '../../react_embeddable/map_embeddables_singleton';
|
||||
import { getCore } from '../../kibana_services';
|
||||
|
||||
export function openModal() {
|
||||
|
|
|
@ -84,11 +84,12 @@
|
|||
"@kbn/code-editor",
|
||||
"@kbn/managed-content-badge",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
"@kbn/esql-utils",
|
||||
"@kbn/apm-data-view",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/embeddable-enhanced-plugin",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/field-utils"
|
||||
],
|
||||
"exclude": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue