diff --git a/x-pack/plugins/maps/common/constants.js b/x-pack/plugins/maps/common/constants.js new file mode 100644 index 0000000000000..2570341aa5756 --- /dev/null +++ b/x-pack/plugins/maps/common/constants.js @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const EMS_CATALOGUE_PATH = 'ems/catalogue'; + +export const EMS_FILES_CATALOGUE_PATH = 'ems/files'; +export const EMS_FILES_API_PATH = 'ems/files'; +export const EMS_FILES_DEFAULT_JSON_PATH = 'file'; +export const EMS_GLYPHS_PATH = 'fonts'; +export const EMS_SPRITES_PATH = 'sprites'; + +export const EMS_TILES_CATALOGUE_PATH = 'ems/tiles'; +export const EMS_TILES_API_PATH = 'ems/tiles'; +export const EMS_TILES_RASTER_STYLE_PATH = 'raster/style'; +export const EMS_TILES_RASTER_TILE_PATH = 'raster/tile'; + +export const EMS_TILES_VECTOR_STYLE_PATH = 'vector/style'; +export const EMS_TILES_VECTOR_SOURCE_PATH = 'vector/source'; +export const EMS_TILES_VECTOR_TILE_PATH = 'vector/tile'; + +export const MAP_SAVED_OBJECT_TYPE = 'map'; +export const APP_ID = 'maps'; +export const APP_ICON = 'gisApp'; +export const TELEMETRY_TYPE = 'maps-telemetry'; + +export const MAP_APP_PATH = `app/${APP_ID}`; +export const GIS_API_PATH = `api/${APP_ID}`; +export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; + +export const MAP_BASE_URL = `/${MAP_APP_PATH}#/${MAP_SAVED_OBJECT_TYPE}`; + +export function createMapPath(id) { + return `${MAP_BASE_URL}/${id}`; +} + +export const LAYER_TYPE = { + TILE: 'TILE', + VECTOR: 'VECTOR', + VECTOR_TILE: 'VECTOR_TILE', + HEATMAP: 'HEATMAP', +}; + +export const SORT_ORDER = { + ASC: 'asc', + DESC: 'desc', +}; + +export const EMS_TMS = 'EMS_TMS'; +export const EMS_FILE = 'EMS_FILE'; +export const ES_GEO_GRID = 'ES_GEO_GRID'; +export const ES_SEARCH = 'ES_SEARCH'; +export const ES_PEW_PEW = 'ES_PEW_PEW'; + +export const FIELD_ORIGIN = { + SOURCE: 'source', + JOIN: 'join', +}; + +export const SOURCE_DATA_ID_ORIGIN = 'source'; +export const META_ID_ORIGIN_SUFFIX = 'meta'; +export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; +export const FORMATTERS_ID_ORIGIN_SUFFIX = 'formatters'; +export const SOURCE_FORMATTERS_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; + +export const GEOJSON_FILE = 'GEOJSON_FILE'; + +export const MIN_ZOOM = 0; +export const MAX_ZOOM = 24; + +export const DECIMAL_DEGREES_PRECISION = 5; // meters precision +export const ZOOM_PRECISION = 2; +export const DEFAULT_MAX_RESULT_WINDOW = 10000; +export const DEFAULT_MAX_INNER_RESULT_WINDOW = 100; +export const DEFAULT_MAX_BUCKETS_LIMIT = 10000; + +export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; +export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; + +export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; + +export const ES_GEO_FIELD_TYPE = { + GEO_POINT: 'geo_point', + GEO_SHAPE: 'geo_shape', +}; + +export const ES_SPATIAL_RELATIONS = { + INTERSECTS: 'INTERSECTS', + DISJOINT: 'DISJOINT', + WITHIN: 'WITHIN', +}; + +export const GEO_JSON_TYPE = { + POINT: 'Point', + MULTI_POINT: 'MultiPoint', + LINE_STRING: 'LineString', + MULTI_LINE_STRING: 'MultiLineString', + POLYGON: 'Polygon', + MULTI_POLYGON: 'MultiPolygon', + GEOMETRY_COLLECTION: 'GeometryCollection', +}; + +export const POLYGON_COORDINATES_EXTERIOR_INDEX = 0; +export const LON_INDEX = 0; +export const LAT_INDEX = 1; + +export const EMPTY_FEATURE_COLLECTION = { + type: 'FeatureCollection', + features: [], +}; + +export const DRAW_TYPE = { + BOUNDS: 'BOUNDS', + POLYGON: 'POLYGON', +}; + +export const METRIC_TYPE = { + AVG: 'avg', + COUNT: 'count', + MAX: 'max', + MIN: 'min', + SUM: 'sum', + UNIQUE_COUNT: 'cardinality', +}; + +export const COUNT_AGG_TYPE = METRIC_TYPE.COUNT; +export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { + defaultMessage: 'count', +}); + +export const COUNT_PROP_NAME = 'doc_count'; + +export const STYLE_TYPE = { + STATIC: 'STATIC', + DYNAMIC: 'DYNAMIC', +}; + +export const LAYER_STYLE_TYPE = { + VECTOR: 'VECTOR', + HEATMAP: 'HEATMAP', +}; + +export const COLOR_MAP_TYPE = { + CATEGORICAL: 'CATEGORICAL', + ORDINAL: 'ORDINAL', +}; + +export const COLOR_PALETTE_MAX_SIZE = 10; + +export const CATEGORICAL_DATA_TYPES = ['string', 'ip', 'boolean']; + +export const SYMBOLIZE_AS_TYPES = { + CIRCLE: 'circle', + ICON: 'icon', +}; + +export const DEFAULT_ICON = 'airfield'; diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json new file mode 100644 index 0000000000000..43bf06261064e --- /dev/null +++ b/x-pack/plugins/maps/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "maps", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "maps"], + "requiredPlugins": ["embeddable", "data"], + "ui": true +} diff --git a/x-pack/plugins/maps/public/embeddable/README.md b/x-pack/plugins/maps/public/embeddable/README.md new file mode 100644 index 0000000000000..1de327702fb87 --- /dev/null +++ b/x-pack/plugins/maps/public/embeddable/README.md @@ -0,0 +1,182 @@ + +### Map specific `input` parameters +- **hideFilterActions:** (Boolean) Set to true to hide all filtering controls. +- **isLayerTOCOpen:** (Boolean) Set to false to render map with legend in collapsed state. +- **openTOCDetails:** (Array of Strings) Array of layer ids. Add layer id to show layer details on initial render. +- **mapCenter:** ({lat, lon, zoom }) Provide mapCenter to customize initial map location. +- **disableInteractive:** (Boolean) Will disable map interactions, panning, zooming in the map. +- **disableTooltipControl:** (Boolean) Will disable tooltip which shows relevant information on hover, like Continent name etc +- **hideToolbarOverlay:** (Boolean) Will disable toolbar, which can be used to navigate to coordinate by entering lat/long and zoom values. +- **hideLayerControl:** (Boolean) Will hide useful layer control, which can be used to hide/show a layer to get a refined view of the map. +- **hideViewControl:** (Boolean) Will hide view control at bottom right of the map, which shows lat/lon values based on mouse hover in the map, this is useful to get coordinate value from a particular point in map. +- **hiddenLayers:** (Array of Strings) Array of layer ids that should be hidden. Any other layers will be set to visible regardless of their value in the layerList used to initialize the embeddable + +### Creating a Map embeddable from saved object +``` + const factory = new MapEmbeddableFactory(); + const input = { + hideFilterActions: true, + isLayerTOCOpen: false, + openTOCDetails: ['tfi3f', 'edh66'], + mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 } + } + const mapEmbeddable = await factory.createFromSavedObject( + 'de71f4f0-1902-11e9-919b-ffe5949a18d2', + input, + parent + ); +``` + +### Creating a Map embeddable from state +``` +const factory = new MapEmbeddableFactory(); +const state = { + layerList: [], // where layerList is same as saved object layerListJSON property (unstringified) + title: 'my map', +} +const input = { + hideFilterActions: true, + isLayerTOCOpen: false, + openTOCDetails: ['tfi3f', 'edh66'], + mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 } +} +const mapEmbeddable = await factory.createFromState(state, input, parent); +``` + +#### Customize tooltip +``` +/** + * Render custom tooltip content + * + * @param {function} addFilters + * @param {function} closeTooltip + * @param {Array} features - Vector features at tooltip location. + * @param {boolean} isLocked + * @param {function} getLayerName - Get layer name. Call with (layerId). Returns Promise. + * @param {function} loadFeatureProperties - Loads feature properties. Call with ({ layerId, featureId }). Returns Promise. + * @param {function} loadFeatureGeometry - Loads feature geometry. Call with ({ layerId, featureId }). Returns geojson geometry object { type, coordinates }. + * + * @return {Component} A React Component. + */ +const renderTooltipContent = ({ addFilters, closeTooltip, features, isLocked, loadFeatureProperties}) => { + return
Custom tooltip content
; +} + +const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent); +``` + + +#### Event handlers +``` +const eventHandlers = { + onDataLoad: (layerId: string, dataId: string) => { + // take action on data load + }, + onDataLoadEnd: (layerId: string, dataId: string, resultMeta: object) => { + // take action on data load end + }, + onDataLoadError: (layerId: string, dataId: string, errorMessage: string) => { + // take action on data load error + }, +} + +const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent, eventHandlers); +``` + + +#### Passing in geospatial data +You can pass geospatial data into the Map embeddable by configuring the layerList parameter with a layer with `GEOJSON_FILE` source. +Geojson sources will not update unless you modify `__featureCollection` property by calling the `setLayerList` method. + +``` +const factory = new MapEmbeddableFactory(); +const state = { + layerList: [ + { + 'id': 'gaxya', + 'label': 'My geospatial data', + 'minZoom': 0, + 'maxZoom': 24, + 'alpha': 1, + 'sourceDescriptor': { + 'id': 'b7486', + 'type': 'GEOJSON_FILE', + '__featureCollection': { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], [10, 10], [10, 0], [0, 0] + ] + ] + }, + "properties": { + "name": "null island", + "another_prop": "something else interesting" + } + } + ] + } + }, + 'visible': true, + 'style': { + 'type': 'VECTOR', + 'properties': {} + }, + 'type': 'VECTOR' + } + ], + title: 'my map', +} +const input = { + hideFilterActions: true, + isLayerTOCOpen: false, + openTOCDetails: ['tfi3f', 'edh66'], + mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 } +} +const mapEmbeddable = await factory.createFromState(state, input, parent); + +mapEmbeddable.setLayerList([ + { + 'id': 'gaxya', + 'label': 'My geospatial data', + 'minZoom': 0, + 'maxZoom': 24, + 'alpha': 1, + 'sourceDescriptor': { + 'id': 'b7486', + 'type': 'GEOJSON_FILE', + '__featureCollection': { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [35, 35], [45, 45], [45, 35], [35, 35] + ] + ] + }, + "properties": { + "name": "null island", + "another_prop": "something else interesting" + } + } + ] + } + }, + 'visible': true, + 'style': { + 'type': 'VECTOR', + 'properties': {} + }, + 'type': 'VECTOR' + } +]); +``` diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/plugins/maps/public/embeddable/map_embeddable.js new file mode 100644 index 0000000000000..c723e996ee679 --- /dev/null +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.js @@ -0,0 +1,260 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { render, unmountComponentAtNode } from 'react-dom'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +import { + Embeddable, + APPLY_FILTER_TRIGGER, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { onlyDisabledFiltersChanged } from '../../../../../../src/plugins/data/public'; + +import { I18nContext } from 'ui/i18n'; + +import { GisMap } from '../connected_components/gis_map'; +import { createMapStore } from '../reducers/store'; +import { npStart } from 'ui/new_platform'; +import { + setGotoWithCenter, + replaceLayerList, + setQuery, + setRefreshConfig, + disableScrollZoom, + disableInteractive, + disableTooltipControl, + hideToolbarOverlay, + hideLayerControl, + hideViewControl, + setHiddenLayers, +} from '../actions/map_actions'; +import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; +import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; +import { getInspectorAdapters, setEventHandlers } from '../reducers/non_serializable_instances'; +import { getMapCenter, getMapZoom, getHiddenLayerIds } from '../selectors/map_selectors'; +import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; + +export class MapEmbeddable extends Embeddable { + type = MAP_SAVED_OBJECT_TYPE; + + constructor(config, initialInput, parent, renderTooltipContent, eventHandlers) { + super( + initialInput, + { + editUrl: config.editUrl, + indexPatterns: config.indexPatterns, + editable: config.editable, + defaultTitle: config.title, + }, + parent + ); + + this._renderTooltipContent = renderTooltipContent; + this._eventHandlers = eventHandlers; + this._layerList = config.layerList; + this._store = createMapStore(); + + this._subscription = this.getInput$().subscribe(input => this.onContainerStateChanged(input)); + } + + getInspectorAdapters() { + return getInspectorAdapters(this._store.getState()); + } + + onContainerStateChanged(containerState) { + if ( + !_.isEqual(containerState.timeRange, this._prevTimeRange) || + !_.isEqual(containerState.query, this._prevQuery) || + !onlyDisabledFiltersChanged(containerState.filters, this._prevFilters) + ) { + this._dispatchSetQuery(containerState); + } + + if (!_.isEqual(containerState.refreshConfig, this._prevRefreshConfig)) { + this._dispatchSetRefreshConfig(containerState); + } + } + + _dispatchSetQuery({ query, timeRange, filters, refresh }) { + this._prevTimeRange = timeRange; + this._prevQuery = query; + this._prevFilters = filters; + this._store.dispatch( + setQuery({ + filters: filters.filter(filter => !filter.meta.disabled), + query, + timeFilters: timeRange, + refresh, + }) + ); + } + + _dispatchSetRefreshConfig({ refreshConfig }) { + this._prevRefreshConfig = refreshConfig; + this._store.dispatch( + setRefreshConfig({ + isPaused: refreshConfig.pause, + interval: refreshConfig.value, + }) + ); + } + + /** + * + * @param {HTMLElement} domNode + * @param {ContainerState} containerState + */ + render(domNode) { + this._store.dispatch(setEventHandlers(this._eventHandlers)); + this._store.dispatch(setReadOnly(true)); + this._store.dispatch(disableScrollZoom()); + + if (_.has(this.input, 'isLayerTOCOpen')) { + this._store.dispatch(setIsLayerTOCOpen(this.input.isLayerTOCOpen)); + } + + if (_.has(this.input, 'openTOCDetails')) { + this._store.dispatch(setOpenTOCDetails(this.input.openTOCDetails)); + } + + if (_.has(this.input, 'disableInteractive') && this.input.disableInteractive) { + this._store.dispatch(disableInteractive(this.input.disableInteractive)); + } + + if (_.has(this.input, 'disableTooltipControl') && this.input.disableTooltipControl) { + this._store.dispatch(disableTooltipControl(this.input.disableTooltipControl)); + } + + if (_.has(this.input, 'hideToolbarOverlay') && this.input.hideToolbarOverlay) { + this._store.dispatch(hideToolbarOverlay(this.input.hideToolbarOverlay)); + } + + if (_.has(this.input, 'hideLayerControl') && this.input.hideLayerControl) { + this._store.dispatch(hideLayerControl(this.input.hideLayerControl)); + } + + if (_.has(this.input, 'hideViewControl') && this.input.hideViewControl) { + this._store.dispatch(hideViewControl(this.input.hideViewControl)); + } + + if (this.input.mapCenter) { + this._store.dispatch( + setGotoWithCenter({ + lat: this.input.mapCenter.lat, + lon: this.input.mapCenter.lon, + zoom: this.input.mapCenter.zoom, + }) + ); + } + + this._store.dispatch(replaceLayerList(this._layerList)); + if (this.input.hiddenLayers) { + this._store.dispatch(setHiddenLayers(this.input.hiddenLayers)); + } + this._dispatchSetQuery(this.input); + this._dispatchSetRefreshConfig(this.input); + + this._domNode = domNode; + + render( + + + + + , + this._domNode + ); + + this._unsubscribeFromStore = this._store.subscribe(() => { + this._handleStoreChanges(); + }); + } + + async setLayerList(layerList) { + this._layerList = layerList; + return await this._store.dispatch(replaceLayerList(this._layerList)); + } + + addFilters = filters => { + npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters, + }); + }; + + destroy() { + super.destroy(); + if (this._unsubscribeFromStore) { + this._unsubscribeFromStore(); + } + + if (this._domNode) { + unmountComponentAtNode(this._domNode); + } + + if (this._subscription) { + this._subscription.unsubscribe(); + } + } + + reload() { + this._dispatchSetQuery({ + query: this._prevQuery, + timeRange: this._prevTimeRange, + filters: this._prevFilters, + refresh: true, + }); + } + + _handleStoreChanges() { + const center = getMapCenter(this._store.getState()); + const zoom = getMapZoom(this._store.getState()); + + const mapCenter = this.input.mapCenter || {}; + if ( + !mapCenter || + mapCenter.lat !== center.lat || + mapCenter.lon !== center.lon || + mapCenter.zoom !== zoom + ) { + this.updateInput({ + mapCenter: { + lat: center.lat, + lon: center.lon, + zoom: zoom, + }, + }); + } + + const isLayerTOCOpen = getIsLayerTOCOpen(this._store.getState()); + if (this.input.isLayerTOCOpen !== isLayerTOCOpen) { + this.updateInput({ + isLayerTOCOpen, + }); + } + + const openTOCDetails = getOpenTOCDetails(this._store.getState()); + if (!_.isEqual(this.input.openTOCDetails, openTOCDetails)) { + this.updateInput({ + openTOCDetails, + }); + } + + const hiddenLayerIds = getHiddenLayerIds(this._store.getState()); + + if (!_.isEqual(this.input.hiddenLayers, hiddenLayerIds)) { + this.updateInput({ + hiddenLayers: hiddenLayerIds, + }); + } + } +} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js new file mode 100644 index 0000000000000..91f5a6ee75965 --- /dev/null +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; +import { + EmbeddableFactory, + ErrorEmbeddable, +} from '../../../../../src/plugins/embeddable/public'; +import { MapEmbeddable } from './map_embeddable'; +import { indexPatternService } from '../kibana_services'; + +import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; +import { createMapStore } from '../reducers/store'; +import { addLayerWithoutDataSync } from '../actions/map_actions'; +import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; +import { getInitialLayers } from '../angular/get_initial_layers'; +import { mergeInputWithSavedMap } from './merge_input_with_saved_map'; +import '../angular/services/gis_map_saved_object_loader'; + +export class MapEmbeddableFactory extends EmbeddableFactory { + type = MAP_SAVED_OBJECT_TYPE; + isEditable; + + constructor(isEditable) { + super({ + savedObjectMetaData: { + name: i18n.translate('xpack.maps.mapSavedObjectLabel', { + defaultMessage: 'Map', + }), + type: MAP_SAVED_OBJECT_TYPE, + getIconForSavedObject: () => APP_ICON, + }, + }); + this.isEditable = isEditable; + } + + // Not supported yet for maps types. + canCreateNew() { + return false; + } + + getDisplayName() { + return i18n.translate('xpack.maps.embeddableDisplayName', { + defaultMessage: 'map', + }); + } + + async _getIndexPatterns(layerList) { + // Need to extract layerList from store to get queryable index pattern ids + const store = createMapStore(); + let queryableIndexPatternIds; + try { + layerList.forEach(layerDescriptor => { + store.dispatch(addLayerWithoutDataSync(layerDescriptor)); + }); + queryableIndexPatternIds = getQueryableUniqueIndexPatternIds(store.getState()); + } catch (error) { + throw new Error( + i18n.translate('xpack.maps.mapEmbeddableFactory.invalidLayerList', { + defaultMessage: 'Unable to load map, malformed layer list', + }) + ); + } + + const promises = queryableIndexPatternIds.map(async indexPatternId => { + try { + return await indexPatternService.get(indexPatternId); + } catch (error) { + // Unable to load index pattern, better to not throw error so map embeddable can render + // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern + return null; + } + }); + const indexPatterns = await Promise.all(promises); + return _.compact(indexPatterns); + } + + async _fetchSavedMap(savedObjectId) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const savedObjectLoader = $injector.get('gisMapSavedObjectLoader'); + return await savedObjectLoader.get(savedObjectId); + } + + async createFromSavedObject(savedObjectId, input, parent) { + const savedMap = await this._fetchSavedMap(savedObjectId); + const layerList = getInitialLayers(savedMap.layerListJSON); + const indexPatterns = await this._getIndexPatterns(layerList); + + const embeddable = new MapEmbeddable( + { + layerList, + title: savedMap.title, + editUrl: chrome.addBasePath(createMapPath(savedObjectId)), + indexPatterns, + editable: this.isEditable(), + }, + input, + parent + ); + + try { + embeddable.updateInput(mergeInputWithSavedMap(input, savedMap)); + } catch (error) { + throw new Error( + i18n.translate('xpack.maps.mapEmbeddableFactory.invalidSavedObject', { + defaultMessage: 'Unable to load map, malformed saved object', + }) + ); + } + + return embeddable; + } + + async createFromState(state, input, parent, renderTooltipContent, eventHandlers) { + const layerList = state && state.layerList ? state.layerList : getInitialLayers(); + const indexPatterns = await this._getIndexPatterns(layerList); + + return new MapEmbeddable( + { + layerList, + title: state && state.title ? state.title : '', + editUrl: null, + indexPatterns, + editable: false, + }, + input, + parent, + renderTooltipContent, + eventHandlers + ); + } + + async create(input) { + window.location.href = chrome.addBasePath(createMapPath('')); + return new ErrorEmbeddable( + 'Maps can only be created with createFromSavedObject or createFromState', + input + ); + } +} diff --git a/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js new file mode 100644 index 0000000000000..935747da93687 --- /dev/null +++ b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; + +const MAP_EMBEDDABLE_INPUT_KEYS = [ + 'hideFilterActions', + 'isLayerTOCOpen', + 'openTOCDetails', + 'mapCenter', +]; + +export function mergeInputWithSavedMap(input, savedMap) { + const mergedInput = _.pick(input, MAP_EMBEDDABLE_INPUT_KEYS); + + if (!_.has(input, 'isLayerTOCOpen') && savedMap.uiStateJSON) { + const uiState = JSON.parse(savedMap.uiStateJSON); + mergedInput.isLayerTOCOpen = _.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN); + } + + if (!_.has(input, 'openTOCDetails') && savedMap.uiStateJSON) { + const uiState = JSON.parse(savedMap.uiStateJSON); + if (_.has(uiState, 'openTOCDetails')) { + mergedInput.openTOCDetails = _.get(uiState, 'openTOCDetails', []); + } + } + + if (!input.mapCenter && savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); + mergedInput.mapCenter = { + lat: mapState.center.lat, + lon: mapState.center.lon, + zoom: mapState.zoom, + }; + } + + return mergedInput; +} diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts new file mode 100644 index 0000000000000..ff79c001c7d6e --- /dev/null +++ b/x-pack/plugins/maps/public/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + MapsPlugin, + MapsPluginSetup, + MapsPluginStart, +} from './plugin'; +import {PluginInitializer, PluginInitializerContext} from 'kibana/public'; + +export const plugin: PluginInitializer = ( + context: PluginInitializerContext +) => { + return new MapsPlugin(context); +}; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js new file mode 100644 index 0000000000000..61825755e257f --- /dev/null +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export let indexPatternService; + +export const initKibanaServices = ({ data }) => { + indexPatternService = data.indexPatterns; +}; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts new file mode 100644 index 0000000000000..caff0098522d8 --- /dev/null +++ b/x-pack/plugins/maps/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { IEmbeddableSetup } from '../../../../src/plugins/embeddable/public'; +// @ts-ignore +import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory.js'; +// @ts-ignore +import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { initKibanaServices } from './kibana_services'; +import {auto} from "angular"; + +export interface MapsPluginSetupDependencies { + embeddable: IEmbeddableSetup; +} +// eslint-disable-line @typescript-eslint/no-empty-interface +export interface MapsPluginStartDependencies {} + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type MapsPluginSetup = ReturnType; +export type MapsPluginStart = ReturnType; + +/** @internal */ +export class MapsPlugin implements + Plugin< + MapsPluginSetup, + MapsPluginStart, + MapsPluginSetupDependencies, + MapsPluginStartDependencies + > { + private getEmbeddableInjector: (() => Promise) | null = null; + constructor(context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { + initKibanaServices(plugins); + + // Set up embeddables + const isEditable = () => core.application.capabilities.get().maps.save as boolean; + if (!this.getEmbeddableInjector) { + throw Error('Maps plugin method getEmbeddableInjector is undefined'); + } + const factory = new MapEmbeddableFactory( + plugins.uiActions.executeTriggerActions, + this.getEmbeddableInjector, + isEditable + ); + plugins.embeddable.registerEmbeddableFactory(factory.type, factory); + plugins.embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); + } + + public start(core: CoreStart, plugins: any) { + // setInspector(plugins.np.inspector); + } +}