[maps] fix Map orphans sources on layer deletion (#159067)

Fixes https://github.com/elastic/kibana/issues/158133

Sources where not getting removed because `return` was used instead of
`continue` in `for...in` loop. This caused the function to return
instead of processing remaining sources.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-06-06 14:34:40 -06:00 committed by GitHub
parent d8374450f3
commit fcd195050f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 61 deletions

View file

@ -13,6 +13,7 @@ import type {
GeoJSONSource, GeoJSONSource,
VectorTileSource, VectorTileSource,
RasterTileSource, RasterTileSource,
SourceSpecification,
StyleSpecification, StyleSpecification,
MapEvent, MapEvent,
MapOptions, MapOptions,
@ -45,6 +46,7 @@ export { maplibregl };
export type { export type {
Map, Map,
LayerSpecification, LayerSpecification,
SourceSpecification,
StyleSpecification, StyleSpecification,
Source, Source,
GeoJSONSource, GeoJSONSource,

View file

@ -43,7 +43,7 @@ import {
import { getCanAccessEmsFonts, getGlyphs, getKibanaFontsGlyphUrl } from './glyphs'; import { getCanAccessEmsFonts, getGlyphs, getKibanaFontsGlyphUrl } from './glyphs';
import { syncLayerOrder } from './sort_layers'; import { syncLayerOrder } from './sort_layers';
import { removeOrphanedSourcesAndLayers } from './utils'; import { removeOrphanedSourcesAndLayers } from './remove_orphaned';
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
import { TileStatusTracker } from './tile_status_tracker'; import { TileStatusTracker } from './tile_status_tracker';
import { DrawFeatureControl } from './draw_control/draw_feature_control'; import { DrawFeatureControl } from './draw_control/draw_feature_control';

View file

@ -5,12 +5,23 @@
* 2.0. * 2.0.
*/ */
import { removeOrphanedSourcesAndLayers } from './utils'; /* eslint-disable max-classes-per-file */
import type {
Map as MbMap,
LayerSpecification,
SourceSpecification,
StyleSpecification,
} from '@kbn/mapbox-gl';
import { removeOrphanedSourcesAndLayers } from './remove_orphaned';
import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants'; import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants';
import _ from 'lodash'; import _ from 'lodash';
import { ILayer } from '../../classes/layers/layer';
class MockMbMap { class MockMbMap {
constructor(style) { private _style: StyleSpecification;
constructor(style: StyleSpecification) {
this._style = _.cloneDeep(style); this._style = _.cloneDeep(style);
} }
@ -18,26 +29,11 @@ class MockMbMap {
return _.cloneDeep(this._style); return _.cloneDeep(this._style);
} }
moveLayer(mbLayerId, nextMbLayerId) { removeSource(sourceId: string) {
const indexOfLayerToMove = this._style.layers.findIndex((layer) => {
return layer.id === mbLayerId;
});
const layerToMove = this._style.layers[indexOfLayerToMove];
this._style.layers.splice(indexOfLayerToMove, 1);
const indexOfNextLayer = this._style.layers.findIndex((layer) => {
return layer.id === nextMbLayerId;
});
this._style.layers.splice(indexOfNextLayer, 0, layerToMove);
}
removeSource(sourceId) {
delete this._style.sources[sourceId]; delete this._style.sources[sourceId];
} }
removeLayer(layerId) { removeLayer(layerId: string) {
const layerToRemove = this._style.layers.findIndex((layer) => { const layerToRemove = this._style.layers.findIndex((layer) => {
return layer.id === layerId; return layer.id === layerId;
}); });
@ -46,14 +42,15 @@ class MockMbMap {
} }
class MockLayer { class MockLayer {
constructor(layerId, mbSourceIds, mbLayerIdsToSource) { private readonly _mbSourceIds: string[];
private readonly _mbLayerIdsToSource: Array<{ id: string; source: string }>;
constructor(mbSourceIds: string[], mbLayerIdsToSource: Array<{ id: string; source: string }>) {
this._mbSourceIds = mbSourceIds; this._mbSourceIds = mbSourceIds;
this._mbLayerIdsToSource = mbLayerIdsToSource; this._mbLayerIdsToSource = mbLayerIdsToSource;
this._layerId = layerId;
}
getId() {
return this._layerId;
} }
// Custom interface used in getMockStyle for populating maplibre style with layers and sources
getMbSourceIds() { getMbSourceIds() {
return this._mbSourceIds; return this._mbSourceIds;
} }
@ -61,58 +58,55 @@ class MockLayer {
return this._mbLayerIdsToSource; return this._mbLayerIdsToSource;
} }
getMbLayerIds() { // ILayer interface
return this._mbLayerIdsToSource.map(({ id }) => id); ownsMbLayerId(mbLayerId: string) {
}
ownsMbLayerId(mbLayerId) {
return this._mbLayerIdsToSource.some((mbLayerToSource) => { return this._mbLayerIdsToSource.some((mbLayerToSource) => {
return mbLayerToSource.id === mbLayerId; return mbLayerToSource.id === mbLayerId;
}); });
} }
ownsMbSourceId(mbSourceId) { ownsMbSourceId(mbSourceId: string) {
return this._mbSourceIds.some((id) => mbSourceId === id); return this._mbSourceIds.some((id) => mbSourceId === id);
} }
} }
function getMockStyle(orderedMockLayerList) { function getMockStyle(orderedMockLayerList: MockLayer[]) {
const mockStyle = { const mockStyle = {
version: 8 as 8,
sources: {}, sources: {},
layers: [], layers: [],
}; } as StyleSpecification;
orderedMockLayerList.forEach((mockLayer) => { orderedMockLayerList.forEach((mockLayer) => {
mockLayer.getMbSourceIds().forEach((mbSourceId) => { mockLayer.getMbSourceIds().forEach((mbSourceId) => {
mockStyle.sources[mbSourceId] = {}; mockStyle.sources[mbSourceId] = {} as unknown as SourceSpecification;
}); });
mockLayer.getMbLayersIdsToSource().forEach(({ id, source }) => { mockLayer.getMbLayersIdsToSource().forEach(({ id, source }) => {
mockStyle.layers.push({ mockStyle.layers.push({
id: id, id,
source: source, source,
}); } as unknown as LayerSpecification);
}); });
}); });
return mockStyle; return mockStyle;
} }
function makeSingleSourceMockLayer(layerId) { function makeSingleSourceMockLayer(layerId: string) {
const source1 = layerId + '_source1';
return new MockLayer( return new MockLayer(
layerId, [source1],
[layerId],
[ [
{ id: layerId + '_fill', source: layerId }, { id: source1 + '_fill', source: source1 },
{ id: layerId + '_line', source: layerId }, { id: source1 + '_line', source: source1 },
] ]
); );
} }
function makeMultiSourceMockLayer(layerId) { function makeMultiSourceMockLayer(layerId: string) {
const source1 = layerId + '_source1'; const source1 = layerId + '_source1';
const source2 = layerId + '_source2'; const source2 = layerId + '_source2';
return new MockLayer( return new MockLayer(
layerId,
[source1, source2], [source1, source2],
[ [
{ id: source1 + '_fill', source: source1 }, { id: source1 + '_fill', source: source1 },
@ -136,14 +130,18 @@ describe('removeOrphanedSourcesAndLayers', () => {
const currentStyle = getMockStyle(currentLayerList); const currentStyle = getMockStyle(currentLayerList);
const mockMbMap = new MockMbMap(currentStyle); const mockMbMap = new MockMbMap(currentStyle);
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); removeOrphanedSourcesAndLayers(
mockMbMap as unknown as MbMap,
nextLayerList as unknown as ILayer[],
spatialFilterLayer as unknown as ILayer
);
const removedStyle = mockMbMap.getStyle(); const removedStyle = mockMbMap.getStyle();
const nextStyle = getMockStyle(nextLayerList); const nextStyle = getMockStyle(nextLayerList);
expect(removedStyle).toEqual(nextStyle); expect(removedStyle).toEqual(nextStyle);
}); });
test('should remove foo and bar layer (multisource)', async () => { test('should remove foo and bar layer (multisource)', () => {
const bazLayer = makeMultiSourceMockLayer('baz'); const bazLayer = makeMultiSourceMockLayer('baz');
const fooLayer = makeMultiSourceMockLayer('foo'); const fooLayer = makeMultiSourceMockLayer('foo');
const barLayer = makeMultiSourceMockLayer('bar'); const barLayer = makeMultiSourceMockLayer('bar');
@ -154,14 +152,18 @@ describe('removeOrphanedSourcesAndLayers', () => {
const currentStyle = getMockStyle(currentLayerList); const currentStyle = getMockStyle(currentLayerList);
const mockMbMap = new MockMbMap(currentStyle); const mockMbMap = new MockMbMap(currentStyle);
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); removeOrphanedSourcesAndLayers(
mockMbMap as unknown as MbMap,
nextLayerList as unknown as ILayer[],
spatialFilterLayer as unknown as ILayer
);
const removedStyle = mockMbMap.getStyle(); const removedStyle = mockMbMap.getStyle();
const nextStyle = getMockStyle(nextLayerList); const nextStyle = getMockStyle(nextLayerList);
expect(removedStyle).toEqual(nextStyle); expect(removedStyle).toEqual(nextStyle);
}); });
test('should not remove anything', async () => { test('should not remove anything', () => {
const bazLayer = makeSingleSourceMockLayer('baz'); const bazLayer = makeSingleSourceMockLayer('baz');
const fooLayer = makeSingleSourceMockLayer('foo'); const fooLayer = makeSingleSourceMockLayer('foo');
const barLayer = makeSingleSourceMockLayer('bar'); const barLayer = makeSingleSourceMockLayer('bar');
@ -172,30 +174,63 @@ describe('removeOrphanedSourcesAndLayers', () => {
const currentStyle = getMockStyle(currentLayerList); const currentStyle = getMockStyle(currentLayerList);
const mockMbMap = new MockMbMap(currentStyle); const mockMbMap = new MockMbMap(currentStyle);
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); removeOrphanedSourcesAndLayers(
mockMbMap as unknown as MbMap,
nextLayerList as unknown as ILayer[],
spatialFilterLayer as unknown as ILayer
);
const removedStyle = mockMbMap.getStyle(); const removedStyle = mockMbMap.getStyle();
const nextStyle = getMockStyle(nextLayerList); const nextStyle = getMockStyle(nextLayerList);
expect(removedStyle).toEqual(nextStyle); expect(removedStyle).toEqual(nextStyle);
}); });
test('should not remove spatial filter layer and sources when spatialFilterLayer is provided', async () => { test('should not remove spatial filter layer and sources when spatialFilterLayer is provided', () => {
const styleWithSpatialFilters = getMockStyle([spatialFilterLayer]); const styleWithSpatialFilters = getMockStyle([spatialFilterLayer]);
const mockMbMap = new MockMbMap(styleWithSpatialFilters); const mockMbMap = new MockMbMap(styleWithSpatialFilters);
removeOrphanedSourcesAndLayers(mockMbMap, [], spatialFilterLayer); removeOrphanedSourcesAndLayers(
mockMbMap as unknown as MbMap,
[],
spatialFilterLayer as unknown as ILayer
);
expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters); expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters);
}); });
test('should not remove mapbox gl draw layers and sources', async () => { test('should remove sources after spatial filter layer', () => {
const barLayer = makeMultiSourceMockLayer('bar');
const currentLayerList = [spatialFilterLayer, barLayer];
const currentStyle = getMockStyle(currentLayerList);
const mockMbMap = new MockMbMap(currentStyle);
removeOrphanedSourcesAndLayers(
mockMbMap as unknown as MbMap,
[],
spatialFilterLayer as unknown as ILayer
);
const removedStyle = mockMbMap.getStyle();
expect(Object.keys(removedStyle.sources)).toEqual([
'SPATIAL_FILTERS_LAYER_ID_source1',
'SPATIAL_FILTERS_LAYER_ID_source2',
]);
});
test('should not remove mapbox gl draw layers and sources', () => {
const fooLayer = makeMultiSourceMockLayer('foo'); const fooLayer = makeMultiSourceMockLayer('foo');
const layerList = [fooLayer]; const layerList = [fooLayer];
const currentStyle = getMockStyle(layerList); const currentStyle = getMockStyle(layerList);
currentStyle.layers.push({ id: 'gl-draw-points' }); currentStyle.layers.push({ id: 'gl-draw-points' } as unknown as LayerSpecification);
const mockMbMap = new MockMbMap(currentStyle); const mockMbMap = new MockMbMap(currentStyle);
removeOrphanedSourcesAndLayers(mockMbMap, layerList, spatialFilterLayer); removeOrphanedSourcesAndLayers(
mockMbMap as unknown as MbMap,
layerList as unknown as ILayer[],
spatialFilterLayer as unknown as ILayer
);
expect(mockMbMap.getStyle()).toEqual(currentStyle); expect(mockMbMap.getStyle()).toEqual(currentStyle);
}); });
}); });

View file

@ -19,7 +19,6 @@ export function removeOrphanedSourcesAndLayers(
return; return;
} }
const mbLayerIdsToRemove: string[] = [];
mbStyle.layers.forEach((mbLayer) => { mbStyle.layers.forEach((mbLayer) => {
// ignore mapbox layers from spatial filter layer // ignore mapbox layers from spatial filter layer
if (spatialFilterLayer.ownsMbLayerId(mbLayer.id)) { if (spatialFilterLayer.ownsMbLayerId(mbLayer.id)) {
@ -35,26 +34,23 @@ export function removeOrphanedSourcesAndLayers(
return layer.ownsMbLayerId(mbLayer.id); return layer.ownsMbLayerId(mbLayer.id);
}); });
if (!targetLayer) { if (!targetLayer) {
mbLayerIdsToRemove.push(mbLayer.id); mbMap.removeLayer(mbLayer.id);
} }
}); });
mbLayerIdsToRemove.forEach((mbLayerId) => mbMap.removeLayer(mbLayerId));
const mbSourcesToRemove = [];
for (const mbSourceId in mbStyle.sources) { for (const mbSourceId in mbStyle.sources) {
if (mbStyle.sources.hasOwnProperty(mbSourceId)) { if (mbStyle.sources.hasOwnProperty(mbSourceId)) {
// ignore mapbox sources from spatial filter layer // ignore mapbox sources from spatial filter layer
if (spatialFilterLayer.ownsMbSourceId(mbSourceId)) { if (spatialFilterLayer.ownsMbSourceId(mbSourceId)) {
return; continue;
} }
const targetLayer = layerList.find((layer) => { const targetLayer = layerList.find((layer) => {
return layer.ownsMbSourceId(mbSourceId); return layer.ownsMbSourceId(mbSourceId);
}); });
if (!targetLayer) { if (!targetLayer) {
mbSourcesToRemove.push(mbSourceId); mbMap.removeSource(mbSourceId);
} }
} }
} }
mbSourcesToRemove.forEach((mbSourceId) => mbMap.removeSource(mbSourceId));
} }