diff --git a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 63b9b48ec809..45592c8a703a 100644 --- a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { EuiFormRow, EuiDualRange } from '@elastic/eui'; import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; @@ -32,7 +32,7 @@ export type ValueMember = EuiDualRangeProps['value'][0]; interface Props extends Omit { value?: Value; allowEmptyRange?: boolean; - label?: string; + label?: string | ReactNode; formRowDisplay?: EuiFormRowDisplayKeys; onChange?: (val: [string, string]) => void; min?: number; diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 25f10c7794fd..98464427cc34 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -223,6 +223,11 @@ export enum SCALING_TYPES { export const RGBA_0000 = 'rgba(0,0,0,0)'; +export enum MVT_FIELD_TYPE { + STRING = 'String', + NUMBER = 'Number', +} + export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID'; export enum INITIAL_LOCATION { diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts similarity index 100% rename from x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts rename to x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts diff --git a/x-pack/plugins/maps/common/descriptor_types/index.ts b/x-pack/plugins/maps/common/descriptor_types/index.ts index af0f4487f471..b0ae065856a5 100644 --- a/x-pack/plugins/maps/common/descriptor_types/index.ts +++ b/x-pack/plugins/maps/common/descriptor_types/index.ts @@ -5,6 +5,6 @@ */ export * from './data_request_descriptor_types'; -export * from './descriptor_types'; +export * from './sources'; export * from './map_descriptor'; export * from './style_property_descriptor_types'; diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 00380ca12a48..027cc886cd7f 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -5,6 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { GeoJsonProperties } from 'geojson'; import { Query } from '../../../../../src/plugins/data/common'; import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; @@ -39,8 +40,9 @@ export type Goto = { }; export type TooltipFeature = { - id: number; + id?: number | string; layerId: string; + mbProperties: GeoJsonProperties; }; export type TooltipState = { diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/sources.ts similarity index 87% rename from x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts rename to x-pack/plugins/maps/common/descriptor_types/sources.ts index c7a706ea64f7..86ace0e32cc8 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/sources.ts @@ -7,7 +7,14 @@ import { FeatureCollection } from 'geojson'; import { Query } from 'src/plugins/data/public'; -import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; +import { + AGG_TYPE, + GRID_RESOLUTION, + RENDER_AS, + SORT_ORDER, + SCALING_TYPES, + MVT_FIELD_TYPE, +} from '../constants'; import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; @@ -96,18 +103,34 @@ export type XYZTMSSourceDescriptor = AbstractSourceDescriptor & urlTemplate: string; }; -export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & { +export type MVTFieldDescriptor = { + name: string; + type: MVT_FIELD_TYPE; +}; + +export type TiledSingleLayerVectorSourceSettings = { urlTemplate: string; layerName: string; // These are the min/max zoom levels of the availability of the a particular layerName in the tileset at urlTemplate. // These are _not_ the visible zoom-range of the data on a map. - // Tiled data can be displayed at higher levels of zoom than that they are stored in the tileset. - // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels + // These are important so mapbox does not issue invalid requests based on the zoom level. + + // Tiled layer data cannot be displayed at lower levels of zoom than that they are stored in the tileset. + // e.g. building footprints at level 14 cannot be displayed at level 0. minSourceZoom: number; + // Tiled layer data can be displayed at higher levels of zoom than that they are stored in the tileset. + // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels maxSourceZoom: number; + + fields: MVTFieldDescriptor[]; }; +export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & + TiledSingleLayerVectorSourceSettings & { + tooltipProperties: string[]; + }; + export type GeojsonFileSourceDescriptor = { __featureCollection: FeatureCollection; name: string; diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts similarity index 100% rename from x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts rename to x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index 60d437d2321b..e0f5c79f1d42 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -128,6 +128,10 @@ export class ESAggField implements IESAggField { async getCategoricalFieldMetaRequest(size: number): Promise { return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null; } + + supportsAutoDomain(): boolean { + return true; + } } export function esAggFieldsFactory( diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index dfd5dc05f7b8..410b38e79ffe 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -20,6 +20,12 @@ export interface IField { isValid(): boolean; getOrdinalFieldMetaRequest(): Promise; getCategoricalFieldMetaRequest(size: number): Promise; + + // Determines whether Maps-app can automatically determine the domain of the field-values + // if this is not the case (e.g. for .mvt tiled data), + // then styling properties that require the domain to be known cannot use this property. + supportsAutoDomain(): boolean; + supportsFieldMeta(): boolean; } @@ -80,4 +86,8 @@ export class AbstractField implements IField { async getCategoricalFieldMetaRequest(size: number): Promise { return null; } + + supportsAutoDomain(): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts new file mode 100644 index 000000000000..eb2bb94b36a6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts @@ -0,0 +1,59 @@ +/* + * 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 { AbstractField, IField } from './field'; +import { FIELD_ORIGIN, MVT_FIELD_TYPE } from '../../../common/constants'; +import { ITiledSingleLayerVectorSource, IVectorSource } from '../sources/vector_source'; +import { MVTFieldDescriptor } from '../../../common/descriptor_types'; + +export class MVTField extends AbstractField implements IField { + private readonly _source: ITiledSingleLayerVectorSource; + private readonly _type: MVT_FIELD_TYPE; + constructor({ + fieldName, + type, + source, + origin, + }: { + fieldName: string; + source: ITiledSingleLayerVectorSource; + origin: FIELD_ORIGIN; + type: MVT_FIELD_TYPE; + }) { + super({ fieldName, origin }); + this._source = source; + this._type = type; + } + + getMVTFieldDescriptor(): MVTFieldDescriptor { + return { + type: this._type, + name: this.getName(), + }; + } + + getSource(): IVectorSource { + return this._source; + } + + async getDataType(): Promise { + if (this._type === MVT_FIELD_TYPE.STRING) { + return 'string'; + } else if (this._type === MVT_FIELD_TYPE.NUMBER) { + return 'number'; + } else { + throw new Error(`Unrecognized MVT field-type ${this._type}`); + } + } + + async getLabel(): Promise { + return this.getName(); + } + + supportsAutoDomain() { + return false; + } +} diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts index 6c504daf3e19..f4625e42ab5d 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts @@ -60,6 +60,10 @@ export class TopTermPercentageField implements IESAggField { return 0; } + supportsAutoDomain(): boolean { + return true; + } + supportsFieldMeta(): boolean { return false; } diff --git a/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts b/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts new file mode 100644 index 000000000000..8c4eb49d5040 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts @@ -0,0 +1,40 @@ +/* + * 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 sinon from 'sinon'; +import { DataRequestContext } from '../../../actions'; +import { DataMeta, MapFilters } from '../../../../common/descriptor_types'; + +export class MockSyncContext implements DataRequestContext { + dataFilters: MapFilters; + isRequestStillActive: (dataId: string, requestToken: symbol) => boolean; + onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) => void; + registerCancelCallback: (requestToken: symbol, callback: () => void) => void; + startLoading: (dataId: string, requestToken: symbol, meta: DataMeta) => void; + stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataMeta) => void; + updateSourceData: (newData: unknown) => void; + + constructor({ dataFilters }: { dataFilters: Partial }) { + const mapFilters: MapFilters = { + filters: [], + timeFilters: { + from: 'now', + to: '15m', + mode: 'relative', + }, + zoom: 0, + ...dataFilters, + }; + + this.dataFilters = mapFilters; + this.isRequestStillActive = sinon.spy(); + this.onLoadError = sinon.spy(); + this.registerCancelCallback = sinon.spy(); + this.startLoading = sinon.spy(); + this.stopLoading = sinon.spy(); + this.updateSourceData = sinon.spy(); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 859d6092dc64..368dcda6b3a5 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -16,7 +16,7 @@ import { import { getFileUploadComponent } from '../../../kibana_services'; import { GeojsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; -// @ts-ignore +// @ts-expect-error import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js index f6b9bd628029..adcc86b9d154 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js @@ -91,7 +91,7 @@ export class HeatmapLayer extends VectorLayer { resolution: this.getSource().getGridResolution(), }); mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); - mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom()); } getLayerTypeIconName() { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 2250d5663378..e122d1cda3ed 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -325,27 +325,28 @@ export class AbstractLayer implements ILayer { return this._source.getMinZoom(); } + _getMbSourceId() { + return this.getId(); + } + _requiresPrevSourceCleanup(mbMap: unknown) { return false; } _removeStaleMbSourcesAndLayers(mbMap: unknown) { if (this._requiresPrevSourceCleanup(mbMap)) { - // @ts-ignore + // @ts-expect-error const mbStyle = mbMap.getStyle(); - // @ts-ignore + // @ts-expect-error mbStyle.layers.forEach((mbLayer) => { - // @ts-ignore if (this.ownsMbLayerId(mbLayer.id)) { - // @ts-ignore + // @ts-expect-error mbMap.removeLayer(mbLayer.id); } }); - // @ts-ignore Object.keys(mbStyle.sources).some((mbSourceId) => { - // @ts-ignore if (this.ownsMbSourceId(mbSourceId)) { - // @ts-ignore + // @ts-expect-error mbMap.removeSource(mbSourceId); } }); @@ -429,7 +430,7 @@ export class AbstractLayer implements ILayer { throw new Error('Should implement AbstractLayer#ownsMbLayerId'); } - ownsMbSourceId(sourceId: string): boolean { + ownsMbSourceId(mbSourceId: string): boolean { throw new Error('Should implement AbstractLayer#ownsMbSourceId'); } diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js index 02df8acbfffa..3e2009c24a2e 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js @@ -74,8 +74,8 @@ export class TileLayer extends AbstractLayer { return; } - const sourceId = this.getId(); - mbMap.addSource(sourceId, { + const mbSourceId = this._getMbSourceId(); + mbMap.addSource(mbSourceId, { type: 'raster', tiles: [tmsSourceData.url], tileSize: 256, @@ -85,7 +85,7 @@ export class TileLayer extends AbstractLayer { mbMap.addLayer({ id: mbLayerId, type: 'raster', - source: sourceId, + source: mbSourceId, minzoom: this._descriptor.minZoom, maxzoom: this._descriptor.maxZoom, }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap new file mode 100644 index 000000000000..f0ae93601ce8 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`icon should use vector icon 1`] = ` +
+`; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx new file mode 100644 index 000000000000..ecd625db3441 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -0,0 +1,163 @@ +/* + * 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 { MockSyncContext } from '../__tests__/mock_sync_context'; +import sinon from 'sinon'; + +jest.mock('../../../kibana_services', () => { + return { + getUiSettings() { + return { + get() { + return false; + }, + }; + }, + }; +}); + +import { shallow } from 'enzyme'; + +import { Feature } from 'geojson'; +import { MVTSingleLayerVectorSource } from '../../sources/mvt_single_layer_vector_source'; +import { + DataRequestDescriptor, + TiledSingleLayerVectorSourceDescriptor, + VectorLayerDescriptor, +} from '../../../../common/descriptor_types'; +import { SOURCE_TYPES } from '../../../../common/constants'; +import { TiledVectorLayer } from './tiled_vector_layer'; + +const defaultConfig = { + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, +}; + +function createLayer( + layerOptions: Partial = {}, + sourceOptions: Partial = {} +): TiledVectorLayer { + const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + ...defaultConfig, + fields: [], + tooltipProperties: [], + ...sourceOptions, + }; + const mvtSource = new MVTSingleLayerVectorSource(sourceDescriptor); + + const defaultLayerOptions = { + ...layerOptions, + sourceDescriptor, + }; + const layerDescriptor = TiledVectorLayer.createDescriptor(defaultLayerOptions); + return new TiledVectorLayer({ layerDescriptor, source: mvtSource }); +} + +describe('visiblity', () => { + it('should get minzoom from source', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + expect(layer.getMinZoom()).toEqual(4); + }); + it('should get maxzoom from default', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + expect(layer.getMaxZoom()).toEqual(24); + }); + it('should get maxzoom from layer options', async () => { + const layer: TiledVectorLayer = createLayer({ maxZoom: 10 }, {}); + expect(layer.getMaxZoom()).toEqual(10); + }); +}); + +describe('icon', () => { + it('should use vector icon', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + + const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); + const component = shallow(iconAndTooltipContent.icon); + expect(component).toMatchSnapshot(); + }); +}); + +describe('getFeatureById', () => { + it('should return null feature', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + const feature = layer.getFeatureById('foobar') as Feature; + expect(feature).toEqual(null); + }); +}); + +describe('syncData', () => { + it('Should sync with source-params', async () => { + const layer: TiledVectorLayer = createLayer({}, {}); + + const syncContext = new MockSyncContext({ dataFilters: {} }); + + await layer.syncData(syncContext); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext.stopLoading); + + // @ts-expect-error + const call = syncContext.stopLoading.getCall(0); + expect(call.args[2]).toEqual(defaultConfig); + }); + + it('Should not resync when no changes to source params', async () => { + const layer1: TiledVectorLayer = createLayer({}, {}); + const syncContext1 = new MockSyncContext({ dataFilters: {} }); + + await layer1.syncData(syncContext1); + + const dataRequestDescriptor: DataRequestDescriptor = { + data: { ...defaultConfig }, + dataId: 'source', + }; + const layer2: TiledVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + {} + ); + const syncContext2 = new MockSyncContext({ dataFilters: {} }); + await layer2.syncData(syncContext2); + // @ts-expect-error + sinon.assert.notCalled(syncContext2.startLoading); + // @ts-expect-error + sinon.assert.notCalled(syncContext2.stopLoading); + }); + + it('Should resync when changes to source params', async () => { + const layer1: TiledVectorLayer = createLayer({}, {}); + const syncContext1 = new MockSyncContext({ dataFilters: {} }); + + await layer1.syncData(syncContext1); + + const dataRequestDescriptor: DataRequestDescriptor = { + data: defaultConfig, + dataId: 'source', + }; + const layer2: TiledVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + { layerName: 'barfoo' } + ); + const syncContext2 = new MockSyncContext({ dataFilters: {} }); + await layer2.syncData(syncContext2); + + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.stopLoading); + + // @ts-expect-error + const call = syncContext2.stopLoading.getCall(0); + expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index a00639aa5fec..c9ae1c805fa3 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -6,31 +6,30 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; +import { Feature } from 'geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants'; import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { ITiledSingleLayerVectorSource } from '../../sources/vector_source'; import { DataRequestContext } from '../../../actions'; -import { ISource } from '../../sources/source'; import { VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types'; export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; static createDescriptor( descriptor: Partial, - mapColors: string[] + mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); layerDescriptor.type = TiledVectorLayer.type; if (!layerDescriptor.style) { - const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors); + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); } @@ -64,13 +63,16 @@ export class TiledVectorLayer extends VectorLayer { ); const prevDataRequest = this.getSourceDataRequest(); - const canSkip = await canSkipSourceUpdate({ - source: this._source as ISource, - prevDataRequest, - nextMeta: searchFilters, - }); - if (canSkip) { - return null; + if (prevDataRequest) { + const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + const canSkipBecauseNoChanges = + data.layerName === this._source.getLayerName() && + data.minSourceZoom === this._source.getMinZoom() && + data.maxSourceZoom === this._source.getMaxZoom(); + + if (canSkipBecauseNoChanges) { + return null; + } } startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters); @@ -89,37 +91,41 @@ export class TiledVectorLayer extends VectorLayer { } _syncSourceBindingWithMb(mbMap: unknown) { - // @ts-ignore - const mbSource = mbMap.getSource(this.getId()); - if (!mbSource) { - const sourceDataRequest = this.getSourceDataRequest(); - if (!sourceDataRequest) { - // this is possible if the layer was invisible at startup. - // the actions will not perform any data=syncing as an optimization when a layer is invisible - // when turning the layer back into visible, it's possible the url has not been resovled yet. - return; - } - - const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if (!sourceMeta) { - return; - } - - const sourceId = this.getId(); - - // @ts-ignore - mbMap.addSource(sourceId, { - type: 'vector', - tiles: [sourceMeta.urlTemplate], - minzoom: sourceMeta.minSourceZoom, - maxzoom: sourceMeta.maxSourceZoom, - }); + // @ts-expect-error + const mbSource = mbMap.getSource(this._getMbSourceId()); + if (mbSource) { + return; } + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url has not been resovled yet. + return; + } + + const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (!sourceMeta) { + return; + } + + const mbSourceId = this._getMbSourceId(); + // @ts-expect-error + mbMap.addSource(mbSourceId, { + type: 'vector', + tiles: [sourceMeta.urlTemplate], + minzoom: sourceMeta.minSourceZoom, + maxzoom: sourceMeta.maxSourceZoom, + }); + } + + ownsMbSourceId(mbSourceId: string): boolean { + return this._getMbSourceId() === mbSourceId; } _syncStylePropertiesWithMb(mbMap: unknown) { // @ts-ignore - const mbSource = mbMap.getSource(this.getId()); + const mbSource = mbMap.getSource(this._getMbSourceId()); if (!mbSource) { return; } @@ -129,32 +135,52 @@ export class TiledVectorLayer extends VectorLayer { return; } const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (sourceMeta.layerName === '') { + return; + } this._setMbPointsProperties(mbMap, sourceMeta.layerName); this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); } _requiresPrevSourceCleanup(mbMap: unknown): boolean { - // @ts-ignore - const mbTileSource = mbMap.getSource(this.getId()); + // @ts-expect-error + const mbTileSource = mbMap.getSource(this._getMbSourceId()); if (!mbTileSource) { return false; } + const dataRequest = this.getSourceDataRequest(); if (!dataRequest) { return false; } const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig; - if ( - mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate && - mbTileSource.minzoom === tiledSourceMeta.minSourceZoom && - mbTileSource.maxzoom === tiledSourceMeta.maxSourceZoom - ) { - // TileURL and zoom-range captures all the state. If this does not change, no updates are required. + + if (!tiledSourceMeta) { return false; } - return true; + const isSourceDifferent = + mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate || + mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom || + mbTileSource.maxzoom !== tiledSourceMeta.maxSourceZoom; + + if (isSourceDifferent) { + return true; + } + + const layerIds = this.getMbLayerIds(); + for (let i = 0; i < layerIds.length; i++) { + // @ts-expect-error + const mbLayer = mbMap.getLayer(layerIds[i]); + if (mbLayer && mbLayer.sourceLayer !== tiledSourceMeta.layerName) { + // If the source-pointer of one of the layers is stale, they will all be stale. + // In this case, all the mb-layers need to be removed and re-added. + return true; + } + } + + return false; } syncLayerWithMB(mbMap: unknown) { @@ -171,4 +197,8 @@ export class TiledVectorLayer extends VectorLayer { // higher resolution vector tiles cannot be displayed at lower-res return Math.max(this._source.getMinZoom(), super.getMinZoom()); } + + getFeatureById(id: string | number): Feature | null { + return null; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index e420087628bc..77daf9c9af57 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -5,6 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { Feature, GeoJsonProperties } from 'geojson'; import { AbstractLayer } from '../layer'; import { IVectorSource } from '../../sources/vector_source'; import { @@ -17,6 +18,7 @@ import { IJoin } from '../../joins/join'; import { IVectorStyle } from '../../styles/vector/vector_style'; import { IField } from '../../fields/field'; import { DataRequestContext } from '../../../actions'; +import { ITooltipProperty } from '../../tooltips/tooltip_property'; export type VectorLayerArguments = { source: IVectorSource; @@ -31,6 +33,8 @@ export interface IVectorLayer extends ILayer { getValidJoins(): IJoin[]; getSource(): IVectorSource; getStyle(): IVectorStyle; + getFeatureById(id: string | number): Feature | null; + getPropertiesForTooltip(properties: GeoJsonProperties): Promise; } export class VectorLayer extends AbstractLayer implements IVectorLayer { @@ -75,4 +79,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; getSource(): IVectorSource; getStyle(): IVectorStyle; + getFeatureById(id: string | number): Feature | null; + getPropertiesForTooltip(properties: GeoJsonProperties): Promise; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 524ab245c676..0a4fcfc23060 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -672,10 +672,10 @@ export class VectorLayer extends AbstractLayer { } this.syncVisibilityWithMb(mbMap, markerLayerId); - mbMap.setLayerZoomRange(markerLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(markerLayerId, this.getMinZoom(), this.getMaxZoom()); if (markerLayerId !== textLayerId) { this.syncVisibilityWithMb(mbMap, textLayerId); - mbMap.setLayerZoomRange(textLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(textLayerId, this.getMinZoom(), this.getMaxZoom()); } } @@ -802,14 +802,14 @@ export class VectorLayer extends AbstractLayer { }); this.syncVisibilityWithMb(mbMap, fillLayerId); - mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom()); const fillFilterExpr = getFillFilterExpression(hasJoins); if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) { mbMap.setFilter(fillLayerId, fillFilterExpr); } this.syncVisibilityWithMb(mbMap, lineLayerId); - mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom()); const lineFilterExpr = getLineFilterExpression(hasJoins); if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { mbMap.setFilter(lineLayerId, lineFilterExpr); @@ -822,9 +822,9 @@ export class VectorLayer extends AbstractLayer { } _syncSourceBindingWithMb(mbMap) { - const mbSource = mbMap.getSource(this.getId()); + const mbSource = mbMap.getSource(this._getMbSourceId()); if (!mbSource) { - mbMap.addSource(this.getId(), { + mbMap.addSource(this._getMbSourceId(), { type: 'geojson', data: EMPTY_FEATURE_COLLECTION, }); @@ -891,16 +891,17 @@ export class VectorLayer extends AbstractLayer { } async getPropertiesForTooltip(properties) { - let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties); - this._addJoinsToSourceTooltips(allTooltips); + const vectorSource = this.getSource(); + let allProperties = await vectorSource.filterAndFormatPropertiesToHtml(properties); + this._addJoinsToSourceTooltips(allProperties); for (let i = 0; i < this.getJoins().length; i++) { const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip( properties ); - allTooltips = [...allTooltips, ...propsFromJoin]; + allProperties = [...allProperties, ...propsFromJoin]; } - return allTooltips; + return allProperties; } canShowTooltip() { @@ -912,7 +913,7 @@ export class VectorLayer extends AbstractLayer { getFeatureById(id) { const featureCollection = this._getSourceFeatureCollection(); if (!featureCollection) { - return; + return null; } return featureCollection.features.find((feature) => { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx index ac69505a9bed..7021859ee982 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx @@ -15,7 +15,7 @@ import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/vi interface Props { layerId: string; - onChange: (args: OnSourceChangeArgs) => void; + onChange: (...args: OnSourceChangeArgs[]) => void; source: IEmsFileSource; tooltipFields: IField[]; } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap new file mode 100644 index 000000000000..f6d0129e85ab --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap @@ -0,0 +1,491 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render error for dupes 1`] = ` + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="String" + /> + + + + + + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="Number" + /> + + + + + + + + + + + Add + + + + +`; + +exports[`should render error for empty name 1`] = ` + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="String" + /> + + + + + + + + + + + Add + + + + +`; + +exports[`should render field editor 1`] = ` + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="String" + /> + + + + + + + + + + + + + + + + + string + + , + "value": "String", + }, + Object { + "inputDisplay": + + + + + number + + , + "value": "Number", + }, + ] + } + valueOfSelected="Number" + /> + + + + + + + + + + + Add + + + + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap new file mode 100644 index 000000000000..699173bd362f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap @@ -0,0 +1,211 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render fields-editor when there is no layername 1`] = ` + + + + + + + Available levels + + + + + } + max={24} + min={0} + onChange={[Function]} + prepend="Zoom" + showInput="inputWithPopover" + showLabels={true} + value={ + Array [ + 4, + 14, + ] + } + /> + +`; + +exports[`should render with fields 1`] = ` + + + + + + + Available levels + + + + + } + max={24} + min={0} + onChange={[Function]} + prepend="Zoom" + showInput="inputWithPopover" + showLabels={true} + value={ + Array [ + 4, + 14, + ] + } + /> + + Fields which are available in + + + foobar + + . + + + These can be used for tooltips and dynamic styling. + + } + delay="regular" + position="top" + > + + Fields + + + + + } + labelType="label" + > + + + +`; + +exports[`should render without fields 1`] = ` + + + + + + + Available levels + + + + + } + max={24} + min={0} + onChange={[Function]} + prepend="Zoom" + showInput="inputWithPopover" + showLabels={true} + value={ + Array [ + 4, + 14, + ] + } + /> + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap new file mode 100644 index 000000000000..ccd0e0064d07 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render source creation editor (fields should _not_ be included) 1`] = ` + + + + + + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap new file mode 100644 index 000000000000..bccf2b17e2b5 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render update source editor (fields _should_ be included) 1`] = ` + + + +
+ +
+
+ + +
+ + + +
+ +
+
+ + +
+ +
+`; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index 067c7f5a47ca..32fa329be85d 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -6,23 +6,21 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - MVTSingleLayerVectorSourceEditor, - MVTSingleLayerVectorSourceConfig, -} from './mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor'; import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types'; export const mvtVectorSourceWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { - defaultMessage: 'Vector source wizard', + defaultMessage: 'Data service implementing the Mapbox vector tile specification', }), icon: 'grid', renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { + const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx new file mode 100644 index 000000000000..0121dc45cb9e --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MVTFieldConfigEditor } from './mvt_field_config_editor'; +import { MVT_FIELD_TYPE } from '../../../../common/constants'; + +test('should render field editor', async () => { + const fields = [ + { + name: 'foo', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'bar', + type: MVT_FIELD_TYPE.NUMBER, + }, + ]; + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); + +test('should render error for empty name', async () => { + const fields = [ + { + name: '', + type: MVT_FIELD_TYPE.STRING, + }, + ]; + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); + +test('should render error for dupes', async () => { + const fields = [ + { + name: 'foo', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'foo', + type: MVT_FIELD_TYPE.NUMBER, + }, + ]; + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx new file mode 100644 index 000000000000..b2a93a4ef88a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx @@ -0,0 +1,210 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { ChangeEvent, Component, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSuperSelect, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; +import { FieldIcon } from '../../../../../../../src/plugins/kibana_react/public'; +import { MVT_FIELD_TYPE } from '../../../../common/constants'; + +function makeOption({ + value, + icon, + message, +}: { + value: MVT_FIELD_TYPE; + icon: string; + message: string; +}) { + return { + value, + inputDisplay: ( + + + + + {message} + + ), + }; +} + +const FIELD_TYPE_OPTIONS = [ + { + value: MVT_FIELD_TYPE.STRING, + icon: 'string', + message: i18n.translate('xpack.maps.mvtSource.stringFieldLabel', { + defaultMessage: 'string', + }), + }, + { + value: MVT_FIELD_TYPE.NUMBER, + icon: 'number', + message: i18n.translate('xpack.maps.mvtSource.numberFieldLabel', { + defaultMessage: 'number', + }), + }, +].map(makeOption); + +interface Props { + fields: MVTFieldDescriptor[]; + onChange: (fields: MVTFieldDescriptor[]) => void; +} + +interface State { + currentFields: MVTFieldDescriptor[]; +} + +export class MVTFieldConfigEditor extends Component { + state: State = { + currentFields: _.cloneDeep(this.props.fields), + }; + + _notifyChange = _.debounce(() => { + const invalid = this.state.currentFields.some((field: MVTFieldDescriptor) => { + return field.name === ''; + }); + + if (!invalid) { + this.props.onChange(this.state.currentFields); + } + }); + + _fieldChange(newFields: MVTFieldDescriptor[]) { + this.setState( + { + currentFields: newFields, + }, + this._notifyChange + ); + } + + _removeField(index: number) { + const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice(); + newFields.splice(index, 1); + this._fieldChange(newFields); + } + + _addField = () => { + const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice(); + newFields.push({ + type: MVT_FIELD_TYPE.STRING, + name: '', + }); + this._fieldChange(newFields); + }; + + _renderFieldTypeDropDown(mvtFieldConfig: MVTFieldDescriptor, index: number) { + const onChange = (type: MVT_FIELD_TYPE) => { + const newFields = this.state.currentFields.slice(); + newFields[index] = { + type, + name: newFields[index].name, + }; + this._fieldChange(newFields); + }; + + return ( + onChange(value)} + compressed + /> + ); + } + + _renderFieldButtonDelete(index: number) { + return ( + { + this._removeField(index); + }} + title={i18n.translate('xpack.maps.mvtSource.trashButtonTitle', { + defaultMessage: 'Remove field', + })} + aria-label={i18n.translate('xpack.maps.mvtSource.trashButtonAriaLabel', { + defaultMessage: 'Remove field', + })} + /> + ); + } + + _renderFieldNameInput(mvtFieldConfig: MVTFieldDescriptor, index: number) { + const onChange = (e: ChangeEvent) => { + const name = e.target.value; + const newFields = this.state.currentFields.slice(); + newFields[index] = { + name, + type: newFields[index].type, + }; + this._fieldChange(newFields); + }; + + const emptyName = mvtFieldConfig.name === ''; + const hasDupes = + this.state.currentFields.filter((field) => field.name === mvtFieldConfig.name).length > 1; + + return ( + + ); + } + + _renderFieldConfig() { + return this.state.currentFields.map((mvtFieldConfig: MVTFieldDescriptor, index: number) => { + return ( + <> + + {this._renderFieldNameInput(mvtFieldConfig, index)} + {this._renderFieldTypeDropDown(mvtFieldConfig, index)} + {this._renderFieldButtonDelete(index)} + + + + ); + }); + } + + render() { + return ( + + {this._renderFieldConfig()} + + + + + {i18n.translate('xpack.maps.mvtSource.addFieldLabel', { + defaultMessage: 'Add', + })} + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx new file mode 100644 index 000000000000..b5c75b97e6cb --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; + +const defaultSettings = { + handleChange: () => {}, + layerName: 'foobar', + fields: [], + minSourceZoom: 4, + maxSourceZoom: 14, + showFields: true, +}; + +test('should render with fields', async () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render without fields', async () => { + const settings = { ...defaultSettings, showFields: false }; + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should not render fields-editor when there is no layername', async () => { + const settings = { ...defaultSettings, layerName: '' }; + const component = shallow(); + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx new file mode 100644 index 000000000000..cd3fd97cf66a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx @@ -0,0 +1,191 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { Fragment, Component, ChangeEvent } from 'react'; +import { EuiFieldText, EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; +import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public'; +import { MVTFieldConfigEditor } from './mvt_field_config_editor'; +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; + +export type MVTSettings = { + layerName: string; + fields: MVTFieldDescriptor[]; + minSourceZoom: number; + maxSourceZoom: number; +}; + +interface State { + currentLayerName: string; + currentMinSourceZoom: number; + currentMaxSourceZoom: number; + currentFields: MVTFieldDescriptor[]; +} + +interface Props { + handleChange: (args: MVTSettings) => void; + layerName: string; + fields: MVTFieldDescriptor[]; + minSourceZoom: number; + maxSourceZoom: number; + showFields: boolean; +} + +export class MVTSingleLayerSourceSettings extends Component { + // Tracking in state to allow for debounce. + // Changes to layer-name and/or min/max zoom require heavy operation at map-level (removing and re-adding all sources/layers) + // To preserve snappyness of typing, debounce the dispatches. + state = { + currentLayerName: this.props.layerName, + currentMinSourceZoom: this.props.minSourceZoom, + currentMaxSourceZoom: this.props.maxSourceZoom, + currentFields: _.cloneDeep(this.props.fields), + }; + + _handleChange = _.debounce(() => { + this.props.handleChange({ + layerName: this.state.currentLayerName, + minSourceZoom: this.state.currentMinSourceZoom, + maxSourceZoom: this.state.currentMaxSourceZoom, + fields: this.state.currentFields, + }); + }, 200); + + _handleLayerNameInputChange = (e: ChangeEvent) => { + this.setState({ currentLayerName: e.target.value }, this._handleChange); + }; + + _handleFieldChange = (fields: MVTFieldDescriptor[]) => { + this.setState({ currentFields: fields }, this._handleChange); + }; + + _handleZoomRangeChange = (e: Value) => { + this.setState( + { + currentMinSourceZoom: parseInt(e[0] as string, 10), + currentMaxSourceZoom: parseInt(e[1] as string, 10), + }, + this._handleChange + ); + }; + + render() { + const preMessage = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPreHelpMessage', + { + defaultMessage: 'Fields which are available in ', + } + ); + const message = ( + <> + {this.state.currentLayerName}.{' '} + + ); + const postMessage = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPostHelpMessage', + { + defaultMessage: 'These can be used for tooltips and dynamic styling.', + } + ); + const fieldEditor = + this.props.showFields && this.state.currentLayerName !== '' ? ( + + {preMessage} + {message} + {postMessage} + + } + > + + {i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsMessage', + { + defaultMessage: 'Fields', + } + )}{' '} + + + + } + > + + + ) : null; + + return ( + + + + + + + + {i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeTopMessage', + { + defaultMessage: 'Available levels', + } + )}{' '} + + + + } + formRowDisplay="columnCompressed" + value={[this.state.currentMinSourceZoom, this.state.currentMaxSourceZoom]} + min={MIN_ZOOM} + max={MAX_ZOOM} + onChange={this._handleZoomRangeChange} + allowEmptyRange={false} + showInput="inputWithPopover" + compressed + showLabels + prepend={i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage', + { + defaultMessage: 'Zoom', + } + )} + /> + {fieldEditor} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx new file mode 100644 index 000000000000..bc08baad7a84 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; +import { MVT_FIELD_TYPE, SOURCE_TYPES } from '../../../../common/constants'; +import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types'; + +const descriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, + fields: [], + tooltipProperties: [], +}; + +describe('getUrlTemplateWithMeta', () => { + it('should echo configuration', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + const config = await source.getUrlTemplateWithMeta(); + expect(config.urlTemplate).toEqual(descriptor.urlTemplate); + expect(config.layerName).toEqual(descriptor.layerName); + expect(config.minSourceZoom).toEqual(descriptor.minSourceZoom); + expect(config.maxSourceZoom).toEqual(descriptor.maxSourceZoom); + }); +}); + +describe('canFormatFeatureProperties', () => { + it('false if no tooltips', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + expect(source.canFormatFeatureProperties()).toEqual(false); + }); + it('true if tooltip', async () => { + const descriptorWithTooltips = { + ...descriptor, + fields: [{ name: 'foobar', type: MVT_FIELD_TYPE.STRING }], + tooltipProperties: ['foobar'], + }; + const source = new MVTSingleLayerVectorSource(descriptorWithTooltips); + expect(source.canFormatFeatureProperties()).toEqual(true); + }); +}); + +describe('filterAndFormatPropertiesToHtml', () => { + const descriptorWithFields = { + ...descriptor, + fields: [ + { + name: 'foo', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'food', + type: MVT_FIELD_TYPE.STRING, + }, + { + name: 'fooz', + type: MVT_FIELD_TYPE.NUMBER, + }, + ], + tooltipProperties: ['foo', 'fooz'], + }; + + it('should get tooltipproperties', async () => { + const source = new MVTSingleLayerVectorSource(descriptorWithFields); + const tooltipProperties = await source.filterAndFormatPropertiesToHtml({ + foo: 'bar', + fooz: 123, + }); + expect(tooltipProperties.length).toEqual(2); + expect(tooltipProperties[0].getPropertyName()).toEqual('foo'); + expect(tooltipProperties[0].getHtmlDisplayValue()).toEqual('bar'); + expect(tooltipProperties[1].getPropertyName()).toEqual('fooz'); + expect(tooltipProperties[1].getHtmlDisplayValue()).toEqual('123'); + }); +}); + +describe('getImmutableSourceProperties', () => { + it('should only show immutable props', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + const properties = await source.getImmutableProperties(); + expect(properties).toEqual([ + { label: 'Data source', value: '.pbf vector tiles' }, + { label: 'Url', value: 'https://example.com/{x}/{y}/{z}.pbf' }, + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts deleted file mode 100644 index 03b91df22d3c..000000000000 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ /dev/null @@ -1,161 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import uuid from 'uuid/v4'; -import { AbstractSource, ImmutableSourceProperty } from '../source'; -import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; -import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; -import { IField } from '../../fields/field'; -import { registerSource } from '../source_registry'; -import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; -import { - MapExtent, - TiledSingleLayerVectorSourceDescriptor, - VectorSourceSyncMeta, -} from '../../../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; -import { ITooltipProperty } from '../../tooltips/tooltip_property'; - -export const sourceTitle = i18n.translate( - 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', - { - defaultMessage: 'Vector Tile Layer', - } -); - -export class MVTSingleLayerVectorSource extends AbstractSource - implements ITiledSingleLayerVectorSource { - static createDescriptor({ - urlTemplate, - layerName, - minSourceZoom, - maxSourceZoom, - }: MVTSingleLayerVectorSourceConfig) { - return { - type: SOURCE_TYPES.MVT_SINGLE_LAYER, - id: uuid(), - urlTemplate, - layerName, - minSourceZoom: Math.max(MIN_ZOOM, minSourceZoom), - maxSourceZoom: Math.min(MAX_ZOOM, maxSourceZoom), - }; - } - - readonly _descriptor: TiledSingleLayerVectorSourceDescriptor; - - constructor( - sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, - inspectorAdapters?: object - ) { - super(sourceDescriptor, inspectorAdapters); - this._descriptor = sourceDescriptor; - } - - renderSourceSettingsEditor() { - return null; - } - - getFieldNames(): string[] { - return []; - } - - getGeoJsonWithMeta( - layerName: 'string', - searchFilters: unknown[], - registerCancelCallback: (callback: () => void) => void - ): Promise { - // todo: remove this method - // This is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. - throw new Error('Does not implement getGeoJsonWithMeta'); - } - - async getFields(): Promise { - return []; - } - - async getImmutableProperties(): Promise { - return [ - { label: getDataSourceLabel(), value: sourceTitle }, - { label: getUrlLabel(), value: this._descriptor.urlTemplate }, - { - label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage', { - defaultMessage: 'Layer name', - }), - value: this._descriptor.layerName, - }, - { - label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage', { - defaultMessage: 'Min zoom', - }), - value: this._descriptor.minSourceZoom.toString(), - }, - { - label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage', { - defaultMessage: 'Max zoom', - }), - value: this._descriptor.maxSourceZoom.toString(), - }, - ]; - } - - async getDisplayName(): Promise { - return this._descriptor.layerName; - } - - async getUrlTemplateWithMeta() { - return { - urlTemplate: this._descriptor.urlTemplate, - layerName: this._descriptor.layerName, - minSourceZoom: this._descriptor.minSourceZoom, - maxSourceZoom: this._descriptor.maxSourceZoom, - }; - } - - async getSupportedShapeTypes(): Promise { - return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; - } - - canFormatFeatureProperties() { - return false; - } - - getMinZoom() { - return this._descriptor.minSourceZoom; - } - - getMaxZoom() { - return this._descriptor.maxSourceZoom; - } - - getBoundsForFilters( - boundsFilters: BoundsFilters, - registerCancelCallback: (requestToken: symbol, callback: () => void) => void - ): MapExtent | null { - return null; - } - - getFieldByName(fieldName: string): IField | null { - return null; - } - - getSyncMeta(): VectorSourceSyncMeta { - return null; - } - - getApplyGlobalQuery(): boolean { - return false; - } - - async filterAndFormatPropertiesToHtml(properties: unknown): Promise { - return []; - } -} - -registerSource({ - ConstructorFunction: MVTSingleLayerVectorSource, - type: SOURCE_TYPES.MVT_SINGLE_LAYER, -}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx new file mode 100644 index 000000000000..ae28828dec5a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid/v4'; +import React from 'react'; +import { GeoJsonProperties } from 'geojson'; +import { AbstractSource, ImmutableSourceProperty, SourceEditorArgs } from '../source'; +import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { + FIELD_ORIGIN, + MAX_ZOOM, + MIN_ZOOM, + SOURCE_TYPES, + VECTOR_SHAPE_TYPE, +} from '../../../../common/constants'; +import { registerSource } from '../source_registry'; +import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; +import { + MapExtent, + MVTFieldDescriptor, + TiledSingleLayerVectorSourceDescriptor, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { MVTField } from '../../fields/mvt_field'; +import { UpdateSourceEditor } from './update_source_editor'; +import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; + +export const sourceTitle = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', + { + defaultMessage: '.pbf vector tiles', + } +); + +export class MVTSingleLayerVectorSource extends AbstractSource + implements ITiledSingleLayerVectorSource { + static createDescriptor({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + fields, + tooltipProperties, + }: Partial) { + return { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + id: uuid(), + urlTemplate: urlTemplate ? urlTemplate : '', + layerName: layerName ? layerName : '', + minSourceZoom: + typeof minSourceZoom === 'number' ? Math.max(MIN_ZOOM, minSourceZoom) : MIN_ZOOM, + maxSourceZoom: + typeof maxSourceZoom === 'number' ? Math.min(MAX_ZOOM, maxSourceZoom) : MAX_ZOOM, + fields: fields ? fields : [], + tooltipProperties: tooltipProperties ? tooltipProperties : [], + }; + } + + readonly _descriptor: TiledSingleLayerVectorSourceDescriptor; + readonly _tooltipFields: MVTField[]; + + constructor( + sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, + inspectorAdapters?: object + ) { + super(sourceDescriptor, inspectorAdapters); + this._descriptor = MVTSingleLayerVectorSource.createDescriptor(sourceDescriptor); + + this._tooltipFields = this._descriptor.tooltipProperties + .map((fieldName) => { + return this.getFieldByName(fieldName); + }) + .filter((f) => f !== null) as MVTField[]; + } + + async supportsFitToBounds() { + return false; + } + + renderSourceSettingsEditor({ onChange }: SourceEditorArgs) { + return ( + + ); + } + + getFieldNames(): string[] { + return this._descriptor.fields.map((field: MVTFieldDescriptor) => { + return field.name; + }); + } + + getMVTFields(): MVTField[] { + return this._descriptor.fields.map((field: MVTFieldDescriptor) => { + return new MVTField({ + fieldName: field.name, + type: field.type, + source: this, + origin: FIELD_ORIGIN.SOURCE, + }); + }); + } + + getFieldByName(fieldName: string): MVTField | null { + try { + return this.createField({ fieldName }); + } catch (e) { + return null; + } + } + + createField({ fieldName }: { fieldName: string }): MVTField { + const field = this._descriptor.fields.find((f: MVTFieldDescriptor) => { + return f.name === fieldName; + }); + if (!field) { + throw new Error(`Cannot create field for fieldName ${fieldName}`); + } + return new MVTField({ + fieldName: field.name, + type: field.type, + source: this, + origin: FIELD_ORIGIN.SOURCE, + }); + } + + getGeoJsonWithMeta( + layerName: 'string', + searchFilters: unknown[], + registerCancelCallback: (callback: () => void) => void + ): Promise { + // Having this method here is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. + throw new Error('Does not implement getGeoJsonWithMeta'); + } + + async getFields(): Promise { + return this.getMVTFields(); + } + + getLayerName(): string { + return this._descriptor.layerName; + } + + async getImmutableProperties(): Promise { + return [ + { label: getDataSourceLabel(), value: sourceTitle }, + { label: getUrlLabel(), value: this._descriptor.urlTemplate }, + ]; + } + + async getDisplayName(): Promise { + return this.getLayerName(); + } + + async getUrlTemplateWithMeta() { + return { + urlTemplate: this._descriptor.urlTemplate, + layerName: this._descriptor.layerName, + minSourceZoom: this._descriptor.minSourceZoom, + maxSourceZoom: this._descriptor.maxSourceZoom, + }; + } + + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; + } + + canFormatFeatureProperties() { + return !!this._tooltipFields.length; + } + + getMinZoom() { + return this._descriptor.minSourceZoom; + } + + getMaxZoom() { + return this._descriptor.maxSourceZoom; + } + + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null { + return null; + } + + getSyncMeta(): VectorSourceSyncMeta { + return null; + } + + getApplyGlobalQuery(): boolean { + return false; + } + + async filterAndFormatPropertiesToHtml( + properties: GeoJsonProperties, + featureId?: string | number + ): Promise { + const tooltips = []; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + for (let i = 0; i < this._tooltipFields.length; i++) { + const mvtField = this._tooltipFields[i]; + if (mvtField.getName() === key) { + const tooltip = new TooltipProperty(key, key, properties[key]); + tooltips.push(tooltip); + break; + } + } + } + } + return tooltips; + } +} + +registerSource({ + ConstructorFunction: MVTSingleLayerVectorSource, + type: SOURCE_TYPES.MVT_SINGLE_LAYER, +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx new file mode 100644 index 000000000000..986756f84001 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx @@ -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. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor'; + +test('should render source creation editor (fields should _not_ be included)', async () => { + const component = shallow( {}} />); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx index 760b8c676cb3..49487e96a454 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx @@ -10,17 +10,14 @@ import _ from 'lodash'; import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; -import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public'; +import { + MVTFieldDescriptor, + TiledSingleLayerVectorSourceSettings, +} from '../../../../common/descriptor_types'; +import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; -export type MVTSingleLayerVectorSourceConfig = { - urlTemplate: string; - layerName: string; - minSourceZoom: number; - maxSourceZoom: number; -}; - -export interface Props { - onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void; +interface Props { + onSourceConfigChange: (sourceConfig: TiledSingleLayerVectorSourceSettings) => void; } interface State { @@ -28,6 +25,7 @@ interface State { layerName: string; minSourceZoom: number; maxSourceZoom: number; + fields?: MVTFieldDescriptor[]; } export class MVTSingleLayerVectorSourceEditor extends Component { @@ -36,6 +34,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component { layerName: '', minSourceZoom: MIN_ZOOM, maxSourceZoom: MAX_ZOOM, + fields: [], }; _sourceConfigChange = _.debounce(() => { @@ -50,6 +49,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component { layerName: this.state.layerName, minSourceZoom: this.state.minSourceZoom, maxSourceZoom: this.state.maxSourceZoom, + fields: this.state.fields, }); } }, 200); @@ -64,23 +64,13 @@ export class MVTSingleLayerVectorSourceEditor extends Component { ); }; - _handleLayerNameInputChange = (e: ChangeEvent) => { - const layerName = e.target.value; - this.setState( - { - layerName, - }, - () => this._sourceConfigChange() - ); - }; - - _handleZoomRangeChange = (e: Value) => { - const minSourceZoom = parseInt(e[0] as string, 10); - const maxSourceZoom = parseInt(e[1] as string, 10); - - if (this.state.minSourceZoom !== minSourceZoom || this.state.maxSourceZoom !== maxSourceZoom) { - this.setState({ minSourceZoom, maxSourceZoom }, () => this._sourceConfigChange()); - } + _handleChange = (state: { + layerName: string; + fields: MVTFieldDescriptor[]; + minSourceZoom: number; + maxSourceZoom: number; + }) => { + this.setState(state, () => this._sourceConfigChange()); }; render() { @@ -90,37 +80,30 @@ export class MVTSingleLayerVectorSourceEditor extends Component { label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', { defaultMessage: 'Url', })} - > - - - - + - ); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts new file mode 100644 index 000000000000..599eaea73c9a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts @@ -0,0 +1,16 @@ +/* + * 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 { MVTFieldDescriptor } from '../../../../common/descriptor_types'; + +export interface MVTSingleLayerVectorSourceConfig { + urlTemplate: string; + layerName: string; + minSourceZoom: number; + maxSourceZoom: number; + fields?: MVTFieldDescriptor[]; + tooltipProperties?: string[]; +} diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx new file mode 100644 index 000000000000..fd19379058e3 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => ({})); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UpdateSourceEditor } from './update_source_editor'; +import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; +import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types'; +import { SOURCE_TYPES } from '../../../../common/constants'; + +const descriptor: TiledSingleLayerVectorSourceDescriptor = { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', + layerName: 'foobar', + minSourceZoom: 4, + maxSourceZoom: 14, + fields: [], + tooltipProperties: [], +}; + +test('should render update source editor (fields _should_ be included)', async () => { + const source = new MVTSingleLayerVectorSource(descriptor); + + const component = shallow( + {}} /> + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx new file mode 100644 index 000000000000..a95991271819 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx @@ -0,0 +1,136 @@ +/* + * 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 React, { Component, Fragment } from 'react'; +import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { TooltipSelector } from '../../../components/tooltip_selector'; +import { MVTField } from '../../fields/mvt_field'; +import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; +import { MVTSettings, MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; +import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; +import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; + +interface Props { + tooltipFields: MVTField[]; + onChange: (...args: OnSourceChangeArgs[]) => void; + source: MVTSingleLayerVectorSource; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface State {} + +export class UpdateSourceEditor extends Component { + _onTooltipPropertiesSelect = (propertyNames: string[]) => { + this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); + }; + + _handleChange = (settings: MVTSettings) => { + const changes: OnSourceChangeArgs[] = []; + if (settings.layerName !== this.props.source.getLayerName()) { + changes.push({ propName: 'layerName', value: settings.layerName }); + } + if (settings.minSourceZoom !== this.props.source.getMinZoom()) { + changes.push({ propName: 'minSourceZoom', value: settings.minSourceZoom }); + } + if (settings.maxSourceZoom !== this.props.source.getMaxZoom()) { + changes.push({ propName: 'maxSourceZoom', value: settings.maxSourceZoom }); + } + if (!_.isEqual(settings.fields, this._getFieldDescriptors())) { + changes.push({ propName: 'fields', value: settings.fields }); + + // Remove dangling tooltips. + // This behaves similar to how stale styling properties are removed (e.g. on metric-change in agg sources) + const sanitizedTooltips = []; + for (let i = 0; i < this.props.tooltipFields.length; i++) { + const tooltipName = this.props.tooltipFields[i].getName(); + for (let j = 0; j < settings.fields.length; j++) { + if (settings.fields[j].name === tooltipName) { + sanitizedTooltips.push(tooltipName); + break; + } + } + } + + if (!_.isEqual(sanitizedTooltips, this.props.tooltipFields)) { + changes.push({ propName: 'tooltipProperties', value: sanitizedTooltips }); + } + } + this.props.onChange(...changes); + }; + + _getFieldDescriptors(): MVTFieldDescriptor[] { + return this.props.source.getMVTFields().map((field: MVTField) => { + return field.getMVTFieldDescriptor(); + }); + } + + _renderSourceSettingsCard() { + const fieldDescriptors: MVTFieldDescriptor[] = this._getFieldDescriptors(); + return ( + + + +
+ +
+
+ + +
+ + +
+ ); + } + + _renderTooltipSelectionCard() { + return ( + + + +
+ +
+
+ + + + +
+ + +
+ ); + } + + render() { + return ( + + {this._renderSourceSettingsCard()} + {this._renderTooltipSelectionCard()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index f937eac33653..c68e22ada8b0 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -17,7 +17,7 @@ import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; export type SourceEditorArgs = { - onChange: (args: OnSourceChangeArgs) => void; + onChange: (...args: OnSourceChangeArgs[]) => void; }; export type ImmutableSourceProperty = { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 99a7478cd836..42993bf36f61 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -5,7 +5,7 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { FeatureCollection } from 'geojson'; +import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; import { Filter, TimeRange } from 'src/plugins/data/public'; import { AbstractSource, ISource } from '../source'; import { IField } from '../../fields/field'; @@ -35,7 +35,7 @@ export type BoundsFilters = { }; export interface IVectorSource extends ISource { - filterAndFormatPropertiesToHtml(properties: unknown): Promise; + filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise; getBoundsForFilters( boundsFilters: BoundsFilters, registerCancelCallback: (requestToken: symbol, callback: () => void) => void @@ -51,10 +51,12 @@ export interface IVectorSource extends ISource { getSyncMeta(): VectorSourceSyncMeta; getFieldNames(): string[]; getApplyGlobalQuery(): boolean; + createField({ fieldName }: { fieldName: string }): IField; + canFormatFeatureProperties(): boolean; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { - filterAndFormatPropertiesToHtml(properties: unknown): Promise; + filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise; getBoundsForFilters( boundsFilters: BoundsFilters, registerCancelCallback: (requestToken: symbol, callback: () => void) => void @@ -72,6 +74,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc canFormatFeatureProperties(): boolean; getApplyGlobalQuery(): boolean; getFieldNames(): string[]; + createField({ fieldName }: { fieldName: string }): IField; } export interface ITiledSingleLayerVectorSource extends IVectorSource { @@ -83,4 +86,5 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource { }>; getMinZoom(): number; getMaxZoom(): number; + getLayerName(): string; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js index b7a80562f10c..fe2f302504a1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js @@ -89,7 +89,7 @@ export class ColorMapSelect extends Component { }; _renderColorStopsInput() { - if (!this.props.useCustomColorMap) { + if (!this.props.isCustomOnly && !this.props.useCustomColorMap) { return null; } @@ -102,7 +102,7 @@ export class ColorMapSelect extends Component { swatches={this.props.swatches} /> ); - } else + } else { colorStopEditor = ( ); + } return ( @@ -121,6 +122,10 @@ export class ColorMapSelect extends Component { } _renderColorMapSelections() { + if (this.props.isCustomOnly) { + return null; + } + const colorMapOptionsWithCustom = [ { value: CUSTOM_COLOR_MAP, @@ -146,19 +151,22 @@ export class ColorMapSelect extends Component { ) : null; return ( - - {toggle} - - - - + + + {toggle} + + + + + + ); } @@ -166,7 +174,6 @@ export class ColorMapSelect extends Component { return ( {this._renderColorMapSelections()} - {this._renderColorStopsInput()} ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js index fa13e1cf6666..90070343a1b4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js @@ -90,6 +90,7 @@ export function DynamicColorForm({ if (styleProperty.isOrdinal()) { return ( { + const field = fields.find((field) => { return field.name === selectedFieldName; }); + //Do not spread in all the other unused values (e.g. type, supportsAutoDomain etc...) + if (field) { + selectedOption = { + value: field.value, + label: field.label, + }; + } } return ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js index e285d91dcd7a..e4dc9d1b4d8f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js @@ -46,19 +46,16 @@ export class StyleMapSelect extends Component { }; _renderCustomStopsInput() { - if (!this.props.useCustomMap) { + return !this.props.isCustomOnly && !this.props.useCustomMap + ? null + : this.props.renderCustomStopsInput(this._onCustomMapChange); + } + + _renderMapSelect() { + if (this.props.isCustomOnly) { return null; } - return ( - - - {this.props.renderCustomStopsInput(this._onCustomMapChange)} - - ); - } - - render() { const mapOptionsWithCustom = [ { value: CUSTOM_MAP, @@ -87,6 +84,15 @@ export class StyleMapSelect extends Component { hasDividers={true} compressed /> + + + ); + } + + render() { + return ( + + {this._renderMapSelect()} {this._renderCustomStopsInput()} ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js index f9f8a6784647..e3724d42a783 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js @@ -36,17 +36,20 @@ export function DynamicIconForm({ }; function renderIconMapSelect() { - if (!styleOptions.field || !styleOptions.field.name) { + const field = styleProperty.getField(); + if (!field) { return null; } return ( ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js index 08f5dfe4f4ba..6cfe656d65a1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js @@ -8,8 +8,8 @@ import React from 'react'; import { StyleMapSelect } from '../style_map_select'; import { i18n } from '@kbn/i18n'; -import { getIconPaletteOptions } from '../../symbol_utils'; import { IconStops } from './icon_stops'; +import { getIconPaletteOptions } from '../../symbol_utils'; export function IconMapSelect({ customIconStops, @@ -19,6 +19,7 @@ export function IconMapSelect({ styleProperty, symbolOptions, useCustomIconMap, + isCustomOnly, }) { function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) { onChange({ @@ -52,6 +53,7 @@ export function IconMapSelect({ useCustomMap={useCustomIconMap} selectedMapId={iconPaletteId} renderCustomStopsInput={renderCustomIconStopsInput} + isCustomOnly={isCustomOnly} /> ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js index 7856a4ddaff3..6528648eff55 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js @@ -62,6 +62,7 @@ export class VectorStyleEditor extends Component { name: field.getName(), origin: field.getOrigin(), type: await field.getDataType(), + supportsAutoDomain: field.supportsAutoDomain(), }; }; @@ -109,7 +110,9 @@ export class VectorStyleEditor extends Component { } _getOrdinalFields() { - return [...this.state.dateFields, ...this.state.numberFields]; + return [...this.state.dateFields, ...this.state.numberFields].filter((field) => { + return field.supportsAutoDomain; + }); } _handleSelectedFeatureChange = (selectedFeature) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js index ae4d935e2457..763eb81ad0f9 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js @@ -10,11 +10,10 @@ import { VECTOR_STYLES } from '../../../../../common/constants'; export class DynamicOrientationProperty extends DynamicStyleProperty { syncIconRotationWithMb(symbolLayerId, mbMap) { - if (this._options.field && this._options.field.name) { - const targetName = getComputedFieldName( - VECTOR_STYLES.ICON_ORIENTATION, - this._options.field.name - ); + if (this._field && this._field.isValid()) { + const targetName = this._field.supportsAutoDomain() + ? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName()) + : this._field.getName(); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]); } else { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js index de868f3f9265..a7a3130875a9 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js @@ -10,7 +10,11 @@ import { getComputedFieldName } from '../style_util'; export class DynamicTextProperty extends DynamicStyleProperty { syncTextFieldWithMb(mbLayerId, mbMap) { if (this._field && this._field.isValid()) { - const targetName = getComputedFieldName(this._styleName, this._options.field.name); + // Fields that support auto-domain are normalized with a field-formatter and stored into a computed-field + // Otherwise, the raw value is just carried over and no computed field is created. + const targetName = this._field.supportsAutoDomain() + ? getComputedFieldName(this._styleName, this.getFieldName()) + : this._field.getName(); mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']); } else { mbMap.setLayoutProperty(mbLayerId, 'text-field', null); diff --git a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts index 7149fe29f90e..7bb79d8d341d 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts @@ -19,7 +19,7 @@ export interface ITooltipProperty { export interface LoadFeatureProps { layerId: string; - featureId: number; + featureId?: number | string; } export interface FeatureGeometry { diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js index 14252dcfc067..557fe5fd5f70 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js @@ -43,16 +43,16 @@ export class LayerPanel extends React.Component { componentDidMount() { this._isMounted = true; - this.loadDisplayName(); - this.loadImmutableSourceProperties(); - this.loadLeftJoinFields(); + this._loadDisplayName(); + this._loadImmutableSourceProperties(); + this._loadLeftJoinFields(); } componentWillUnmount() { this._isMounted = false; } - loadDisplayName = async () => { + _loadDisplayName = async () => { if (!this.props.selectedLayer) { return; } @@ -63,7 +63,7 @@ export class LayerPanel extends React.Component { } }; - loadImmutableSourceProperties = async () => { + _loadImmutableSourceProperties = async () => { if (!this.props.selectedLayer) { return; } @@ -74,7 +74,7 @@ export class LayerPanel extends React.Component { } }; - async loadLeftJoinFields() { + async _loadLeftJoinFields() { if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { return; } @@ -97,8 +97,11 @@ export class LayerPanel extends React.Component { } } - _onSourceChange = ({ propName, value, newLayerType }) => { - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + _onSourceChange = (...args) => { + for (let i = 0; i < args.length; i++) { + const { propName, value, newLayerType } = args[i]; + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + } }; _renderFilterSection() { diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js index 362186a8f554..5e2a153b2ccb 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js @@ -31,14 +31,15 @@ export class FeatureProperties extends React.Component { this._isMounted = false; } - _loadProperties = () => { + _loadProperties = async () => { this._fetchProperties({ nextFeatureId: this.props.featureId, nextLayerId: this.props.layerId, + mbProperties: this.props.mbProperties, }); }; - _fetchProperties = async ({ nextLayerId, nextFeatureId }) => { + _fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => { if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) { // do not reload same feature properties return; @@ -64,6 +65,7 @@ export class FeatureProperties extends React.Component { properties = await this.props.loadFeatureProperties({ layerId: nextLayerId, featureId: nextFeatureId, + mbProperties: mbProperties, }); } catch (error) { if (this._isMounted) { diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js index e5b97947602b..d91bc8e803ab 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js @@ -132,6 +132,7 @@ export class FeaturesTooltip extends React.Component { { sinon.assert.notCalled(closeOnClickTooltipStub); sinon.assert.calledWith(openOnClickTooltipStub, { - features: [{ id: 1, layerId: 'tfi3f' }], + features: [{ id: 1, layerId: 'tfi3f', mbProperties: { __kbn__feature_id__: 1 } }], location: [100, 30], }); }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js index 03c2aeb2edd0..6c4205768040 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -58,7 +58,7 @@ export class TooltipPopover extends Component { // Mapbox feature geometry is from vector tile and is not the same as the original geometry. _loadFeatureGeometry = ({ layerId, featureId }) => { const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { + if (!tooltipLayer || typeof featureId === 'undefined') { return null; } @@ -70,22 +70,24 @@ export class TooltipPopover extends Component { return targetFeature.geometry; }; - _loadFeatureProperties = async ({ layerId, featureId }) => { + _loadFeatureProperties = async ({ layerId, featureId, mbProperties }) => { const tooltipLayer = this._findLayerById(layerId); if (!tooltipLayer) { return []; } - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return []; + let targetFeature; + if (typeof featureId !== 'undefined') { + targetFeature = tooltipLayer.getFeatureById(featureId); } - return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); + + const properties = targetFeature ? targetFeature.properties : mbProperties; + return await tooltipLayer.getPropertiesForTooltip(properties); }; _loadPreIndexedShape = async ({ layerId, featureId }) => { const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { + if (!tooltipLayer || typeof featureId === 'undefined') { return null; } diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx index ff6e8859be04..98d4d3bd8fab 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { MapToolTipComponent } from './map_tool_tip'; -import { MapFeature } from '../types'; +import { TooltipFeature } from '../../../../../../maps/common/descriptor_types'; describe('MapToolTip', () => { test('placeholder component renders correctly against snapshot', () => { @@ -18,10 +18,11 @@ describe('MapToolTip', () => { test('full component renders correctly against snapshot', () => { const addFilters = jest.fn(); const closeTooltip = jest.fn(); - const features: MapFeature[] = [ + const features: TooltipFeature[] = [ { id: 1, layerId: 'layerId', + mbProperties: {}, }, ]; const getLayerName = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts index f91fd677ba7f..e3ca3c5b8428 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts @@ -36,11 +36,6 @@ export type SetQuery = (params: { refetch: inputsModel.Refetch; }) => void; -export interface MapFeature { - id: number; - layerId: string; -} - export interface FeatureGeometry { coordinates: [number]; type: string; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 074e93e10fd1..b5cdad5583e1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9214,9 +9214,6 @@ "xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです", "xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス", "xpack.maps.source.mapSettingsPanel.initialLocationLabel": "マップの初期位置情報", - "xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "レイヤー名", - "xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大ズーム", - "xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小ズーム", "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "ベトルタイルレイヤー", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズームレベル", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "レイヤー名", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9d0bd9552667..568397912141 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9218,9 +9218,6 @@ "xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴", "xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务", "xpack.maps.source.mapSettingsPanel.initialLocationLabel": "初始地图位置", - "xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "图层名称", - "xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大缩放", - "xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小缩放", "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "矢量磁贴图层", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放级别", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "图层名称", diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 68d3c2536ee0..5d75679432c9 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -76,7 +76,11 @@ export default function ({ getPageObjects, getService }) { const { lat, lon, zoom } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(41); expect(Math.round(lon)).to.equal(-102); - expect(Math.round(zoom)).to.equal(5); + + // Centering is correct, but screen-size and dpi affect zoom level, + // causing this test to be brittle in different environments + // Expecting zoom-level to be between ]4,5] + expect(Math.ceil(zoom)).to.equal(5); }); });