From bb947ed7bf5fc953647e5d509828e92b82d6db2d Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 25 Jan 2023 20:59:12 -0800 Subject: [PATCH 01/77] [Cypress test fix] Wait map saved before open maps listing (#218) Signed-off-by: Junqiu Lei --- .github/workflows/cypress-workflow.yml | 2 +- cypress/integration/documentsLayer.spec.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 903f0e7f..604f7f7c 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -118,7 +118,7 @@ jobs: # Window is slow so wait longer - name: Sleep until OSD server starts - windows if: ${{ matrix.os == 'windows-latest' }} - run: Start-Sleep -s 400 + run: Start-Sleep -s 600 shell: powershell - name: Sleep until OSD server starts - non-windows diff --git a/cypress/integration/documentsLayer.spec.js b/cypress/integration/documentsLayer.spec.js index 58df4fc3..9e166306 100644 --- a/cypress/integration/documentsLayer.spec.js +++ b/cypress/integration/documentsLayer.spec.js @@ -45,6 +45,7 @@ describe('Documents layer', () => { cy.wait(5000).get('[data-test-subj="top-nav"]').click(); cy.wait(5000).get('[data-test-subj="savedObjectTitle"]').type(uniqueName); cy.wait(5000).get('[data-test-subj="confirmSaveSavedObjectButton"]').click(); + cy.wait(5000).get('[data-test-subj="breadcrumb last"]').should('contain', uniqueName); }); it('Open saved map with documents layer', () => { From 538628c448503b819cfbbb5a1462dce97efd9d54 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Mon, 30 Jan 2023 19:59:14 -0800 Subject: [PATCH 02/77] Refactor add layer operations (#222) * Refactor add geoshape layers outside document layer * Update layer opeartions * Add unit test Signed-off-by: Vijayan Balasubramanian --- public/model/documentLayerFunctions.ts | 314 ++++++---------------- public/model/map/__mocks__/layer.ts | 25 ++ public/model/map/__mocks__/map.ts | 54 ++++ public/model/map/layer_operations.test.ts | 279 +++++++++++++++++++ public/model/map/layer_operations.ts | 180 +++++++++++++ 5 files changed, 622 insertions(+), 230 deletions(-) create mode 100644 public/model/map/__mocks__/layer.ts create mode 100644 public/model/map/__mocks__/map.ts create mode 100644 public/model/map/layer_operations.test.ts create mode 100644 public/model/map/layer_operations.ts diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index befcf5b5..0dfce730 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -8,6 +8,14 @@ import { parse } from 'wellknown'; import { DocumentLayerSpecification } from './mapLayerType'; import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { + addCircleLayer, + addLineLayer, + addPolygonLayer, + updateCircleLayer, + updateLineLayer, + updatePolygonLayer, +} from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -23,23 +31,6 @@ const openSearchGeoJSONMap = new Map([ ['geometrycollection', 'GeometryCollection'], ]); -const buildLayerSuffix = (layerId: string, mapLibreType: string) => { - if (mapLibreType.toLowerCase() === 'circle') { - return layerId; - } - if (mapLibreType.toLowerCase() === 'line') { - return layerId + '-line'; - } - if (mapLibreType.toLowerCase() === 'fill') { - return layerId + '-fill'; - } - if (mapLibreType.toLowerCase() === 'fill-outline') { - return layerId + '-outline'; - } - // if unknown type is found, use layerId as default - return layerId; -}; - const getFieldValue = (data: any, name: string) => { if (!name) { return null; @@ -128,226 +119,62 @@ const addNewLayer = ( beforeLayerId: string | undefined ) => { const maplibreInstance = maplibreRef.current; + if (!maplibreInstance) { + return; + } const mbLayerBeforeId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); - const addLineLayer = ( - documentLayerConfig: DocumentLayerSpecification, - beforeId: string | undefined - ) => { - const lineLayerId = buildLayerSuffix(documentLayerConfig.id, 'line'); - maplibreInstance?.addLayer( - { - id: lineLayerId, - type: 'line', - source: documentLayerConfig.id, - filter: ['==', '$type', 'LineString'], - paint: { - 'line-color': documentLayerConfig.style?.fillColor, - 'line-opacity': documentLayerConfig.opacity / 100, - 'line-width': documentLayerConfig.style?.borderThickness, - }, - }, - beforeId - ); - maplibreInstance?.setLayoutProperty(lineLayerId, 'visibility', documentLayerConfig.visibility); - maplibreInstance?.setLayerZoomRange( - lineLayerId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] - ); - }; - - const addCircleLayer = ( - documentLayerConfig: DocumentLayerSpecification, - beforeId: string | undefined - ) => { - const circleLayerId = buildLayerSuffix(documentLayerConfig.id, 'circle'); - maplibreInstance?.addLayer( - { - id: circleLayerId, - type: 'circle', - source: layerConfig.id, - filter: ['==', '$type', 'Point'], - paint: { - 'circle-radius': documentLayerConfig.style?.markerSize, - 'circle-color': documentLayerConfig.style?.fillColor, - 'circle-opacity': documentLayerConfig.opacity / 100, - 'circle-stroke-width': documentLayerConfig.style?.borderThickness, - 'circle-stroke-color': documentLayerConfig.style?.borderColor, - }, - }, - beforeId - ); - maplibreInstance?.setLayoutProperty( - circleLayerId, - 'visibility', - documentLayerConfig.visibility - ); - maplibreInstance?.setLayerZoomRange( - circleLayerId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] - ); - }; - - const addFillLayer = ( - documentLayerConfig: DocumentLayerSpecification, - beforeId: string | undefined - ) => { - const fillLayerId = buildLayerSuffix(documentLayerConfig.id, 'fill'); - maplibreInstance?.addLayer( + const source = getLayerSource(data, layerConfig); + maplibreInstance.addSource(layerConfig.id, { + type: 'geojson', + data: source, + }); + addCircleLayer( + maplibreInstance, + { + fillColor: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + outlineColor: layerConfig.style?.borderColor, + radius: layerConfig.style?.markerSize, + sourceId: layerConfig.id, + visibility: layerConfig.visibility, + width: layerConfig.style?.borderThickness, + }, + mbLayerBeforeId + ); + const geoFieldType = getGeoFieldType(layerConfig); + if (geoFieldType === 'geo_shape') { + addLineLayer( + maplibreInstance, { - id: fillLayerId, - type: 'fill', - source: layerConfig.id, - filter: ['==', '$type', 'Polygon'], - paint: { - 'fill-color': documentLayerConfig.style?.fillColor, - 'fill-opacity': documentLayerConfig.opacity / 100, - }, + width: layerConfig.style?.borderThickness, + color: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + sourceId: layerConfig.id, + visibility: layerConfig.visibility, }, - beforeId - ); - maplibreInstance?.setLayoutProperty(fillLayerId, 'visibility', documentLayerConfig.visibility); - maplibreInstance?.setLayerZoomRange( - fillLayerId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] + mbLayerBeforeId ); - // Due to limitations on WebGL, fill can't render outlines with width wider than 1, - // so we have to create another style layer with type=line to apply width. - const outlineId = buildLayerSuffix(documentLayerConfig.id, 'fill-outline'); - maplibreInstance?.addLayer( + addPolygonLayer( + maplibreInstance, { - id: outlineId, - type: 'line', - source: layerConfig.id, - filter: ['==', '$type', 'Polygon'], - paint: { - 'line-color': layerConfig.style?.borderColor, - 'line-opacity': layerConfig.opacity / 100, - 'line-width': layerConfig.style?.borderThickness, - }, + width: layerConfig.style?.borderThickness, + fillColor: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + sourceId: layerConfig.id, + outlineColor: layerConfig.style?.borderColor, + visibility: layerConfig.visibility, }, - beforeId + mbLayerBeforeId ); - maplibreInstance?.setLayoutProperty(outlineId, 'visibility', layerConfig.visibility); - maplibreInstance?.setLayerZoomRange( - outlineId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] - ); - }; - - if (maplibreInstance) { - const source = getLayerSource(data, layerConfig); - maplibreInstance.addSource(layerConfig.id, { - type: 'geojson', - data: source, - }); - addCircleLayer(layerConfig, mbLayerBeforeId); - const geoFieldType = getGeoFieldType(layerConfig); - if (geoFieldType === 'geo_shape') { - addLineLayer(layerConfig, mbLayerBeforeId); - addFillLayer(layerConfig, mbLayerBeforeId); - } } }; -const updateCircleLayer = ( - maplibreInstance: Maplibre, - documentLayerConfig: DocumentLayerSpecification -) => { - const circleLayerId = buildLayerSuffix(documentLayerConfig.id, 'circle'); - const circleLayerStyle = documentLayerConfig.style; - maplibreInstance?.setLayerZoomRange( - circleLayerId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] - ); - maplibreInstance?.setPaintProperty( - circleLayerId, - 'circle-opacity', - documentLayerConfig.opacity / 100 - ); - maplibreInstance?.setPaintProperty(circleLayerId, 'circle-color', circleLayerStyle?.fillColor); - maplibreInstance?.setPaintProperty( - circleLayerId, - 'circle-stroke-color', - circleLayerStyle?.borderColor - ); - maplibreInstance?.setPaintProperty( - circleLayerId, - 'circle-stroke-width', - circleLayerStyle?.borderThickness - ); - maplibreInstance?.setPaintProperty(circleLayerId, 'circle-radius', circleLayerStyle?.markerSize); -}; - -const updateLineLayer = ( - maplibreInstance: Maplibre, - documentLayerConfig: DocumentLayerSpecification -) => { - const lineLayerId = buildLayerSuffix(documentLayerConfig.id, 'line'); - maplibreInstance?.setLayerZoomRange( - lineLayerId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] - ); - maplibreInstance?.setPaintProperty( - lineLayerId, - 'line-opacity', - documentLayerConfig.opacity / 100 - ); - maplibreInstance?.setPaintProperty( - lineLayerId, - 'line-color', - documentLayerConfig.style?.fillColor - ); - maplibreInstance?.setPaintProperty( - lineLayerId, - 'line-width', - documentLayerConfig.style?.borderThickness - ); -}; - -const updateFillLayer = ( - maplibreInstance: Maplibre, - documentLayerConfig: DocumentLayerSpecification -) => { - const fillLayerId = buildLayerSuffix(documentLayerConfig.id, 'fill'); - maplibreInstance?.setLayerZoomRange( - fillLayerId, - documentLayerConfig.zoomRange[0], - documentLayerConfig.zoomRange[1] - ); - maplibreInstance?.setPaintProperty( - fillLayerId, - 'fill-opacity', - documentLayerConfig.opacity / 100 - ); - maplibreInstance?.setPaintProperty( - fillLayerId, - 'fill-color', - documentLayerConfig.style?.fillColor - ); - maplibreInstance?.setPaintProperty( - fillLayerId, - 'fill-outline-color', - documentLayerConfig.style?.borderColor - ); - const outlineLayerId = buildLayerSuffix(documentLayerConfig.id, 'fill-outline'); - maplibreInstance?.setPaintProperty( - outlineLayerId, - 'line-color', - documentLayerConfig.style?.borderColor - ); - maplibreInstance?.setPaintProperty( - outlineLayerId, - 'line-width', - documentLayerConfig.style?.borderThickness - ); -}; - const updateLayerConfig = ( layerConfig: DocumentLayerSpecification, maplibreRef: MaplibreRef, @@ -360,11 +187,38 @@ const updateLayerConfig = ( // @ts-ignore dataSource.setData(getLayerSource(data, layerConfig)); } - updateCircleLayer(maplibreInstance, layerConfig); + updateCircleLayer(maplibreInstance, { + fillColor: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + outlineColor: layerConfig.style?.borderColor, + radius: layerConfig.style?.markerSize, + sourceId: layerConfig.id, + visibility: layerConfig.visibility, + width: layerConfig.style?.borderThickness, + }); const geoFieldType = getGeoFieldType(layerConfig); if (geoFieldType === 'geo_shape') { - updateLineLayer(maplibreInstance, layerConfig); - updateFillLayer(maplibreInstance, layerConfig); + updateLineLayer(maplibreInstance, { + width: layerConfig.style?.borderThickness, + color: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + sourceId: layerConfig.id, + visibility: layerConfig.visibility, + }); + updatePolygonLayer(maplibreInstance, { + width: layerConfig.style?.borderThickness, + fillColor: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + sourceId: layerConfig.id, + outlineColor: layerConfig.style?.borderColor, + visibility: layerConfig.visibility, + }); } } }; diff --git a/public/model/map/__mocks__/layer.ts b/public/model/map/__mocks__/layer.ts new file mode 100644 index 00000000..1f307ff3 --- /dev/null +++ b/public/model/map/__mocks__/layer.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export class MockLayer { + private layerProperties: Map = new Map(); + + constructor(id: string) { + this.layerProperties.set('id', id); + } + + public setProperty(name: string, value: any): this { + this.layerProperties.set(name, value); + return this; + } + + public getProperty(name: string): any { + return this.layerProperties.get(name); + } + + public hasProperty(name: string): boolean { + return this.layerProperties.has(name); + } +} diff --git a/public/model/map/__mocks__/map.ts b/public/model/map/__mocks__/map.ts new file mode 100644 index 00000000..cf89df85 --- /dev/null +++ b/public/model/map/__mocks__/map.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LayerSpecification } from 'maplibre-gl'; +import { MockLayer } from './layer'; + +export class MockMaplibreMap { + public _layers: MockLayer[]; + + constructor() { + this._layers = new Array(); + } + + getLayer(id: string): MockLayer[] { + return this._layers.filter((layer) => (layer.getProperty('id') as string).includes(id)); + } + + public setLayerZoomRange(layerId: string, minZoom: number, maxZoom: number) { + this.setProperty(layerId, 'minZoom', minZoom); + this.setProperty(layerId, 'maxZoom', maxZoom); + } + + public setLayoutProperty(layerId: string, property: string, value: any) { + this.setProperty(layerId, property, value); + } + + public setPaintProperty(layerId: string, property: string, value: any) { + this.setProperty(layerId, property, value); + } + + public setProperty(layerId: string, property: string, value: any) { + this.getLayer(layerId)?.forEach((layer) => { + layer.setProperty(property, value); + }); + } + + addLayer(layerSpec: LayerSpecification, beforeId?: string) { + const layer: MockLayer = new MockLayer(layerSpec.id); + Object.keys(layerSpec).forEach((key) => { + // @ts-ignore + layer.setProperty(key, layerSpec[key]); + }); + if (!beforeId) { + this._layers.push(layer); + return; + } + const beforeLayerIndex = this._layers.findIndex((l) => { + return l.getProperty('id') === beforeId; + }); + this._layers.splice(beforeLayerIndex, 0, layer); + } +} diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts new file mode 100644 index 00000000..779fdb4f --- /dev/null +++ b/public/model/map/layer_operations.test.ts @@ -0,0 +1,279 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { + addCircleLayer, + addLineLayer, + addPolygonLayer, + updateCircleLayer, + updateLineLayer, + updatePolygonLayer, +} from './layer_operations'; +import { Map as Maplibre } from 'maplibre-gl'; +import { MockMaplibreMap } from './__mocks__/map'; + +describe('Circle layer', () => { + it('add new circle layer', () => { + const mockMap = new MockMaplibreMap(); + const sourceId: string = 'geojson-source'; + const expectedLayerId: string = sourceId + '-circle'; + expect(mockMap.getLayer(expectedLayerId).length).toBe(0); + expect( + addCircleLayer((mockMap as unknown) as Maplibre, { + maxZoom: 10, + minZoom: 2, + opacity: 60, + outlineColor: 'green', + radius: 10, + sourceId, + visibility: 'visible', + width: 2, + fillColor: 'red', + }) + ).toBe(expectedLayerId); + expect(mockMap.getLayer(sourceId).length).toBe(1); + + const addedLayer = mockMap.getLayer(sourceId)[0]; + + expect(addedLayer.getProperty('id')).toBe(expectedLayerId); + expect(addedLayer.getProperty('visibility')).toBe('visible'); + expect(addedLayer.getProperty('source')).toBe(sourceId); + expect(addedLayer.getProperty('type')).toBe('circle'); + expect(addedLayer.getProperty('filter')).toEqual(['==', '$type', 'Point']); + expect(addedLayer.getProperty('minZoom')).toBe(2); + expect(addedLayer.getProperty('maxZoom')).toBe(10); + expect(addedLayer.getProperty('circle-opacity')).toBe(0.6); + expect(addedLayer.getProperty('circle-color')).toBe('red'); + expect(addedLayer.getProperty('circle-stroke-color')).toBe('green'); + expect(addedLayer.getProperty('circle-stroke-width')).toBe(2); + expect(addedLayer.getProperty('circle-radius')).toBe(10); + }); + + it('update circle layer', () => { + const mockMap = new MockMaplibreMap(); + const sourceId: string = 'geojson-source'; + + // add layer first + const addedLayerId: string = addCircleLayer((mockMap as unknown) as Maplibre, { + maxZoom: 10, + minZoom: 2, + opacity: 60, + outlineColor: 'green', + radius: 10, + sourceId, + visibility: 'visible', + width: 2, + fillColor: 'red', + }); + expect( + updateCircleLayer((mockMap as unknown) as Maplibre, { + maxZoom: 12, + minZoom: 4, + opacity: 80, + outlineColor: 'yellow', + radius: 8, + sourceId, + visibility: 'none', + width: 7, + fillColor: 'blue', + }) + ).toBe(addedLayerId); + expect(mockMap.getLayer(addedLayerId).length).toBe(1); + + const updatedLayer = mockMap.getLayer(addedLayerId)[0]; + expect(updatedLayer.getProperty('id')).toBe(addedLayerId); + expect(updatedLayer.getProperty('visibility')).toBe('none'); + expect(updatedLayer.getProperty('source')).toBe(sourceId); + expect(updatedLayer.getProperty('type')).toBe('circle'); + expect(updatedLayer.getProperty('filter')).toEqual(['==', '$type', 'Point']); + expect(updatedLayer.getProperty('minZoom')).toBe(4); + expect(updatedLayer.getProperty('maxZoom')).toBe(12); + expect(updatedLayer.getProperty('circle-opacity')).toBe(0.8); + expect(updatedLayer.getProperty('circle-color')).toBe('blue'); + expect(updatedLayer.getProperty('circle-stroke-color')).toBe('yellow'); + expect(updatedLayer.getProperty('circle-stroke-width')).toBe(7); + expect(updatedLayer.getProperty('circle-radius')).toBe(8); + }); +}); + +describe('Line layer', () => { + it('add new Line layer', () => { + const mockMap = new MockMaplibreMap(); + const sourceId: string = 'geojson-source'; + const expectedLayerId: string = sourceId + '-line'; + expect(mockMap.getLayer(expectedLayerId).length).toBe(0); + expect( + addLineLayer((mockMap as unknown) as Maplibre, { + color: 'red', + maxZoom: 10, + minZoom: 2, + opacity: 60, + sourceId, + visibility: 'visible', + width: 2, + }) + ).toBe(expectedLayerId); + expect(mockMap.getLayer(sourceId).length).toBe(1); + const addedLayer = mockMap.getLayer(sourceId)[0]; + expect(addedLayer.getProperty('id')).toBe(expectedLayerId); + expect(addedLayer.getProperty('visibility')).toBe('visible'); + expect(addedLayer.getProperty('source')).toBe(sourceId); + expect(addedLayer.getProperty('type')).toBe('line'); + expect(addedLayer.getProperty('filter')).toEqual(['==', '$type', 'LineString']); + expect(addedLayer.getProperty('minZoom')).toBe(2); + expect(addedLayer.getProperty('maxZoom')).toBe(10); + expect(addedLayer.getProperty('line-opacity')).toBe(0.6); + expect(addedLayer.getProperty('line-color')).toBe('red'); + expect(addedLayer.getProperty('line-width')).toBe(2); + }); + + it('update line layer', () => { + const mockMap = new MockMaplibreMap(); + const sourceId: string = 'geojson-source'; + + // add layer first + const addedLineLayerId: string = addLineLayer((mockMap as unknown) as Maplibre, { + color: 'red', + maxZoom: 10, + minZoom: 2, + opacity: 60, + sourceId, + visibility: 'visible', + width: 2, + }); + expect( + updateLineLayer((mockMap as unknown) as Maplibre, { + color: 'blue', + maxZoom: 12, + minZoom: 4, + opacity: 80, + sourceId, + visibility: 'none', + width: 12, + }) + ).toBe(addedLineLayerId); + expect(mockMap.getLayer(addedLineLayerId).length).toBe(1); + + const updatedLayer = mockMap.getLayer(addedLineLayerId)[0]; + expect(updatedLayer.getProperty('id')).toBe(addedLineLayerId); + expect(updatedLayer.getProperty('visibility')).toBe('none'); + expect(updatedLayer.getProperty('source')).toBe(sourceId); + expect(updatedLayer.getProperty('type')).toBe('line'); + expect(updatedLayer.getProperty('filter')).toEqual(['==', '$type', 'LineString']); + expect(updatedLayer.getProperty('minZoom')).toBe(4); + expect(updatedLayer.getProperty('maxZoom')).toBe(12); + expect(updatedLayer.getProperty('line-opacity')).toBe(0.8); + expect(updatedLayer.getProperty('line-color')).toBe('blue'); + expect(updatedLayer.getProperty('line-width')).toBe(12); + }); +}); + +describe('Polygon layer', () => { + it('add new polygon layer', () => { + const mockMap = new MockMaplibreMap(); + const sourceId: string = 'geojson-source'; + const expectedFillLayerId = sourceId + '-fill'; + const expectedOutlineLayerId = expectedFillLayerId + '-outline'; + expect(mockMap.getLayer(expectedFillLayerId).length).toBe(0); + expect(mockMap.getLayer(expectedOutlineLayerId).length).toBe(0); + addPolygonLayer((mockMap as unknown) as Maplibre, { + maxZoom: 10, + minZoom: 2, + opacity: 60, + outlineColor: 'green', + sourceId, + visibility: 'visible', + width: 2, + fillColor: 'red', + }); + expect(mockMap.getLayer(sourceId).length).toBe(2); + + const fillLayer = mockMap + .getLayer(sourceId) + .filter((layer) => layer.getProperty('id').toString().endsWith('-fill'))[0]; + + expect(fillLayer.getProperty('id')).toBe(expectedFillLayerId); + expect(fillLayer.getProperty('visibility')).toBe('visible'); + expect(fillLayer.getProperty('source')).toBe(sourceId); + expect(fillLayer.getProperty('type')).toBe('fill'); + expect(fillLayer.getProperty('filter')).toEqual(['==', '$type', 'Polygon']); + expect(fillLayer.getProperty('minZoom')).toBe(2); + expect(fillLayer.getProperty('maxZoom')).toBe(10); + expect(fillLayer.getProperty('fill-opacity')).toBe(0.6); + expect(fillLayer.getProperty('fill-color')).toBe('red'); + const outlineLayer = mockMap + .getLayer(sourceId) + .filter((layer) => layer.getProperty('id').toString().endsWith('-fill-outline'))[0]; + expect(outlineLayer.getProperty('id')).toBe(expectedOutlineLayerId); + expect(outlineLayer.getProperty('visibility')).toBe('visible'); + expect(outlineLayer.getProperty('source')).toBe(sourceId); + expect(outlineLayer.getProperty('type')).toBe('line'); + expect(outlineLayer.getProperty('filter')).toEqual(['==', '$type', 'Polygon']); + expect(outlineLayer.getProperty('minZoom')).toBe(2); + expect(outlineLayer.getProperty('maxZoom')).toBe(10); + expect(outlineLayer.getProperty('line-opacity')).toBe(0.6); + expect(outlineLayer.getProperty('line-color')).toBe('green'); + expect(outlineLayer.getProperty('line-width')).toBe(2); + }); + + it('update polygon layer', () => { + const mockMap = new MockMaplibreMap(); + const sourceId: string = 'geojson-source'; + + const expectedFillLayerId = sourceId + '-fill'; + const expectedOutlineLayerId = expectedFillLayerId + '-outline'; + // add layer first + addPolygonLayer((mockMap as unknown) as Maplibre, { + maxZoom: 10, + minZoom: 2, + opacity: 60, + outlineColor: 'green', + sourceId, + visibility: 'visible', + width: 2, + fillColor: 'red', + }); + + expect(mockMap.getLayer(sourceId).length).toBe(2); + // update polygon for test + updatePolygonLayer((mockMap as unknown) as Maplibre, { + maxZoom: 12, + minZoom: 4, + opacity: 80, + outlineColor: 'yellow', + sourceId, + visibility: 'none', + width: 7, + fillColor: 'blue', + }); + + expect(mockMap.getLayer(sourceId).length).toBe(2); + const fillLayer = mockMap + .getLayer(sourceId) + .filter((layer) => layer.getProperty('id') === expectedFillLayerId)[0]; + + expect(fillLayer.getProperty('id')).toBe(expectedFillLayerId); + expect(fillLayer.getProperty('visibility')).toBe('none'); + expect(fillLayer.getProperty('source')).toBe(sourceId); + expect(fillLayer.getProperty('type')).toBe('fill'); + expect(fillLayer.getProperty('filter')).toEqual(['==', '$type', 'Polygon']); + expect(fillLayer.getProperty('minZoom')).toBe(4); + expect(fillLayer.getProperty('maxZoom')).toBe(12); + expect(fillLayer.getProperty('fill-opacity')).toBe(0.8); + expect(fillLayer.getProperty('fill-color')).toBe('blue'); + const outlineLayer = mockMap + .getLayer(sourceId) + .filter((layer) => layer.getProperty('id') === expectedOutlineLayerId)[0]; + expect(outlineLayer.getProperty('id')).toBe(expectedOutlineLayerId); + expect(outlineLayer.getProperty('visibility')).toBe('none'); + expect(outlineLayer.getProperty('source')).toBe(sourceId); + expect(outlineLayer.getProperty('type')).toBe('line'); + expect(outlineLayer.getProperty('filter')).toEqual(['==', '$type', 'Polygon']); + expect(outlineLayer.getProperty('minZoom')).toBe(4); + expect(outlineLayer.getProperty('maxZoom')).toBe(12); + expect(outlineLayer.getProperty('line-opacity')).toBe(0.8); + expect(outlineLayer.getProperty('line-color')).toBe('yellow'); + expect(outlineLayer.getProperty('line-width')).toBe(7); + }); +}); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts new file mode 100644 index 00000000..948f06c1 --- /dev/null +++ b/public/model/map/layer_operations.ts @@ -0,0 +1,180 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Map as Maplibre } from 'maplibre-gl'; + +export interface LineLayerSpecification { + sourceId: string; + visibility: string; + color: string; + opacity: number; + width: number; + minZoom: number; + maxZoom: number; +} + +export const addLineLayer = ( + map: Maplibre, + specification: LineLayerSpecification, + beforeId?: string +): string => { + const lineLayerId = specification.sourceId + '-line'; + map.addLayer( + { + id: lineLayerId, + type: 'line', + source: specification.sourceId, + filter: ['==', '$type', 'LineString'], + }, + beforeId + ); + return updateLineLayer(map, specification, lineLayerId); +}; + +export const updateLineLayer = ( + map: Maplibre, + specification: LineLayerSpecification, + layerId?: string +): string => { + const lineLayerId = layerId ? layerId : specification.sourceId + '-line'; + map.setPaintProperty(lineLayerId, 'line-opacity', specification.opacity / 100); + map.setPaintProperty(lineLayerId, 'line-color', specification.color); + map.setPaintProperty(lineLayerId, 'line-width', specification.width); + map.setLayoutProperty(lineLayerId, 'visibility', specification.visibility); + map.setLayerZoomRange(lineLayerId, specification.minZoom, specification.maxZoom); + return lineLayerId; +}; + +export interface CircleLayerSpecification { + sourceId: string; + visibility: string; + fillColor: string; + outlineColor: string; + radius: number; + opacity: number; + width: number; + minZoom: number; + maxZoom: number; +} + +export const addCircleLayer = ( + map: Maplibre, + specification: CircleLayerSpecification, + beforeId?: string +): string => { + const circleLayerId = specification.sourceId + '-circle'; + map.addLayer( + { + id: circleLayerId, + type: 'circle', + source: specification.sourceId, + filter: ['==', '$type', 'Point'], + }, + beforeId + ); + return updateCircleLayer(map, specification, circleLayerId); +}; + +export const updateCircleLayer = ( + map: Maplibre, + specification: CircleLayerSpecification, + layerId?: string +): string => { + const circleLayerId = layerId ? layerId : specification.sourceId + '-circle'; + map.setLayoutProperty(circleLayerId, 'visibility', specification.visibility); + map.setLayerZoomRange(circleLayerId, specification.minZoom, specification.maxZoom); + map.setPaintProperty(circleLayerId, 'circle-opacity', specification.opacity / 100); + map.setPaintProperty(circleLayerId, 'circle-color', specification.fillColor); + map.setPaintProperty(circleLayerId, 'circle-stroke-color', specification.outlineColor); + map.setPaintProperty(circleLayerId, 'circle-stroke-width', specification.width); + map.setPaintProperty(circleLayerId, 'circle-stroke-opacity', specification.opacity / 100); + map.setPaintProperty(circleLayerId, 'circle-radius', specification.radius); + return circleLayerId; +}; + +export interface PolygonLayerSpecification { + sourceId: string; + visibility: string; + fillColor: string; + outlineColor: string; + opacity: number; + width: number; + minZoom: number; + maxZoom: number; +} + +export const addPolygonLayer = ( + map: Maplibre, + specification: PolygonLayerSpecification, + beforeId?: string +) => { + const fillLayerId = specification.sourceId + '-fill'; + map.addLayer( + { + id: fillLayerId, + type: 'fill', + source: specification.sourceId, + filter: ['==', '$type', 'Polygon'], + }, + beforeId + ); + updatePolygonFillLayer(map, specification, fillLayerId); + + // Due to limitations on WebGL, fill can't render outlines with width wider than 1, + // so we have to create another style layer with type=line to apply width. + const outlineId = fillLayerId + '-outline'; + map.addLayer( + { + id: outlineId, + type: 'line', + source: specification.sourceId, + filter: ['==', '$type', 'Polygon'], + }, + beforeId + ); + updateLineLayer( + map, + { + width: specification.width, + color: specification.outlineColor, + maxZoom: specification.maxZoom, + minZoom: specification.minZoom, + opacity: specification.opacity, + sourceId: specification.sourceId, + visibility: specification.visibility, + }, + outlineId + ); +}; + +export const updatePolygonLayer = (map: Maplibre, specification: PolygonLayerSpecification) => { + const fillLayerId: string = updatePolygonFillLayer(map, specification); + const outlineLayerId: string = fillLayerId + '-outline'; + updateLineLayer( + map, + { + width: specification.width, + color: specification.outlineColor, + maxZoom: specification.maxZoom, + minZoom: specification.minZoom, + opacity: specification.opacity, + sourceId: specification.sourceId, + visibility: specification.visibility, + }, + outlineLayerId + ); +}; + +const updatePolygonFillLayer = ( + map: Maplibre, + specification: PolygonLayerSpecification, + layerId?: string +): string => { + const fillLayerId = layerId ? layerId : specification.sourceId + '-fill'; + map.setLayoutProperty(fillLayerId, 'visibility', specification.visibility); + map.setLayerZoomRange(fillLayerId, specification.minZoom, specification.maxZoom); + map.setPaintProperty(fillLayerId, 'fill-opacity', specification.opacity / 100); + map.setPaintProperty(fillLayerId, 'fill-color', specification.fillColor); + return fillLayerId; +}; From ab56667adaa34cf8e8b894fe00130cf8c90f93e9 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Tue, 31 Jan 2023 17:06:56 -0800 Subject: [PATCH 03/77] Refactor layer operations (#224) * Add layer as constructor parameter * Refactor get/has layer * Refactor remove layer * Refactor visibility property * Rename visibility layer Signed-off-by: Vijayan Balasubramanian --- .../layer_control_panel.tsx | 8 +- public/model/OSMLayerFunctions.ts | 74 ++++-------- public/model/customLayerFunctions.ts | 26 +---- public/model/documentLayerFunctions.ts | 47 +++----- public/model/layersFunctions.ts | 48 +------- public/model/map/__mocks__/map.ts | 72 ++++++++++-- public/model/map/layer_operations.test.ts | 109 ++++++++++++++++-- public/model/map/layer_operations.ts | 46 +++++++- 8 files changed, 265 insertions(+), 165 deletions(-) diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 1d536281..310ecd91 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -48,6 +48,7 @@ import { } from '../../model/layerRenderController'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; +import {moveLayers, removeLayers, updateLayerVisibility} from "../../model/map/layer_operations"; interface MaplibreRef { current: Maplibre | null; @@ -241,7 +242,8 @@ export const LayerControlPanel = memo( const currentMaplibreLayerId = layers[prevIndex].id; const beforeMaplibreLayerId = beforeMaplibreLayerID(prevIndex, newIndex); - LayerActions.move(maplibreRef, currentMaplibreLayerId, beforeMaplibreLayerId); + + moveLayers(maplibreRef.current!, currentMaplibreLayerId, beforeMaplibreLayerId); // update map layers const layersClone = [...layers]; @@ -288,7 +290,7 @@ export const LayerControlPanel = memo( layer.visibility = LAYER_VISIBILITY.VISIBLE; setLayerVisibility(new Map(layerVisibility.set(layer.id, true))); } - layersFunctionMap[layer.type]?.hide(maplibreRef, layer); + updateLayerVisibility(maplibreRef.current!, layer.id, layer.visibility); }; const onDeleteLayerIconClick = (layer: MapLayerSpecification) => { @@ -298,7 +300,7 @@ export const LayerControlPanel = memo( const onDeleteLayerConfirm = () => { if (selectedDeleteLayer) { - layersFunctionMap[selectedDeleteLayer.type]?.remove(maplibreRef, selectedDeleteLayer); + removeLayers(maplibreRef.current!, selectedDeleteLayer.id, true); removeLayer(selectedDeleteLayer.id); setIsDeleteLayerModalVisible(false); setSelectedDeleteLayer(undefined); diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index ca879121..871904f0 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -1,6 +1,7 @@ import { Map as Maplibre, LayerSpecification } from 'maplibre-gl'; import { OSMLayerSpecification } from './mapLayerType'; -import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { getMaplibreBeforeLayerId } from './layersFunctions'; +import { getLayers, hasLayer } from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -17,36 +18,27 @@ const fetchStyleLayers = (url: string) => { }); }; -const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { - return maplibreRef.current?.getStyle().layers || []; -}; - const handleStyleLayers = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((mbLayer) => { - if (mbLayer.id.includes(layerConfig.id)) { - maplibreRef.current?.setLayerZoomRange( - mbLayer.id, - layerConfig.zoomRange[0], - layerConfig.zoomRange[1] - ); - // TODO: figure out error reason - if (mbLayer.type === 'symbol') { - return; - } - maplibreRef.current?.setPaintProperty( - mbLayer.id, - `${mbLayer.type}-opacity`, - layerConfig.opacity / 100 - ); + getLayers(maplibreRef.current!, layerConfig.id).forEach((mbLayer) => { + maplibreRef.current?.setLayerZoomRange( + mbLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + // TODO: figure out error reason + if (mbLayer.type === 'symbol') { + return; } + maplibreRef.current?.setPaintProperty( + mbLayer.id, + `${mbLayer.type}-opacity`, + layerConfig.opacity / 100 + ); }); }; const updateLayerConfig = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { - if (maplibreRef.current) { - handleStyleLayers(layerConfig, maplibreRef); - } + handleStyleLayers(layerConfig, maplibreRef); }; const addNewLayer = ( @@ -55,16 +47,16 @@ const addNewLayer = ( beforeLayerId: string | undefined ) => { if (maplibreRef.current) { - const layerSource = layerConfig?.source; - const layerStyle = layerConfig?.style; + const { source, style } = layerConfig; maplibreRef.current.addSource(layerConfig.id, { type: 'vector', - url: layerSource?.dataURL, + url: source?.dataURL, }); - fetchStyleLayers(layerStyle?.styleURL).then((styleLayers: LayerSpecification[]) => { + fetchStyleLayers(style?.styleURL).then((styleLayers: LayerSpecification[]) => { const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); styleLayers.forEach((styleLayer) => { styleLayer.id = styleLayer.id + '_' + layerConfig.id; + // TODO: Add comments on why we skip background type if (styleLayer.type !== 'background') { styleLayer.source = layerConfig.id; } @@ -98,26 +90,8 @@ export const OSMLayerFunctions = { ) => { // If layer already exist in maplibre source, update layer config // else add new layer. - if (layerExistInMbSource(layerConfig.id, maplibreRef)) { - updateLayerConfig(layerConfig, maplibreRef); - } else { - addNewLayer(layerConfig, maplibreRef, beforeLayerId); - } - }, - remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((mbLayer: { id: any }) => { - if (mbLayer.id.includes(layerConfig.id)) { - maplibreRef.current?.removeLayer(mbLayer.id); - } - }); - }, - hide: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((mbLayer: { id: any }) => { - if (mbLayer.id.includes(layerConfig.id)) { - maplibreRef.current?.setLayoutProperty(mbLayer.id, 'visibility', layerConfig.visibility); - } - }); + return hasLayer(maplibreRef.current!, layerConfig.id) + ? updateLayerConfig(layerConfig, maplibreRef) + : addNewLayer(layerConfig, maplibreRef, beforeLayerId); }, }; diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index b8f43bd9..2c675434 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -1,6 +1,7 @@ import { Map as Maplibre, AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; -import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { getMaplibreBeforeLayerId } from './layersFunctions'; +import { hasLayer, removeLayers } from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -97,26 +98,11 @@ export const CustomLayerFunctions = { layerConfig: CustomLayerSpecification, beforeLayerId: string | undefined ) => { - if (layerExistInMbSource(layerConfig.id, maplibreRef)) { - updateLayerConfig(layerConfig, maplibreRef); - } else { - addNewLayer(layerConfig, maplibreRef, beforeLayerId); - } + return hasLayer(maplibreRef.current!, layerConfig.id) + ? updateLayerConfig(layerConfig, maplibreRef) + : addNewLayer(layerConfig, maplibreRef, beforeLayerId); }, remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((mbLayer: { id: any }) => { - if (mbLayer.id.includes(layerConfig.id)) { - maplibreRef.current?.removeLayer(mbLayer.id); - } - }); - }, - hide: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((mbLayer: { id: any }) => { - if (mbLayer.id.includes(layerConfig.id)) { - maplibreRef.current?.setLayoutProperty(mbLayer.id, 'visibility', layerConfig.visibility); - } - }); + removeLayers(maplibreRef.current!, layerConfig.id, true); }, }; diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index 0dfce730..d803572c 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -7,14 +7,17 @@ import { Map as Maplibre } from 'maplibre-gl'; import { parse } from 'wellknown'; import { DocumentLayerSpecification } from './mapLayerType'; import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; -import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; +import { getMaplibreBeforeLayerId } from './layersFunctions'; import { addCircleLayer, addLineLayer, addPolygonLayer, + hasLayer, + removeLayers, updateCircleLayer, updateLineLayer, updatePolygonLayer, + updateLayerVisibility, } from './map/layer_operations'; interface MaplibreRef { @@ -175,7 +178,7 @@ const addNewLayer = ( } }; -const updateLayerConfig = ( +const updateLayer = ( layerConfig: DocumentLayerSpecification, maplibreRef: MaplibreRef, data: any @@ -188,21 +191,21 @@ const updateLayerConfig = ( dataSource.setData(getLayerSource(data, layerConfig)); } updateCircleLayer(maplibreInstance, { - fillColor: layerConfig.style?.fillColor, + fillColor: layerConfig.style.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, - outlineColor: layerConfig.style?.borderColor, + outlineColor: layerConfig.style.borderColor, radius: layerConfig.style?.markerSize, sourceId: layerConfig.id, visibility: layerConfig.visibility, - width: layerConfig.style?.borderThickness, + width: layerConfig.style.borderThickness, }); const geoFieldType = getGeoFieldType(layerConfig); if (geoFieldType === 'geo_shape') { updateLineLayer(maplibreInstance, { - width: layerConfig.style?.borderThickness, - color: layerConfig.style?.fillColor, + width: layerConfig.style.borderThickness, + color: layerConfig.style.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, @@ -210,13 +213,13 @@ const updateLayerConfig = ( visibility: layerConfig.visibility, }); updatePolygonLayer(maplibreInstance, { - width: layerConfig.style?.borderThickness, - fillColor: layerConfig.style?.fillColor, + width: layerConfig.style.borderThickness, + fillColor: layerConfig.style.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, sourceId: layerConfig.id, - outlineColor: layerConfig.style?.borderColor, + outlineColor: layerConfig.style.borderColor, visibility: layerConfig.visibility, }); } @@ -230,26 +233,8 @@ export const DocumentLayerFunctions = { data: any, beforeLayerId: string | undefined ) => { - if (layerExistInMbSource(layerConfig.id, maplibreRef)) { - updateLayerConfig(layerConfig, maplibreRef, data); - } else { - addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); - } - }, - remove: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((layer: { id: any }) => { - if (layer.id.includes(layerConfig.id)) { - maplibreRef.current?.removeLayer(layer.id); - } - }); - }, - hide: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => { - const layers = getCurrentStyleLayers(maplibreRef); - layers.forEach((layer) => { - if (layer.id.includes(layerConfig.id)) { - maplibreRef.current?.setLayoutProperty(layer.id, 'visibility', layerConfig.visibility); - } - }); + return hasLayer(maplibreRef.current!, layerConfig.id) + ? updateLayer(layerConfig, maplibreRef, data) + : addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); }, }; diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts index 68634816..71625459 100644 --- a/public/model/layersFunctions.ts +++ b/public/model/layersFunctions.ts @@ -13,6 +13,7 @@ import { OSMLayerFunctions } from './OSMLayerFunctions'; import { DocumentLayerFunctions } from './documentLayerFunctions'; import { MapLayerSpecification } from './mapLayerType'; import { CustomLayerFunctions } from './customLayerFunctions'; +import { getLayers } from './map/layer_operations'; interface MaplibreRef { current: Maplibre | null; @@ -22,37 +23,6 @@ interface MaplibreRef { current: Maplibre | null; } -const getAllMaplibreLayersIncludesId = (maplibreRef: MaplibreRef, layerId?: string) => { - if (!layerId && !maplibreRef) { - return []; - } - return ( - maplibreRef.current - ?.getStyle() - .layers.filter((layer) => layer.id?.includes(String(layerId)) === true) || [] - ); -}; - -export const LayerActions = { - move: (maplibreRef: MaplibreRef, sourceId: string, beforeId?: string) => { - const sourceMaplibreLayers = getAllMaplibreLayersIncludesId(maplibreRef, sourceId); - if (!sourceMaplibreLayers) { - return; - } - const beforeMaplibreLayers = getAllMaplibreLayersIncludesId(maplibreRef, beforeId); - if (!beforeMaplibreLayers || beforeMaplibreLayers.length < 1) { - // move to top - sourceMaplibreLayers.forEach((layer) => maplibreRef.current?.moveLayer(layer.id)); - return; - } - const topOfBeforeLayer = beforeMaplibreLayers[0]; - sourceMaplibreLayers.forEach((layer) => - maplibreRef.current?.moveLayer(layer.id, topOfBeforeLayer.id) - ); - return; - }, -}; - export const layersFunctionMap: { [key: string]: any } = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: OSMLayerFunctions, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DocumentLayerFunctions, @@ -65,16 +35,12 @@ export const layersTypeNameMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, }; -const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { - return maplibreRef.current?.getStyle().layers || []; -}; - export const getMaplibreBeforeLayerId = ( selectedLayer: MapLayerSpecification, maplibreRef: MaplibreRef, beforeLayerId: string | undefined ): string | undefined => { - const currentLoadedMbLayers = getCurrentStyleLayers(maplibreRef); + const currentLoadedMbLayers = getLayers(maplibreRef.current!); if (beforeLayerId) { const beforeMbLayer = currentLoadedMbLayers.find((mbLayer) => mbLayer.id.includes(beforeLayerId) @@ -84,16 +50,6 @@ export const getMaplibreBeforeLayerId = ( return undefined; }; -export const layerExistInMbSource = (layerConfigId: string, maplibreRef: MaplibreRef) => { - const layers = getCurrentStyleLayers(maplibreRef); - for (const layer in layers) { - if (layers[layer].id.includes(layerConfigId)) { - return true; - } - } - return false; -}; - export const layersTypeIconMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, diff --git a/public/model/map/__mocks__/map.ts b/public/model/map/__mocks__/map.ts index cf89df85..1f662def 100644 --- a/public/model/map/__mocks__/map.ts +++ b/public/model/map/__mocks__/map.ts @@ -6,15 +6,50 @@ import { LayerSpecification } from 'maplibre-gl'; import { MockLayer } from './layer'; +export type Source = any; + export class MockMaplibreMap { - public _layers: MockLayer[]; + private _styles: { + layers: MockLayer[]; + sources: Map; + }; + + constructor(layers: MockLayer[]) { + this._styles = { + layers: new Array(...layers), + sources: new Map(), + }; + } + + public addSource(sourceId: string, source: Source) { + this._styles.sources.set(sourceId, source); + } - constructor() { - this._layers = new Array(); + public getSource(sourceId: string): string | undefined { + return this._styles.sources.get(sourceId); + } + + public removeSource(sourceId: string) { + this._styles.sources.delete(sourceId); + } + + public getLayers(): MockLayer[] { + return [...this._styles.layers]; } getLayer(id: string): MockLayer[] { - return this._layers.filter((layer) => (layer.getProperty('id') as string).includes(id)); + return this._styles.layers.filter((layer) => (layer.getProperty('id') as string).includes(id)); + } + + getStyle(): { layers: LayerSpecification[] } { + const layerSpecs: LayerSpecification[] = this._styles.layers.map((layer) => { + return { + id: String(layer.getProperty('id')), + } as LayerSpecification; + }); + return { + layers: layerSpecs, + }; } public setLayerZoomRange(layerId: string, minZoom: number, maxZoom: number) { @@ -43,12 +78,35 @@ export class MockMaplibreMap { layer.setProperty(key, layerSpec[key]); }); if (!beforeId) { - this._layers.push(layer); + this._styles.layers.push(layer); return; } - const beforeLayerIndex = this._layers.findIndex((l) => { + const beforeLayerIndex = this._styles.layers.findIndex((l) => { return l.getProperty('id') === beforeId; }); - this._layers.splice(beforeLayerIndex, 0, layer); + this._styles.layers.splice(beforeLayerIndex, 0, layer); + } + + private move(fromIndex: number, toIndex: number) { + const element = this._styles.layers[fromIndex]; + this._styles.layers.splice(fromIndex, 1); + this._styles.layers.splice(toIndex, 0, element); + } + + moveLayer(layerId: string, beforeId?: string) { + if (layerId === beforeId) { + return; + } + const fromIndex: number = this._styles.layers.indexOf(this.getLayer(layerId)[0]); + const toIndex: number = beforeId + ? this._styles.layers.indexOf(this.getLayer(beforeId)[0]) + : this._styles.layers.length; + this.move(fromIndex, toIndex); + } + + removeLayer(layerId: string) { + this._styles.layers = this.getLayers().filter( + (layer) => !(layer.getProperty('id') as string).includes(layerId) + ); } } diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 779fdb4f..14ffc02e 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -6,16 +6,19 @@ import { addCircleLayer, addLineLayer, addPolygonLayer, + getLayers, + hasLayer, moveLayers, removeLayers, updateCircleLayer, updateLineLayer, - updatePolygonLayer, + updatePolygonLayer, updateLayerVisibility, } from './layer_operations'; import { Map as Maplibre } from 'maplibre-gl'; import { MockMaplibreMap } from './__mocks__/map'; +import { MockLayer } from './__mocks__/layer'; describe('Circle layer', () => { it('add new circle layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedLayerId: string = sourceId + '-circle'; expect(mockMap.getLayer(expectedLayerId).length).toBe(0); @@ -51,7 +54,7 @@ describe('Circle layer', () => { }); it('update circle layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; // add layer first @@ -99,7 +102,7 @@ describe('Circle layer', () => { describe('Line layer', () => { it('add new Line layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedLayerId: string = sourceId + '-line'; expect(mockMap.getLayer(expectedLayerId).length).toBe(0); @@ -129,7 +132,7 @@ describe('Line layer', () => { }); it('update line layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; // add layer first @@ -171,7 +174,7 @@ describe('Line layer', () => { describe('Polygon layer', () => { it('add new polygon layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedFillLayerId = sourceId + '-fill'; const expectedOutlineLayerId = expectedFillLayerId + '-outline'; @@ -218,7 +221,7 @@ describe('Polygon layer', () => { }); it('update polygon layer', () => { - const mockMap = new MockMaplibreMap(); + const mockMap = new MockMaplibreMap([]); const sourceId: string = 'geojson-source'; const expectedFillLayerId = sourceId + '-fill'; @@ -277,3 +280,95 @@ describe('Polygon layer', () => { expect(outlineLayer.getProperty('line-width')).toBe(7); }); }); + +describe('get layer', () => { + it('should get layer successfully', function () { + const mockLayer: MockLayer = new MockLayer('layer-1'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); + const actualLayers = getLayers((mockMap as unknown) as Maplibre, 'layer-1'); + expect(actualLayers.length).toBe(1); + expect(actualLayers[0].id).toBe(mockLayer.getProperty('id')); + }); + + it('should confirm no layer exists', function () { + const mockLayer: MockLayer = new MockLayer('layer-1'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); + expect(hasLayer((mockMap as unknown) as Maplibre, 'layer-2')).toBe(false); + }); + + it('should confirm layer exists', function () { + const mockLayer: MockLayer = new MockLayer('layer-1'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); + expect(hasLayer((mockMap as unknown) as Maplibre, 'layer-1')).toBe(true); + }); +}); + +describe('move layer', () => { + it('should move to top', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + const mockLayer3: MockLayer = new MockLayer('layer-2'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + moveLayers((mockMap as unknown) as Maplibre, 'layer-1'); + const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); + expect(reorderedLayer).toEqual(['layer-2', 'layer-1', 'layer-11']); + }); + it('should move before middle layer', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-2'); + const mockLayer3: MockLayer = new MockLayer('layer-3'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + moveLayers((mockMap as unknown) as Maplibre, 'layer-1', 'layer-2'); + const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); + expect(reorderedLayer).toEqual(['layer-2', 'layer-1', 'layer-3']); + }); + it('should not move if no layer is matched', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-2'); + const mockLayer3: MockLayer = new MockLayer('layer-3'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + moveLayers((mockMap as unknown) as Maplibre, 'layer-4', 'layer-2'); + const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); + expect(reorderedLayer).toEqual(['layer-1', 'layer-2', 'layer-3']); + }); +}); + +describe('delete layer', function () { + it('should delete layer without source', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + const mockLayer3: MockLayer = new MockLayer('layer-2'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + mockMap.addSource('layer-1', 'geojson'); + removeLayers((mockMap as unknown) as Maplibre, 'layer-1'); + expect(mockMap.getLayers().length).toBe(1); + expect(mockMap.getSource('layer-1')).toBeDefined(); + expect(mockMap.getLayers()[0].getProperty('id')).toBe('layer-2'); + }); + it('should delete layer with source', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + const mockLayer3: MockLayer = new MockLayer('layer-2'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); + mockMap.addSource('layer-2', 'geojson'); + removeLayers((mockMap as unknown) as Maplibre, 'layer-2', true); + expect(mockMap.getLayers().length).toBe(2); + expect(mockMap.getSource('layer-2')).toBeUndefined(); + expect( + mockMap.getLayers().filter((layer) => String(layer?.getProperty('id')) === 'layer-2') + ).toEqual([]); + }); +}); + +describe('update visibility', function () { + it('should update visibility for given layer', function () { + const mockLayer1: MockLayer = new MockLayer('layer-1'); + const mockLayer2: MockLayer = new MockLayer('layer-11'); + mockLayer1.setProperty('visibility', 'none'); + const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2]); + updateLayerVisibility((mockMap as unknown) as Maplibre, 'layer-1', 'visible'); + expect(mockMap.getLayers().map((layer) => String(layer.getProperty('visibility')))).toEqual( + Array(2).fill('visible') + ); + }); +}); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 948f06c1..7c3cfd99 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -2,7 +2,51 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { Map as Maplibre } from 'maplibre-gl'; +import { LayerSpecification, Map as Maplibre } from 'maplibre-gl'; + +export const getLayers = (map: Maplibre, dashboardMapsLayerId?: string): LayerSpecification[] => { + const layers: LayerSpecification[] = map.getStyle().layers; + return dashboardMapsLayerId + ? layers.filter((layer) => layer.id.includes(dashboardMapsLayerId)) + : layers; +}; + +export const hasLayer = (map: Maplibre, dashboardMapsLayerId: string): boolean => { + const maplibreMapLayers = getLayers(map); + for (const layer of maplibreMapLayers) { + if (layer.id.includes(dashboardMapsLayerId)) { + return true; + } + } + return false; +}; + +export const moveLayers = (map: Maplibre, sourceId: string, beforeId?: string) => { + const sourceLayers = getLayers(map, sourceId); + if (!sourceLayers.length) { + return; + } + const beforeLayers = beforeId ? getLayers(map, beforeId) : []; + const topOfBeforeLayer = beforeLayers.length ? beforeLayers[0].id : undefined; + sourceLayers.forEach((layer) => map?.moveLayer(layer.id, topOfBeforeLayer)); + return; +}; + +export const removeLayers = (map: Maplibre, layerId: string, removeSource?: boolean) => { + getLayers(map, layerId).forEach((layer) => { + map.removeLayer(layer.id); + }); + // client might remove source if it is not required anymore. + if (removeSource && map.getSource(layerId)) { + map.removeSource(layerId); + } +}; + +export const updateLayerVisibility = (map: Maplibre, layerId: string, visibility: string) => { + getLayers(map, layerId).forEach((layer) => { + map.setLayoutProperty(layer.id, 'visibility', visibility); + }); +}; export interface LineLayerSpecification { sourceId: string; From e40617e17d943765a1f580c3fdbecaf66c9b7179 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Fri, 3 Feb 2023 22:27:39 -0800 Subject: [PATCH 04/77] Refactor layer properties as own interface (#225) * Remove unused methods * Create layer interface to abstract common properties. Signed-off-by: Vijayan Balasubramanian --- public/model/customLayerFunctions.ts | 4 ---- public/model/map/layer_operations.ts | 24 ++++++++++-------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index 2c675434..5da7fe92 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -7,10 +7,6 @@ interface MaplibreRef { current: Maplibre | null; } -const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { - return maplibreRef.current?.getStyle().layers || []; -}; - const updateLayerConfig = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { const maplibreInstance = maplibreRef.current; if (maplibreInstance) { diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 7c3cfd99..06241b2f 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -48,16 +48,20 @@ export const updateLayerVisibility = (map: Maplibre, layerId: string, visibility }); }; -export interface LineLayerSpecification { +// Common properties that every DashboardMap layer contains +interface Layer { sourceId: string; - visibility: string; - color: string; opacity: number; - width: number; minZoom: number; maxZoom: number; } +export interface LineLayerSpecification extends Layer { + visibility: string; + color: string; + width: number; +} + export const addLineLayer = ( map: Maplibre, specification: LineLayerSpecification, @@ -90,16 +94,12 @@ export const updateLineLayer = ( return lineLayerId; }; -export interface CircleLayerSpecification { - sourceId: string; +export interface CircleLayerSpecification extends Layer { visibility: string; fillColor: string; outlineColor: string; radius: number; - opacity: number; width: number; - minZoom: number; - maxZoom: number; } export const addCircleLayer = ( @@ -137,15 +137,11 @@ export const updateCircleLayer = ( return circleLayerId; }; -export interface PolygonLayerSpecification { - sourceId: string; +export interface PolygonLayerSpecification extends Layer { visibility: string; fillColor: string; outlineColor: string; - opacity: number; width: number; - minZoom: number; - maxZoom: number; } export const addPolygonLayer = ( From 88bfeb9724f0a6f2e98be6b5ca016cb0f30c568a Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Mon, 6 Feb 2023 14:42:24 -0800 Subject: [PATCH 05/77] Fix popup display while zoomed out (#226) * Update tool tip condition * Clean up code Signed-off-by: Vijayan Balasubramanian --- .../layer_control_panel.tsx | 8 ++---- .../map_container/map_container.tsx | 15 ++++++++--- public/components/tooltip/create_tooltip.tsx | 27 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 310ecd91..e77a8b26 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -35,11 +35,7 @@ import { LAYER_PANEL_SHOW_LAYER_ICON, LAYER_VISIBILITY, } from '../../../common'; -import { - LayerActions, - layersFunctionMap, - referenceLayerTypeLookup, -} from '../../model/layersFunctions'; +import { referenceLayerTypeLookup } from '../../model/layersFunctions'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { @@ -48,7 +44,7 @@ import { } from '../../model/layerRenderController'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; -import {moveLayers, removeLayers, updateLayerVisibility} from "../../model/map/layer_operations"; +import { moveLayers, removeLayers, updateLayerVisibility } from '../../model/map/layer_operations'; interface MaplibreRef { current: Maplibre | null; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 3f215365..3d833e5d 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -13,7 +13,7 @@ import { MAP_INITIAL_STATE, DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { IndexPattern } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; -import { createPopup, getPopupLngLat, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; +import { createPopup, getPopupLocation, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; import { handleDataLayerRender } from '../../model/layerRenderController'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; @@ -86,12 +86,13 @@ export const MapContainer = ({ if (features && maplibreRef.current) { clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); clickPopup - ?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat) + ?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)) .addTo(maplibreRef.current); } } function onMouseMoveMap(e: MapEventType['mousemove']) { + // This is required to update coordinates on map only on mouse move setCoordinates(e.lngLat.wrap()); // remove previous popup @@ -107,7 +108,7 @@ export const MapContainer = ({ showLayerSelection: false, }); hoverPopup - ?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat) + ?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)) .addTo(maplibreRef.current); } } @@ -166,7 +167,13 @@ export const MapContainer = ({ return (
- + {coordinates && `lat: ${coordinates.lat.toFixed(4)}, lon: ${coordinates.lng.toFixed(4)}, `} diff --git a/public/components/tooltip/create_tooltip.tsx b/public/components/tooltip/create_tooltip.tsx index 7d86fc66..d7c019d3 100644 --- a/public/components/tooltip/create_tooltip.tsx +++ b/public/components/tooltip/create_tooltip.tsx @@ -1,17 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Popup, MapGeoJSONFeature } from 'maplibre-gl'; +import { Popup, MapGeoJSONFeature, LngLat } from 'maplibre-gl'; import { MapLayerSpecification, DocumentLayerSpecification } from '../../model/mapLayerType'; import { FeatureGroupItem, TooltipContainer } from './tooltipContainer'; +import {MAX_LONGITUDE} from "../../../common"; -type Options = { +interface Options { features: MapGeoJSONFeature[]; layers: DocumentLayerSpecification[]; showCloseButton?: boolean; showPagination?: boolean; showLayerSelection?: boolean; -}; +} export function isTooltipEnabledLayer( layer: MapLayerSpecification @@ -19,7 +20,8 @@ export function isTooltipEnabledLayer( return ( layer.type !== 'opensearch_vector_tile_map' && layer.type !== 'custom_map' && - layer.source.showTooltips === true + layer.source.showTooltips === true && + !!layer.source.tooltipFields?.length ); } @@ -39,15 +41,22 @@ export function groupFeaturesByLayers( return featureGroups; } -export function getPopupLngLat(geometry: GeoJSON.Geometry) { +export function getPopupLocation(geometry: GeoJSON.Geometry, mousePoint: LngLat) { // geometry.coordinates is different for different geometry.type, here we use the geometry.coordinates // of a Point as the position of the popup. For other types, such as Polygon, MultiPolygon, etc, // use mouse position should be better - if (geometry.type === 'Point') { - return [geometry.coordinates[0], geometry.coordinates[1]] as [number, number]; - } else { - return null; + if (geometry.type !== 'Point') { + return mousePoint; + } + const coordinates = geometry.coordinates; + // Copied from https://maplibre.org/maplibre-gl-js-docs/example/popup-on-click/ + // Ensure that if the map is zoomed out such that multiple + // copies of the feature are visible, the popup appears + // over the copy being pointed to. + while (Math.abs(mousePoint.lng - coordinates[0]) > MAX_LONGITUDE) { + coordinates[0] += mousePoint.lng > coordinates[0] ? 360 : -360; } + return [coordinates[0], coordinates[1]] as [number, number]; } export function createPopup({ From bd47be36d9c6dea1d2c9ed5646d96ecf8a03e105 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Tue, 7 Feb 2023 08:51:37 -0800 Subject: [PATCH 06/77] Move bound estimation to model (#227) move map bounds to model and added unit test. Signed-off-by: Vijayan Balasubramanian --- public/model/geo/filter.test.ts | 42 ++++++++++++++ public/model/geo/filter.ts | 38 +++++++++++++ public/model/layerRenderController.ts | 68 +++++------------------ public/model/map/__mocks__/map.ts | 17 +++++- public/model/map/bondary.test.ts | 42 ++++++++++++++ public/model/map/boundary.ts | 40 +++++++++++++ public/model/map/layer_operations.test.ts | 7 ++- test/setup.jest.js | 1 + 8 files changed, 196 insertions(+), 59 deletions(-) create mode 100644 public/model/geo/filter.test.ts create mode 100644 public/model/geo/filter.ts create mode 100644 public/model/map/bondary.test.ts create mode 100644 public/model/map/boundary.ts diff --git a/public/model/geo/filter.test.ts b/public/model/geo/filter.test.ts new file mode 100644 index 00000000..d35ca6c8 --- /dev/null +++ b/public/model/geo/filter.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LngLat } from 'maplibre-gl'; +import { GeoBounds } from '../map/boundary'; +import { FilterMeta, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; +import { buildBBoxFilter } from './filter'; + +describe('test bounding box filter', function () { + it('should return valid bounding box', function () { + const mockBounds: GeoBounds = { + bottomRight: new LngLat(-2.340000000000032, 27.67), + topLeft: new LngLat(-135.18, 71.01), + }; + const mockFilterMeta: FilterMeta = { + alias: null, + disabled: true, + negate: false, + }; + const actualGeoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter( + 'field-name', + mockBounds, + mockFilterMeta + ); + const expectedBounds = { + bottom_right: { + lat: mockBounds.bottomRight.lat, + lon: mockBounds.bottomRight.lng, + }, + top_left: { + lat: mockBounds.topLeft.lat, + lon: mockBounds.topLeft.lng, + }, + }; + expect(actualGeoBoundingBoxFilter.geo_bounding_box).toEqual({ + ['field-name']: expectedBounds, + }); + expect(actualGeoBoundingBoxFilter.meta.params).toEqual(expectedBounds); + }); +}); diff --git a/public/model/geo/filter.ts b/public/model/geo/filter.ts new file mode 100644 index 00000000..b41f219c --- /dev/null +++ b/public/model/geo/filter.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LatLon } from '@opensearch-project/opensearch/api/types'; +import { FilterMeta, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; +import { GeoBounds } from '../map/boundary'; + +export const buildBBoxFilter = ( + fieldName: string, + mapBounds: GeoBounds, + filterMeta: FilterMeta +): GeoBoundingBoxFilter => { + const bottomRight: LatLon = { + lon: mapBounds.bottomRight.lng, + lat: mapBounds.bottomRight.lat, + }; + + const topLeft: LatLon = { + lon: mapBounds.topLeft.lng, + lat: mapBounds.topLeft.lat, + }; + + const boundingBox = { + bottom_right: bottomRight, + top_left: topLeft, + }; + return { + meta: { + ...filterMeta, + params: boundingBox, + }, + geo_bounding_box: { + [fieldName]: boundingBox, + }, + }; +}; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 98802cf4..7c11bb2d 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { LngLatBounds, Map as Maplibre } from 'maplibre-gl'; +import { Map as Maplibre } from 'maplibre-gl'; import { DocumentLayerSpecification, MapLayerSpecification } from './mapLayerType'; -import { DASHBOARDS_MAPS_LAYER_TYPE, MAX_LONGITUDE, MIN_LONGITUDE } from '../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; import { buildOpenSearchQuery, Filter, + FilterMeta, GeoBoundingBoxFilter, getTime, IOpenSearchDashboardsSearchResponse, @@ -17,34 +18,13 @@ import { import { layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; +import {GeoBounds, getBounds} from './map/boundary'; +import { buildBBoxFilter } from './geo/filter'; interface MaplibreRef { current: Maplibre | null; } -// calculate lng limits based on map bounds -// maps can render more than 1 copies of map at lower zoom level and displays -// one side from 1 copy and other side from other copy at higher zoom level if -// screen crosses internation dateline -function calculateBoundingBoxLngLimit(bounds: LngLatBounds) { - const boundsMinLng = bounds.getNorthWest().lng; - const boundsMaxLng = bounds.getSouthEast().lng; - // if bounds expands more than 360 then, consider complete globe is visible - if (boundsMaxLng - boundsMinLng >= MAX_LONGITUDE - MIN_LONGITUDE) { - return { - right: MAX_LONGITUDE, - left: MIN_LONGITUDE, - }; - } - // wrap bounds if only portion of globe is visible - // wrap() returns a new LngLat object whose longitude is - // wrapped to the range (-180, 180). - return { - right: bounds.getSouthEast().wrap().lng, - left: bounds.getNorthWest().wrap().lng, - }; -} - export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, @@ -117,36 +97,14 @@ export const handleDataLayerRender = ( const geoFieldType = mapLayer.source.geoFieldType; // geo bounding box query supports geo_point fields - if ( - geoFieldType === 'geo_point' && - mapLayer.source.useGeoBoundingBoxFilter && - maplibreRef.current - ) { - const mapBounds = maplibreRef.current.getBounds(); - const lngLimit = calculateBoundingBoxLngLimit(mapBounds); - const filterBoundingBox = { - bottom_right: { - lon: lngLimit.right, - lat: mapBounds.getSouthEast().lat, - }, - top_left: { - lon: lngLimit.left, - lat: mapBounds.getNorthWest().lat, - }, - }; - const geoBoundingBoxFilter: GeoBoundingBoxFilter = { - meta: { - disabled: false, - negate: false, - alias: null, - params: filterBoundingBox, - }, - geo_bounding_box: { - [geoField]: filterBoundingBox, - }, - }; - filters.push(geoBoundingBoxFilter); - } + const mapBounds: GeoBounds = getBounds(maplibreRef.current!); + const meta: FilterMeta = { + alias: null, + disabled: !mapLayer.source.useGeoBoundingBoxFilter || geoFieldType !== 'geo_point', + negate: false, + }; + const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); + filters.push(geoBoundingBoxFilter); return prepareDataLayerSource(mapLayer, mapState, services, filters).then((result) => { const { layer, dataSource } = result; diff --git a/public/model/map/__mocks__/map.ts b/public/model/map/__mocks__/map.ts index 1f662def..df965093 100644 --- a/public/model/map/__mocks__/map.ts +++ b/public/model/map/__mocks__/map.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { LayerSpecification } from 'maplibre-gl'; +import { LayerSpecification, LngLatBounds } from 'maplibre-gl'; import { MockLayer } from './layer'; export type Source = any; @@ -14,11 +14,16 @@ export class MockMaplibreMap { sources: Map; }; - constructor(layers: MockLayer[]) { + private _bounds: LngLatBounds; + + constructor(layers: MockLayer[], bounds?: LngLatBounds) { this._styles = { layers: new Array(...layers), sources: new Map(), }; + if (bounds) { + this._bounds = bounds; + } } public addSource(sourceId: string, source: Source) { @@ -109,4 +114,12 @@ export class MockMaplibreMap { (layer) => !(layer.getProperty('id') as string).includes(layerId) ); } + + setBounds = (bounds: LngLatBounds) => { + this._bounds = bounds; + }; + + getBounds = (): LngLatBounds => { + return this._bounds; + }; } diff --git a/public/model/map/bondary.test.ts b/public/model/map/bondary.test.ts new file mode 100644 index 00000000..39513a5a --- /dev/null +++ b/public/model/map/bondary.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LngLat, LngLatBounds, Map as Maplibre } from 'maplibre-gl'; +import { MockMaplibreMap } from './__mocks__/map'; +import { GeoBounds, getBounds } from './boundary'; + +describe('verify get bounds', function () { + it('should cover complete map if more than on copy is visible', function () { + const ne: LngLat = new LngLat(333.811, 82.8); + const sw: LngLat = new LngLat(-248.8, -79.75); + const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); + const expectedBounds: GeoBounds = { + bottomRight: new LngLat(180, -79.75), + topLeft: new LngLat(-180, 82.8), + }; + expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); + }); + + it('should wrap if map crosses international date line', function () { + const ne: LngLat = new LngLat(11.56, 80.85); + const sw: LngLat = new LngLat(-220.77, 21.52); + const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); + const expectedBounds: GeoBounds = { + bottomRight: new LngLat(11.559999999999945, 21.52), + topLeft: new LngLat(139.23000000000002, 80.85), + }; + expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); + }); + it('should give same value as map bounds for other cases', function () { + const sw: LngLat = new LngLat(-135.18, 27.67); + const ne: LngLat = new LngLat(-2.34, 71.01); + const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); + const expectedBounds: GeoBounds = { + bottomRight: new LngLat(-2.340000000000032, 27.67), + topLeft: new LngLat(-135.18, 71.01), + }; + expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); + }); +}); diff --git a/public/model/map/boundary.ts b/public/model/map/boundary.ts new file mode 100644 index 00000000..3b272429 --- /dev/null +++ b/public/model/map/boundary.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LngLat, LngLatBounds, Map as Maplibre } from 'maplibre-gl'; +import { MAX_LONGITUDE, MIN_LONGITUDE } from '../../../common'; + +export interface GeoBounds { + bottomRight: LngLat; + topLeft: LngLat; +} + +// calculate lng limits based on map bounds +// maps can render more than 1 copies of map at lower zoom level and displays +// one side from 1 copy and other side from other copy at higher zoom level if +// screen crosses internation dateline +export function getBounds(map: Maplibre): GeoBounds { + const mapBounds: LngLatBounds = map.getBounds(); + const boundsMinLng: number = mapBounds.getNorthWest().lng; + const boundsMaxLng: number = mapBounds.getSouthEast().lng; + + let right: number; + let left: number; + // if bounds expands more than 360 then, consider complete globe is visible + if (boundsMaxLng - boundsMinLng >= MAX_LONGITUDE - MIN_LONGITUDE) { + right = MAX_LONGITUDE; + left = MIN_LONGITUDE; + } else { + // wrap bounds if only portion of globe is visible + // wrap() returns a new LngLat object whose longitude is + // wrapped to the range (-180, 180). + right = mapBounds.getSouthEast().wrap().lng; + left = mapBounds.getNorthWest().wrap().lng; + } + return { + bottomRight: new LngLat(right, mapBounds.getSouthEast().lat), + topLeft: new LngLat(left, mapBounds.getNorthWest().lat), + }; +} diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 14ffc02e..53456619 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -7,10 +7,13 @@ import { addLineLayer, addPolygonLayer, getLayers, - hasLayer, moveLayers, removeLayers, + hasLayer, + moveLayers, + removeLayers, updateCircleLayer, updateLineLayer, - updatePolygonLayer, updateLayerVisibility, + updatePolygonLayer, + updateLayerVisibility, } from './layer_operations'; import { Map as Maplibre } from 'maplibre-gl'; import { MockMaplibreMap } from './__mocks__/map'; diff --git a/test/setup.jest.js b/test/setup.jest.js index e5fbbdf9..8bcca072 100644 --- a/test/setup.jest.js +++ b/test/setup.jest.js @@ -34,3 +34,4 @@ beforeEach(() => { afterEach(() => { console.error.mockRestore(); }); +window.URL.createObjectURL = function () {}; From 347677ef0f0cd8abfa39d9b19cbf85721fd33364 Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Wed, 8 Feb 2023 13:05:13 -0600 Subject: [PATCH 07/77] Delete Sample data after cypress test (#235) Signed-off-by: Naveen Tatikonda --- cypress/integration/documentsLayer.spec.js | 5 +++++ cypress/integration/opensearchMapLayer.spec.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/cypress/integration/documentsLayer.spec.js b/cypress/integration/documentsLayer.spec.js index 9e166306..79059d16 100644 --- a/cypress/integration/documentsLayer.spec.js +++ b/cypress/integration/documentsLayer.spec.js @@ -54,4 +54,9 @@ describe('Documents layer', () => { cy.contains(uniqueName).click(); cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Documents layer 1'); }); + + after(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); + cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); + }); }); diff --git a/cypress/integration/opensearchMapLayer.spec.js b/cypress/integration/opensearchMapLayer.spec.js index 75642344..6301d516 100644 --- a/cypress/integration/opensearchMapLayer.spec.js +++ b/cypress/integration/opensearchMapLayer.spec.js @@ -36,4 +36,9 @@ describe('Default OpenSearch base map layer', () => { } cy.get('[data-test-subj="mapStatusBar"]').should('contain', 'zoom: 22'); }); + + after(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); + cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); + }); }); From ffb12e6e78f274676da19b3bd91a78cbe884a208 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 8 Feb 2023 14:03:29 -0800 Subject: [PATCH 08/77] Fix cypress ci on Ubuntu OS (#236) Signed-off-by: Junqiu Lei --- .github/workflows/cypress-workflow.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 604f7f7c..17d02994 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -115,7 +115,6 @@ jobs: yarn start --no-base-path --no-watch & shell: bash - # Window is slow so wait longer - name: Sleep until OSD server starts - windows if: ${{ matrix.os == 'windows-latest' }} run: Start-Sleep -s 600 @@ -123,7 +122,7 @@ jobs: - name: Sleep until OSD server starts - non-windows if: ${{ matrix.os != 'windows-latest' }} - run: sleep 400 + run: sleep 600 shell: bash - name: Install Cypress From 61d298aae7db24de571567e44cd05a9385c73118 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 13 Feb 2023 13:48:40 -0800 Subject: [PATCH 09/77] Update development and maintainer document (#239) * Update development document Signed-off-by: Junqiu Lei * Update maintainer document Signed-off-by: Junqiu Lei * update dashboards-maps name Signed-off-by: Junqiu Lei --------- Signed-off-by: Junqiu Lei --- DEVELOPER_GUIDE.md | 68 +++++++++++++++++++++++++++------------------- MAINTAINERS.md | 10 +++++-- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 4e1ef1ac..f17686db 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -2,17 +2,18 @@ So you want to contribute code to this project? Excellent! We're glad you're here. Here's what you need to do. -- [Forking and Cloning](#forking-and-cloning) +- [Forking](#forking) - [Install Prerequisites](#install-prerequisites) - [Setup](#setup) - [Run](#run) +- [Build](#build) - [Test](#test) - [Submitting Changes](#submitting-changes) - [Backports](#backports) -### Forking and Cloning +### Forking -Fork this repository on GitHub, and clone locally with `git clone`. +Fork the repository on GitHub. ### Install Prerequisites @@ -21,41 +22,54 @@ You will need to install [node.js](https://nodejs.org/en/), [nvm](https://github ### Setup -1. Download the OpenSearch Dashboards source code for the [version specified in package.json](./src/plugins/custom_import_map/package.json#L3) you want to set up. -2. Change your node version to the version specified in `.node-version` inside the OpenSearch Dashboards root directory. -3. Create a `plugins` directory inside the OpenSearch Dashboards source code directory, if `plugins` directory doesn't exist. -4. cd into `plugins` directory in the OpenSearch Dashboards source code directory. -5. Check out this package from version control into the `plugins` directory. -```bash -git clone git@github.com:opensearch-project/dashboards-maps.git plugins --no-checkout -cd plugins -echo 'src/plugins/custom_import_map/*' >> .git/info/sparse-checkout -git config core.sparseCheckout true -git checkout main -``` -6. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/src/plugins/custom_import_map`. +1. Download OpenSearch [Geospatial](https://github.com/opensearch-project/geospatial) plugin, which requires same version with OpenSearch-Dashboard and dashboards-maps plugin. +2. Run `./gradlew run` under Geospatial plugin root path to start OpenSearch cluster. +3. Download the OpenSearch Dashboards source code for the version specified in package.json you want to set up. +4. Change your node version by `nvm use `to the version specified in `.node-version` inside the OpenSearch Dashboards root directory. +5. Create a `plugins` directory inside the OpenSearch Dashboards source code directory, if `plugins` directory doesn't exist. +6. cd into `plugins` directory in the OpenSearch Dashboards source code directory. +7. Check out this package from version control into the `plugins` directory. +8. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/dashboards-maps` folder. Ultimately, your directory structure should look like this: ```md -. ├── OpenSearch-Dashboards -│ └── plugins -│ └── src/plugins/custom_import_map +│ ├── plugins +│ │ └── dashboards-maps ``` ### Run -From OpenSearch-Dashbaords repo (root folder), run the following command - -- `yarn start` +From OpenSearch-Dashboards repo (root folder), the following commands start OpenSearch Dashboards and includes this plugin. + +``` +yarn osd bootstrap +yarn start --no-base-path +``` + +OpenSearch Dashboards will be available on `localhost:5601`. + +### Build - Starts OpenSearch Dashboards and includes this plugin. OpenSearch Dashboards will be available on `localhost:5601`. +To build the plugin's distributable zip simply run `yarn build`. + +Example output: ./build/customImportMapDashboards-1.0.0.0.zip ### Test -From custom_import_map folder running the following command runs the plugin unit tests - +From dashboards-maps folder running the following command runs the plugin unit tests: + +#### Unit test +``` +yarn test:jest +``` -`yarn test:jest` +#### Integration Tests +Integration tests for this plugin are written using the Cypress test framework. +``` +yarn run cypress run +``` ### Submitting Changes @@ -63,8 +77,6 @@ See [CONTRIBUTING](CONTRIBUTING.md). ### Backports -The Github workflow in [`backport.yml`](.github/workflows/backport.yml) creates backport PRs automatically when the original PR -with an appropriate label `backport ` is merged to main with the backport workflow run successfully on the -PR. For example, if a PR on main needs to be backported to `1.x` branch, add a label `backport 1.x` to the PR and make sure the +The GitHub backport workflow creates backport PRs automatically for PRs with label `backport `. Label should be attached to the original PR, backport workflow starts when original PR merged to main branch. For example, if a PR on main needs to be backported to `1.x` branch, add a label `backport 1.x` to the PR and make sure the backport workflow runs on the PR along with other checks. Once this PR is merged to main, the workflow will create a backport PR -to the `1.x` branch. \ No newline at end of file +to the `1.x` branch. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 95c18283..927e4248 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -5,7 +5,13 @@ This document contains a list of maintainers in this repo. See [opensearch-proje ## Current Maintainers | Maintainer | GitHub ID | Affiliation | -| ----------------------- | ------------------------------------------- | ----------- | -| Shivam Dhar | [Shivamdhar](https://github.com/Shivamdhar) | Amazon | +|-------------------------|---------------------------------------------| ----------- | | Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | | Vamshi Vijay Nakkirtha | [vamshin](https://github.com/vamshin) | Amazon | +| Junqiu Lei | [junqiu-lei](https://github.com/junqiu-lei) | Amazon | + +## Emeritus + +| Maintainer | GitHub ID | Affiliation | +|-------------------------|---------------------------------------------| ----------- | +| Shivam Dhar | [Shivamdhar](https://github.com/Shivamdhar) | Amazon | From c688c23f37ee1fc33f1f5e5af685236bb1475f3b Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Mon, 13 Feb 2023 14:33:45 -0800 Subject: [PATCH 10/77] Refactor delete layer modal (#242) Signed-off-by: Vijayan Balasubramanian --- package.json | 1 + .../delete_layer_modal.test.tsx | 23 ++++++++++++++ .../delete_layer_modal.tsx | 30 +++++++++++++++++++ .../layer_control_panel.tsx | 28 +++++------------ 4 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 public/components/layer_control_panel/delete_layer_modal.test.tsx create mode 100644 public/components/layer_control_panel/delete_layer_modal.tsx diff --git a/package.json b/package.json index 5550e19f..74f13d2e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "wellknown": "^0.5.0" }, "devDependencies": { + "@types/react-test-renderer": "^18.0.0", "cypress": "9.5.4", "cypress-multi-reporters": "^1.5.0", "prettier": "^2.1.1" diff --git a/public/components/layer_control_panel/delete_layer_modal.test.tsx b/public/components/layer_control_panel/delete_layer_modal.test.tsx new file mode 100644 index 00000000..85bc626c --- /dev/null +++ b/public/components/layer_control_panel/delete_layer_modal.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DeleteLayerModal } from './delete_layer_modal'; +import React from 'react'; +import { EuiConfirmModal } from '@elastic/eui'; +import TestRenderer from 'react-test-renderer'; + +describe('test delete layer modal', function () { + it('should show modal', function () { + const deleteLayerModal = TestRenderer.create( + {}} onConfirm={() => {}} /> + ); + const testInstance = deleteLayerModal.root; + expect(testInstance.findByType(EuiConfirmModal).props.title).toBe('Delete layer'); + expect(testInstance.findByType(EuiConfirmModal).props.confirmButtonText).toBe('Delete'); + expect(testInstance.findByType(EuiConfirmModal).props.cancelButtonText).toBe('Cancel'); + expect(testInstance.findByType(EuiConfirmModal).props.buttonColor).toBe('danger'); + expect(testInstance.findByType(EuiConfirmModal).props.defaultFocusedButton).toBe('confirm'); + }); +}); diff --git a/public/components/layer_control_panel/delete_layer_modal.tsx b/public/components/layer_control_panel/delete_layer_modal.tsx new file mode 100644 index 00000000..75d5cf3d --- /dev/null +++ b/public/components/layer_control_panel/delete_layer_modal.tsx @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiConfirmModal } from '@elastic/eui'; +import React from 'react'; + +interface DeleteLayerModalProps { + onCancel: () => void; + onConfirm: () => void; + layerName: string; +} +export const DeleteLayerModal = ({ onCancel, onConfirm, layerName }: DeleteLayerModalProps) => { + return ( + +

+ Do you want to delete layer {layerName}? +

+
+ ); +}; diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index e77a8b26..802379c5 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -45,6 +45,7 @@ import { import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; import { moveLayers, removeLayers, updateLayerVisibility } from '../../model/map/layer_operations'; +import { DeleteLayerModal } from './delete_layer_modal'; interface MaplibreRef { current: Maplibre | null; @@ -308,25 +309,6 @@ export const LayerControlPanel = memo( setSelectedDeleteLayer(undefined); }; - let deleteLayerModal; - if (isDeleteLayerModalVisible) { - deleteLayerModal = ( - -

- Do you want to delete layer {selectedDeleteLayer?.name}? -

-
- ); - } - const getLayerTooltipContent = (layer: MapLayerSpecification) => { if (zoom < layer.zoomRange[0] || zoom > layer.zoomRange[1]) { return `Layer is not visible outside of zoom range ${layer.zoomRange[0]} - ${layer.zoomRange[1]}`; @@ -504,7 +486,13 @@ export const LayerControlPanel = memo( mapConfig={mapConfig} layerCount={layers.length} /> - {deleteLayerModal} + {isDeleteLayerModalVisible && ( + + )}
From 816ce7945567ec1aeec7b74a2f30599e95969410 Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Tue, 14 Feb 2023 12:14:44 -0600 Subject: [PATCH 11/77] Add maps saved object for sample datasets (#240) * Add Default Saved object for Maps Signed-off-by: Naveen Tatikonda * Consume config and output result Signed-off-by: Kawika Avilla * Refactor the code Signed-off-by: Naveen Tatikonda * Address Review Comments Signed-off-by: Naveen Tatikonda --------- Signed-off-by: Naveen Tatikonda Signed-off-by: Kawika Avilla Co-authored-by: Kawika Avilla --- cypress/integration/add_saved_object.spec.js | 33 +++++++ opensearch_dashboards.json | 2 +- server/plugin.ts | 25 +++++- .../sample_data/flights_saved_objects.ts | 90 +++++++++++++++++++ server/types.ts | 5 ++ 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 cypress/integration/add_saved_object.spec.js create mode 100644 server/services/sample_data/flights_saved_objects.ts diff --git a/cypress/integration/add_saved_object.spec.js b/cypress/integration/add_saved_object.spec.js new file mode 100644 index 00000000..12dc6528 --- /dev/null +++ b/cypress/integration/add_saved_object.spec.js @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../utils/constants'; + +describe('Add flights dataset saved object', () => { + before(() => { + cy.visit(`${BASE_PATH}/app/maps-dashboards`, { + retryOnStatusCodeFailure: true, + timeout: 60000, + }); + cy.get('div[data-test-subj="indexPatternEmptyState"]', { timeout: 60000 }) + .contains(/Add sample data/) + .click(); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/Add data/) + .click(); + cy.wait(60000); + }); + + after(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); + cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); + }); + + it('check if maps saved object of flights dataset can be found and open', () => { + cy.visit(`${BASE_PATH}/app/maps-dashboards`); + cy.contains('[Flights] Maps Cancelled Flights Destination Location').click(); + cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Cancelled flights'); + }); +}); diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 1723bfde..306f3d37 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -5,5 +5,5 @@ "server": true, "ui": true, "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "navigation", "savedObjects", "data"], - "optionalPlugins": [] + "optionalPlugins": ["home"] } diff --git a/server/plugin.ts b/server/plugin.ts index 0be30228..cae4e8ce 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -12,27 +12,44 @@ import { Logger, } from '../../../src/core/server'; -import { CustomImportMapPluginSetup, CustomImportMapPluginStart } from './types'; +import { HomeServerPluginSetup } from '../../../src/plugins/home/server'; +import { getFlightsSavedObjects } from './services/sample_data/flights_saved_objects'; + +import { + AppPluginSetupDependencies, + CustomImportMapPluginSetup, + CustomImportMapPluginStart, +} from './types'; import { createGeospatialCluster } from './clusters'; import { GeospatialService, OpensearchService } from './services'; import { geospatial, opensearch } from '../server/routes'; import { mapSavedObjectsType } from './saved_objects'; import { capabilitiesProvider } from './saved_objects/capabilities_provider'; +import { ConfigSchema } from '../common/config'; export class CustomImportMapPlugin implements Plugin { private readonly logger: Logger; private readonly globalConfig$; + private readonly config$; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.globalConfig$ = initializerContext.config.legacy.globalConfig$; + this.config$ = initializerContext.config.create(); + } + + // Adds dashboards-maps saved objects to existing sample datasets using home plugin + private addMapsSavedObjects(home: HomeServerPluginSetup, config: ConfigSchema) { + home.sampleData.addSavedObjectsToSampleDataset('flights', getFlightsSavedObjects(config)); } - public async setup(core: CoreSetup) { + public async setup(core: CoreSetup, plugins: AppPluginSetupDependencies) { this.logger.debug('customImportMap: Setup'); // @ts-ignore const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); + // @ts-ignore + const config = (await this.config$.pipe(first()).toPromise()) as ConfigSchema; const geospatialClient = createGeospatialCluster(core, globalConfig); // Initialize services @@ -40,6 +57,8 @@ export class CustomImportMapPlugin const opensearchService = new OpensearchService(geospatialClient); const router = core.http.createRouter(); + const { home } = plugins; + // Register server side APIs geospatial(geospatialService, router); opensearch(opensearchService, router); @@ -50,6 +69,8 @@ export class CustomImportMapPlugin // Register capabilities core.capabilities.registerProvider(capabilitiesProvider); + if (home) this.addMapsSavedObjects(home, config); + return {}; } diff --git a/server/services/sample_data/flights_saved_objects.ts b/server/services/sample_data/flights_saved_objects.ts new file mode 100644 index 00000000..af8eb499 --- /dev/null +++ b/server/services/sample_data/flights_saved_objects.ts @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { ConfigSchema } from '../../../common/config'; + +const layerList = (config: ConfigSchema) => [ + { + name: 'Default map', + description: '', + type: 'opensearch_vector_tile_map', + id: 'cad56fcc-be02-43ea-a1a6-1d17f437acf7', + zoomRange: [0, 22], + opacity: 100, + visibility: 'visible', + source: { + dataURL: config.opensearchVectorTileDataUrl, + }, + style: { + styleURL: config.opensearchVectorTileStyleUrl, + }, + }, + { + name: 'Cancelled flights', + description: 'Shows cancelled flights', + type: 'documents', + id: 'f3ae28ce-2494-4e50-ae31-4603cfcbfd7d', + zoomRange: [2, 22], + opacity: 70, + visibility: 'visible', + source: { + indexPatternRefName: 'opensearch_dashboards_sample_data_flights', + geoFieldType: 'geo_point', + geoFieldName: 'DestLocation', + documentRequestNumber: 1000, + tooltipFields: ['Carrier', 'Cancelled'], + showTooltips: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + useGeoBoundingBoxFilter: true, + filters: [ + { + meta: { + index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + alias: null, + negate: false, + disabled: false, + }, + query: { + match_phrase: { + Cancelled: true, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + style: { + fillColor: '#CA8EAE', + borderColor: '#CA8EAE', + borderThickness: 1, + markerSize: 5, + }, + }, +]; + +export const getFlightsSavedObjects = (config: ConfigSchema) => { + return [ + { + id: '122713b0-9e70-11ed-9463-35a6f30dbef6', + type: 'map', + updated_at: '2023-01-27T18:26:09.643Z', + version: 'WzIzLDFd', + migrationVersion: {}, + attributes: { + title: i18n.translate('home.sampleData.flightsSpec.mapsCancelledFlights', { + defaultMessage: '[Flights] Maps Cancelled Flights Destination Location', + }), + description: 'Sample map to show cancelled flights location at destination', + layerList: JSON.stringify(layerList(config)), + mapState: + '{"timeRange":{"from":"now-15d","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}', + }, + references: [], + }, + ]; +}; diff --git a/server/types.ts b/server/types.ts index f506de95..0cc8bb06 100644 --- a/server/types.ts +++ b/server/types.ts @@ -2,8 +2,13 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { HomeServerPluginSetup } from '../../../src/plugins/home/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomImportMapPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomImportMapPluginStart {} + +export interface AppPluginSetupDependencies { + home?: HomeServerPluginSetup; +} From 9fc98ca0ed70a7e3a92b1cc250dec006e6d63dff Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Mon, 20 Feb 2023 13:22:16 -0500 Subject: [PATCH 12/77] Created untriaged issue workflow. (#248) Signed-off-by: dblock --- .github/workflows/add-untriaged.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/add-untriaged.yml diff --git a/.github/workflows/add-untriaged.yml b/.github/workflows/add-untriaged.yml new file mode 100644 index 00000000..9dcc7020 --- /dev/null +++ b/.github/workflows/add-untriaged.yml @@ -0,0 +1,19 @@ +name: Apply 'untriaged' label during issue lifecycle + +on: + issues: + types: [opened, reopened, transferred] + +jobs: + apply-label: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['untriaged'] + }) From 73be41889bbf923f4321431e85433fdd89bc5bf4 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Mon, 20 Feb 2023 15:57:41 -0800 Subject: [PATCH 13/77] Add close button to tooltip hover (#263) When large number of points and shapes are rendered on same location, tooltip occasionally is not cleared. Adding close button will solve this problem. Signed-off-by: Vijayan Balasubramanian --- public/components/map_container/map_container.tsx | 3 ++- public/components/tooltip/create_tooltip.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 3d833e5d..a17708b7 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -103,7 +103,8 @@ export const MapContainer = ({ hoverPopup = createPopup({ features, layers: tooltipEnabledLayers, - showCloseButton: false, + // enable close button to avoid occasional dangling tooltip that is not cleared during mouse leave action + showCloseButton: true, showPagination: false, showLayerSelection: false, }); diff --git a/public/components/tooltip/create_tooltip.tsx b/public/components/tooltip/create_tooltip.tsx index d7c019d3..d278e721 100644 --- a/public/components/tooltip/create_tooltip.tsx +++ b/public/components/tooltip/create_tooltip.tsx @@ -4,7 +4,7 @@ import { Popup, MapGeoJSONFeature, LngLat } from 'maplibre-gl'; import { MapLayerSpecification, DocumentLayerSpecification } from '../../model/mapLayerType'; import { FeatureGroupItem, TooltipContainer } from './tooltipContainer'; -import {MAX_LONGITUDE} from "../../../common"; +import { MAX_LONGITUDE } from '../../../common'; interface Options { features: MapGeoJSONFeature[]; From e48407ba6efe359f063cf6c45b975d4efa6d9c9e Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 20 Feb 2023 15:59:25 -0800 Subject: [PATCH 14/77] Add map as embeddable to dashboard (#231) * Add map as embeddable to dashboard Signed-off-by: Junqiu Lei * Create new map from dashboard visualization and save return Signed-off-by: Junqiu Lei * Enable interval refresh in dashboard Signed-off-by: Junqiu Lei * Enable filters in dashboard Signed-off-by: Junqiu Lei * Enable query in dashboard Signed-off-by: Junqiu Lei * Add cypress test Signed-off-by: Junqiu Lei * Fix maps listing in visualization listing page Signed-off-by: Junqiu Lei * Refactor controller Signed-off-by: Junqiu Lei * Resolve feedback Signed-off-by: Junqiu Lei --------- Signed-off-by: Junqiu Lei --- common/constants/shared.ts | 4 +- common/index.ts | 8 +- .../integration/add_map_to_dashboard.spec.js | 36 +++ opensearch_dashboards.json | 2 +- package-lock.json | 249 ++++++++++++++++++ public/components/app.tsx | 16 +- .../layer_control_panel.tsx | 36 ++- .../map_container/map_container.scss | 8 +- .../map_container/map_container.tsx | 112 ++++++-- public/components/map_page/index.ts | 2 +- public/components/map_page/map_page.tsx | 80 ++++-- .../map_top_nav/get_top_nav_config.tsx | 213 ++++++++++----- .../components/map_top_nav/top_nav_menu.tsx | 68 +++-- public/components/maps_list/maps_list.tsx | 6 +- public/embeddable/index.ts | 7 + public/embeddable/map_component.tsx | 57 ++++ public/embeddable/map_embeddable.tsx | 97 +++++++ public/embeddable/map_embeddable_factory.tsx | 99 +++++++ public/model/layerRenderController.ts | 53 ++-- public/plugin.tsx | 76 +++++- public/services.ts | 4 + public/types.ts | 27 +- public/utils/breadcrumbs.ts | 4 +- yarn.lock | 31 +++ 24 files changed, 1090 insertions(+), 205 deletions(-) create mode 100644 cypress/integration/add_map_to_dashboard.spec.js create mode 100644 package-lock.json create mode 100644 public/embeddable/index.ts create mode 100644 public/embeddable/map_component.tsx create mode 100644 public/embeddable/map_embeddable.tsx create mode 100644 public/embeddable/map_embeddable_factory.tsx diff --git a/common/constants/shared.ts b/common/constants/shared.ts index 7d1ed801..1fe05b96 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -12,5 +12,5 @@ export const MAX_FILE_PAYLOAD_SIZE_IN_MB = 25; export const MAX_FILE_PAYLOAD_SIZE = fromMBtoBytes(MAX_FILE_PAYLOAD_SIZE_IN_MB); export const PLUGIN_ID = 'customImportMap'; export const PLUGIN_NAME = 'customImportMap'; -export const PLUGIN_NAVIGATION_BAR_TILE = 'Maps'; -export const PLUGIN_NAVIGATION_BAR_ID = 'maps-dashboards'; +export const MAPS_APP_DISPLAY_NAME = 'Maps'; +export const MAPS_APP_ID = 'maps-dashboards'; diff --git a/common/index.ts b/common/index.ts index 80f243ba..7a1dff36 100644 --- a/common/index.ts +++ b/common/index.ts @@ -9,7 +9,7 @@ import { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, - PLUGIN_NAVIGATION_BAR_ID, + MAPS_APP_ID, PLUGIN_NAME, } from './constants/shared'; @@ -19,7 +19,7 @@ export { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, - PLUGIN_NAVIGATION_BAR_ID, + MAPS_APP_ID, PLUGIN_NAME, }; @@ -44,6 +44,10 @@ export const MAX_LAYER_NAME_LIMIT = 35; export const MAX_LONGITUDE = 180; export const MIN_LONGITUDE = -180; export const NEW_MAP_LAYER_DEFAULT_PREFIX = 'New layer'; +export const MAP_SAVED_OBJECT_TYPE = 'map'; +// TODO: Replace with actual app icon +export const MAPS_APP_ICON = 'gisApp'; +export const MAPS_VISUALIZATION_DESCRIPTION = 'Create map visualization with multiple layers'; // Starting position [lng, lat] and zoom export const MAP_INITIAL_STATE = { diff --git a/cypress/integration/add_map_to_dashboard.spec.js b/cypress/integration/add_map_to_dashboard.spec.js new file mode 100644 index 00000000..fbb3da8f --- /dev/null +++ b/cypress/integration/add_map_to_dashboard.spec.js @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../utils/constants'; + +describe('Add map to dashboard', () => { + before(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { + retryOnStatusCodeFailure: true, + timeout: 60000, + }); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/(Add|View) data/) + .click(); + cy.wait(60000); + }); + + it('Add new map to dashboard', () => { + const testMapName = 'saved-map-' + Date.now().toString(); + cy.visit(`${BASE_PATH}/app/dashboards`); + cy.get('button[data-test-subj="newItemButton"]').click(); + cy.get('button[data-test-subj="dashboardAddNewPanelButton"]').click(); + cy.get('button[data-test-subj="visType-customImportMap"]').click(); + cy.wait(5000).get('button[data-test-subj="mapSaveButton"]').click(); + cy.wait(5000).get('[data-test-subj="savedObjectTitle"]').type(testMapName); + cy.wait(5000).get('[data-test-subj="confirmSaveSavedObjectButton"]').click(); + cy.get('.embPanel__titleText').should('contain', testMapName); + }); + + after(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); + cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); + }); +}); diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 306f3d37..e8abd481 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -4,6 +4,6 @@ "opensearchDashboardsVersion": "3.0.0", "server": true, "ui": true, - "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "navigation", "savedObjects", "data"], + "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable", "visualizations"], "optionalPlugins": ["home"] } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..b786a608 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,249 @@ +{ + "name": "mapsDashboards", + "version": "3.0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "requires": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + } + }, + "@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==" + }, + "@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" + }, + "@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" + }, + "@mapbox/tiny-sdf": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz", + "integrity": "sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw==" + }, + "@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + }, + "@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "requires": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" + }, + "@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + }, + "@types/mapbox__point-geometry": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", + "integrity": "sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA==" + }, + "@types/mapbox__vector-tile": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz", + "integrity": "sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g==", + "requires": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "@types/pbf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz", + "integrity": "sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ==" + }, + "csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" + }, + "earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, + "geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "maplibre-gl": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-2.4.0.tgz", + "integrity": "sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==", + "requires": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.5", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@types/geojson": "^7946.0.10", + "@types/mapbox__point-geometry": "^0.1.2", + "@types/mapbox__vector-tile": "^1.3.0", + "@types/pbf": "^3.0.2", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.4", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "global-prefix": "^3.0.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.2", + "quickselect": "^2.0.0", + "supercluster": "^7.1.5", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" + }, + "pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "requires": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + } + }, + "potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, + "protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, + "resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "requires": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "requires": { + "kdbush": "^3.0.0" + } + }, + "tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "requires": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } +} diff --git a/public/components/app.tsx b/public/components/app.tsx index 79e07c1c..a0122f46 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -24,15 +24,13 @@ export const MapsDashboardsApp = ({ mapConfig }: Props) => { return ( -
- - } - /> - } /> - -
+ + } + /> + } /> +
); diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 802379c5..65b5761c 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -8,7 +8,6 @@ import { DropResult, EuiButtonEmpty, EuiButtonIcon, - EuiConfirmModal, EuiDragDropContext, EuiDraggable, EuiDroppable, @@ -60,6 +59,7 @@ interface Props { mapState: MapState; zoom: number; mapConfig: ConfigSchema; + inDashboardMode: boolean; } export const LayerControlPanel = memo( @@ -72,6 +72,7 @@ export const LayerControlPanel = memo( mapState, zoom, mapConfig, + inDashboardMode, }: Props) => { const { services } = useOpenSearchDashboards(); const { @@ -321,8 +322,13 @@ export const LayerControlPanel = memo( return visibleLayers.includes(layer); }; + if (inDashboardMode) { + return null; + } + + let content; if (isLayerControlVisible) { - return ( + content = ( ); + } else { + content = ( + + setIsLayerControlVisible((visible) => !visible)} + aria-label="Show layer control" + title="Expand layers panel" + /> + + ); } - return ( - - setIsLayerControlVisible((visible) => !visible)} - aria-label="Show layer control" - title="Expand layers panel" - /> - - ); + return
{content}
; } ); diff --git a/public/components/map_container/map_container.scss b/public/components/map_container/map_container.scss index 7453ca94..a4ee3065 100644 --- a/public/components/map_container/map_container.scss +++ b/public/components/map_container/map_container.scss @@ -6,10 +6,10 @@ @import "maplibre-gl/dist/maplibre-gl.css"; @import "../../variables"; -/* stylelint-disable no-empty-source */ -.map-container { - width: 100%; - min-height: calc(100vh - #{$mapHeaderOffset}); +.mapAppContainer, .map-page, .map-container, .map-main{ + display: flex; + flex-direction: column; + flex: 1; } .maplibregl-ctrl-top-left { diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index a17708b7..8cf58546 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -6,18 +6,26 @@ import React, { useEffect, useRef, useState } from 'react'; import { EuiPanel } from '@elastic/eui'; import { LngLat, Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; -import { debounce } from 'lodash'; +import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { MAP_INITIAL_STATE, DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; -import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { + IndexPattern, + RefreshInterval, + TimeRange, + Filter, + Query, +} from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { createPopup, getPopupLocation, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; import { handleDataLayerRender } from '../../model/layerRenderController'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { ResizeChecker } from '../../../../../src/plugins/opensearch_dashboards_utils/public'; import { MapServices } from '../../types'; import { ConfigSchema } from '../../../common/config'; +import { referenceLayerTypeLookup } from '../../model/layersFunctions'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -27,6 +35,11 @@ interface MapContainerProps { maplibreRef: React.MutableRefObject; mapState: MapState; mapConfig: ConfigSchema; + inDashboardMode: boolean; + timeRange?: TimeRange; + refreshConfig?: RefreshInterval; + filters?: Filter[]; + query?: Query; } export const MapContainer = ({ @@ -37,6 +50,11 @@ export const MapContainer = ({ maplibreRef, mapState, mapConfig, + inDashboardMode, + timeRange, + refreshConfig, + filters, + query, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); const mapContainer = useRef(null); @@ -68,6 +86,28 @@ export const MapContainer = ({ maplibreInstance.on('move', () => { return setZoom(Number(maplibreInstance.getZoom().toFixed(2))); }); + + // By default, Maplibre only auto resize map window when browser size changes, but in dashboard mode, we need + // manually resize map window size when map panel size changes + const mapContainerElement: HTMLElement | null = document.querySelector('.map-page'); + let resizeChecker: ResizeChecker; + if (mapContainerElement) { + resizeChecker = new ResizeChecker(mapContainerElement); + if (inDashboardMode) { + resizeChecker.on( + 'resize', + throttle(() => { + maplibreInstance?.resize(); + }, 300) + ); + } + } + return () => { + maplibreInstance.remove(); + if (resizeChecker) { + resizeChecker.destroy(); + } + }; }, []); // Create onClick tooltip for each layer features that has tooltip enabled @@ -166,8 +206,43 @@ export const MapContainer = ({ }; }, [layers, mapState, services]); + // Update data layers when state bar time range, filters and query changes + useEffect(() => { + layers.forEach((layer: MapLayerSpecification) => { + if (referenceLayerTypeLookup[layer.type]) { + return; + } + handleDataLayerRender( + layer, + mapState, + services, + maplibreRef, + undefined, + timeRange, + filters, + query + ); + }); + }, [timeRange, mapState, filters]); + + // Update data layers when state bar enable auto refresh + useEffect(() => { + let intervalId: NodeJS.Timeout | undefined; + if (refreshConfig && !refreshConfig.pause) { + intervalId = setInterval(() => { + layers.forEach((layer: MapLayerSpecification) => { + if (referenceLayerTypeLookup[layer.type]) { + return; + } + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined, timeRange); + }); + }, refreshConfig.value); + } + return () => clearInterval(intervalId); + }, [refreshConfig]); + return ( -
+
-
- {mounted && ( - - )} -
+ {mounted && ( + + )}
); diff --git a/public/components/map_page/index.ts b/public/components/map_page/index.ts index a79e0689..a43e82ae 100644 --- a/public/components/map_page/index.ts +++ b/public/components/map_page/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { MapPage } from './map_page'; +export { MapPage, MapComponent } from './map_page'; diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index 26beef27..0151d9a0 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { SimpleSavedObject } from 'opensearch-dashboards/public'; import { Map as Maplibre } from 'maplibre-gl'; +import { SimpleSavedObject } from '../../../../../src/core/public'; import { MapContainer } from '../map_container'; import { MapTopNavMenu } from '../map_top_nav'; import { MapServices } from '../../types'; @@ -17,22 +17,45 @@ import { MAP_LAYER_DEFAULT_NAME, OPENSEARCH_MAP_LAYER, } from '../../../common'; +import { MapLayerSpecification } from '../../model/mapLayerType'; import { getLayerConfigMap, getInitialMapState } from '../../utils/getIntialConfig'; -import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { + Filter, + IndexPattern, + RefreshInterval, + TimeRange, + Query, +} from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; -interface Props { +interface MapPageProps { mapConfig: ConfigSchema; } -export const MapPage = ({ mapConfig }: Props) => { +interface MapComponentProps { + mapConfig: ConfigSchema; + mapIdFromSavedObject: string; + timeRange?: TimeRange; + inDashboardMode: boolean; + refreshConfig?: RefreshInterval; + filters?: Filter[]; + query?: Query; +} +export const MapComponent = ({ + mapIdFromSavedObject, + mapConfig, + timeRange, + inDashboardMode, + refreshConfig, + filters, + query, +}: MapComponentProps) => { const { services } = useOpenSearchDashboards(); const { savedObjects: { client: savedObjectsClient }, } = services; const [layers, setLayers] = useState([]); - const { id: mapIdFromUrl } = useParams<{ id: string }>(); const [savedMapObject, setSavedMapObject] = useState | null>(); const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); @@ -40,8 +63,8 @@ export const MapPage = ({ mapConfig }: Props) => { const [mapState, setMapState] = useState(getInitialMapState()); useEffect(() => { - if (mapIdFromUrl) { - savedObjectsClient.get('map', mapIdFromUrl).then((res) => { + if (mapIdFromSavedObject) { + savedObjectsClient.get('map', mapIdFromSavedObject).then((res) => { setSavedMapObject(res); const layerList: MapLayerSpecification[] = JSON.parse(res.attributes.layerList as string); const savedMapState: MapState = JSON.parse(res.attributes.mapState as string); @@ -58,25 +81,30 @@ export const MapPage = ({ mapConfig }: Props) => { setLayersIndexPatterns(savedIndexPatterns); }); } else { - const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap(mapConfig)[ - OPENSEARCH_MAP_LAYER.type - ]; + const initialDefaultLayer: MapLayerSpecification = + getLayerConfigMap(mapConfig)[OPENSEARCH_MAP_LAYER.type]; initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; setLayers([initialDefaultLayer]); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( -
- +
+ {inDashboardMode ? null : ( + + )} + { maplibreRef={maplibreRef} mapState={mapState} mapConfig={mapConfig} + inDashboardMode={inDashboardMode} + timeRange={timeRange} + refreshConfig={refreshConfig} + filters={filters} + query={query} />
); }; + +export const MapPage = ({ mapConfig }: MapPageProps) => { + const { id: mapId } = useParams<{ id: string }>(); + return ( + + ); +}; diff --git a/public/components/map_top_nav/get_top_nav_config.tsx b/public/components/map_top_nav/get_top_nav_config.tsx index 4269c356..d69f8fc6 100644 --- a/public/components/map_top_nav/get_top_nav_config.tsx +++ b/public/components/map_top_nav/get_top_nav_config.tsx @@ -7,15 +7,14 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { - OnSaveProps, SavedObjectSaveModalOrigin, showSaveModal, checkForDuplicateTitle, + SavedObjectSaveOpts, } from '../../../../../src/plugins/saved_objects/public'; import { MapServices } from '../../types'; import { MapState } from '../../model/mapState'; - -const SAVED_OBJECT_TYPE = 'map'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../common'; interface GetTopNavConfigParams { mapIdFromUrl: string; @@ -25,16 +24,11 @@ interface GetTopNavConfigParams { setTitle: (title: string) => void; setDescription: (description: string) => void; mapState: MapState; + originatingApp?: string; } export const getTopNavConfig = ( - { - notifications: { toasts }, - i18n: { Context: I18nContext }, - savedObjects: { client: savedObjectsClient }, - history, - overlays, - }: MapServices, + services: MapServices, { mapIdFromUrl, layers, @@ -43,8 +37,15 @@ export const getTopNavConfig = ( setTitle, setDescription, mapState, + originatingApp, }: GetTopNavConfigParams ) => { + const { + embeddable, + i18n: { Context: I18nContext }, + scopedHistory, + } = services; + const stateTransfer = embeddable.getStateTransfer(scopedHistory); const topNavConfig: TopNavMenuData[] = [ { iconType: 'save', @@ -53,65 +54,8 @@ export const getTopNavConfig = ( label: i18n.translate('maps.topNav.saveMapButtonLabel', { defaultMessage: `Save`, }), - run: (_anchorElement) => { - const onModalSave = async ({ newTitle, newDescription, onTitleDuplicate }: OnSaveProps) => { - let newlySavedMap; - const saveAttributes = { - title: newTitle, - description: newDescription, - layerList: JSON.stringify(layers), - mapState: JSON.stringify(mapState), - }; - try { - await checkForDuplicateTitle( - { - title: newTitle, - lastSavedTitle: title, - copyOnSave: false, - getDisplayName: () => SAVED_OBJECT_TYPE, - getOpenSearchType: () => SAVED_OBJECT_TYPE, - }, - false, - onTitleDuplicate, - { - savedObjectsClient, - overlays, - } - ); - } catch (_error) { - return {}; - } - if (mapIdFromUrl) { - // edit existing map - newlySavedMap = await savedObjectsClient.update( - SAVED_OBJECT_TYPE, - mapIdFromUrl, - saveAttributes - ); - } else { - // save new map - newlySavedMap = await savedObjectsClient.create(SAVED_OBJECT_TYPE, saveAttributes); - } - const id = newlySavedMap.id; - if (id) { - history.push({ - ...history.location, - pathname: `${id}`, - }); - setTitle(newTitle); - setDescription(newDescription); - toasts.addSuccess({ - title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { - defaultMessage: `Saved ${newTitle}`, - values: { - visTitle: newTitle, - }, - }), - }); - } - return { id }; - }; - + testId: 'mapSaveButton', + run: (_anchorElement: any) => { const documentInfo = { title, description, @@ -120,9 +64,20 @@ export const getTopNavConfig = ( const saveModal = ( {}} + originatingApp={originatingApp} + getAppNameFromId={stateTransfer.getAppNameFromId} /> ); showSaveModal(saveModal, I18nContext); @@ -131,3 +86,121 @@ export const getTopNavConfig = ( ]; return topNavConfig; }; + +export const onGetSave = ( + title: string, + originatingApp: string | undefined, + mapIdFromUrl: string, + services: MapServices, + layers: any, + mapState: MapState, + setTitle: (title: string) => void, + setDescription: (description: string) => void +) => { + const onSave = async ({ + newTitle, + newDescription, + onTitleDuplicate, + returnToOrigin, + }: SavedObjectSaveOpts & { + newTitle: string; + newCopyOnSave: boolean; + returnToOrigin: boolean; + newDescription?: string; + }) => { + const { + savedObjects: { client: savedObjectsClient }, + history, + toastNotifications, + overlays, + embeddable, + application, + } = services; + const stateTransfer = embeddable.getStateTransfer(); + let newlySavedMap; + const saveAttributes = { + title: newTitle, + description: newDescription, + layerList: JSON.stringify(layers), + mapState: JSON.stringify(mapState), + }; + try { + await checkForDuplicateTitle( + { + title: newTitle, + lastSavedTitle: title, + copyOnSave: false, + getDisplayName: () => MAP_SAVED_OBJECT_TYPE, + getOpenSearchType: () => MAP_SAVED_OBJECT_TYPE, + }, + false, + onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } catch (_error) { + return {}; + } + try { + if (mapIdFromUrl) { + // edit existing map + newlySavedMap = await savedObjectsClient.update( + MAP_SAVED_OBJECT_TYPE, + mapIdFromUrl, + saveAttributes + ); + } else { + // save new map + newlySavedMap = await savedObjectsClient.create(MAP_SAVED_OBJECT_TYPE, saveAttributes); + } + const id = newlySavedMap.id; + if (id) { + history.push({ + ...history.location, + pathname: `${id}`, + }); + setTitle(newTitle); + if (newDescription) { + setDescription(newDescription); + } + toastNotifications.addSuccess({ + title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { + defaultMessage: `Saved ${newTitle}`, + values: { + visTitle: newTitle, + }, + }), + }); + if (originatingApp && returnToOrigin) { + // create or edit map directly from another app, such as `dashboard` + if (!mapIdFromUrl && stateTransfer) { + // create new embeddable to transfer to originatingApp + await stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { + state: { type: MAP_SAVED_OBJECT_TYPE, input: { savedObjectId: id } }, + }); + return { id }; + } else { + // update an existing visBuilder from another app + application.navigateToApp(originatingApp); + } + } + } + return { id }; + } catch (error: any) { + toastNotifications.addDanger({ + title: i18n.translate('maps.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving ${newTitle}`, + values: { + visTitle: newTitle, + }, + }), + text: error.message, + 'data-test-subj': 'saveMapError', + }); + return { error }; + } + }; + return onSave; +}; diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index 15b2fbe3..b5b9a184 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; -import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { SimpleSavedObject } from '../../../../../src/core/public'; import { IndexPattern, Query, TimeRange } from '../../../../../src/plugins/data/public'; -import { DASHBOARDS_MAPS_LAYER_TYPE, PLUGIN_NAVIGATION_BAR_ID } from '../../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, MAPS_APP_ID } from '../../../common'; import { getTopNavConfig } from './get_top_nav_config'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; @@ -24,11 +24,16 @@ interface MapTopNavMenuProps { maplibreRef: any; mapState: MapState; setMapState: (mapState: MapState) => void; + inDashboardMode: boolean; + timeRange?: TimeRange; + originatingApp?: string; } export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, + inDashboardMode, + timeRange, layers, layersIndexPatterns, maplibreRef, @@ -43,6 +48,8 @@ export const MapTopNavMenu = ({ }, chrome, application: { navigateToApp }, + embeddable, + scopedHistory, } = services; const [title, setTitle] = useState(''); @@ -52,6 +59,7 @@ export const MapTopNavMenu = ({ const [queryConfig, setQueryConfig] = useState({ query: '', language: 'kuery' }); const [refreshIntervalValue, setRefreshIntervalValue] = useState(60000); const [isRefreshPaused, setIsRefreshPaused] = useState(false); + const [originatingApp, setOriginatingApp] = useState(); const changeTitle = useCallback( (newTitle: string) => { chrome.setBreadcrumbs(getSavedMapBreadcrumbs(newTitle, navigateToApp)); @@ -60,6 +68,14 @@ export const MapTopNavMenu = ({ [chrome, navigateToApp] ); + useEffect(() => { + const { originatingApp: value } = + embeddable + .getStateTransfer(scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; + setOriginatingApp(value); + }, [embeddable, scopedHistory]); + useEffect(() => { if (savedMapObject) { setTitle(savedMapObject.attributes.title); @@ -73,10 +89,9 @@ export const MapTopNavMenu = ({ const refreshDataLayerRender = () => { layers.forEach((layer: MapLayerSpecification) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { - return; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); } - handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); }); }; @@ -90,13 +105,17 @@ export const MapTopNavMenu = ({ }; useEffect(() => { - setDateFrom(mapState.timeRange.from); - setDateTo(mapState.timeRange.to); + if (!inDashboardMode) { + setDateFrom(mapState.timeRange.from); + setDateTo(mapState.timeRange.to); + } else { + setDateFrom(timeRange!.from); + setDateTo(timeRange!.to); + } setQueryConfig(mapState.query); setIsRefreshPaused(mapState.refreshInterval.pause); setRefreshIntervalValue(mapState.refreshInterval.value); - refreshDataLayerRender(); - }, [mapState]); + }, [mapState, timeRange]); const onRefreshChange = useCallback( ({ isPaused, refreshInterval }: { isPaused: boolean; refreshInterval: number }) => { @@ -106,23 +125,28 @@ export const MapTopNavMenu = ({ [] ); + const config = useMemo(() => { + return getTopNavConfig(services, { + mapIdFromUrl, + layers, + title, + description, + setTitle, + setDescription, + mapState, + originatingApp, + }); + }, [services, mapIdFromUrl, layers, title, description, mapState, originatingApp]); + return ( { const { @@ -39,7 +39,7 @@ export const MapsList = () => { }, [docTitle, navigateToApp, setBreadcrumbs]); const navigateToSavedMapPage = (id: string) => { - navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: `/${id}` }); + navigateToApp(MAPS_APP_ID, { path: `/${id}` }); }; const tableColumns = [ @@ -70,7 +70,7 @@ export const MapsList = () => { ]; const navigateToCreateMapPage = () => { - navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: APP_PATH.CREATE_MAP }); + navigateToApp(MAPS_APP_ID, { path: APP_PATH.CREATE_MAP }); }; const fetchMaps = useCallback(async (): Promise<{ diff --git a/public/embeddable/index.ts b/public/embeddable/index.ts new file mode 100644 index 00000000..9687b6ce --- /dev/null +++ b/public/embeddable/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './map_embeddable'; +export * from './map_embeddable_factory'; diff --git a/public/embeddable/map_component.tsx b/public/embeddable/map_component.tsx new file mode 100644 index 00000000..a2fb58f4 --- /dev/null +++ b/public/embeddable/map_component.tsx @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { + withEmbeddableSubscription, + EmbeddableOutput, +} from '../../../../src/plugins/embeddable/public'; +import { MapEmbeddable, MapInput } from './map_embeddable'; +import { MapComponent } from '../components/map_page/'; +import { OpenSearchDashboardsContextProvider } from '../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../types'; +import { TimeRange } from '../../../../src/plugins/data/common'; + +interface Props { + embeddable: MapEmbeddable; + input: MapInput; + output: EmbeddableOutput; +} +export function MapEmbeddableComponentInner({ embeddable, input }: Props) { + const [timeRange, setTimeRange] = useState(input.timeRange); + const [refreshConfig, setRefreshConfig] = useState(input.refreshConfig); + const [filters, setFilters] = useState(input.filters); + const [query, setQuery] = useState(input.query); + const services: MapServices = { + ...embeddable.getServiceSettings(), + }; + + useEffect(() => { + setTimeRange(input.timeRange); + setRefreshConfig(input.refreshConfig); + setFilters(input.filters); + setQuery(input.query); + }, [input.refreshConfig, input.timeRange, input.filters, input.query]); + + return ( + + + + ); +} + +export const MapEmbeddableComponent = withEmbeddableSubscription< + MapInput, + EmbeddableOutput, + MapEmbeddable +>(MapEmbeddableComponentInner); diff --git a/public/embeddable/map_embeddable.tsx b/public/embeddable/map_embeddable.tsx new file mode 100644 index 00000000..72e03d0b --- /dev/null +++ b/public/embeddable/map_embeddable.tsx @@ -0,0 +1,97 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; +import { MAP_SAVED_OBJECT_TYPE, MAPS_APP_ID } from '../../common'; +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, + IContainer, +} from '../../../../src/plugins/embeddable/public'; +import { MapEmbeddableComponent } from './map_component'; +import { ConfigSchema } from '../../common/config'; +import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; +import { RefreshInterval } from '../../../../src/plugins/data/public'; + +export const MAP_EMBEDDABLE = MAP_SAVED_OBJECT_TYPE; + +export interface MapInput extends EmbeddableInput { + savedObjectId: string; + refreshConfig?: RefreshInterval; +} + +export type MapOutput = EmbeddableOutput; + +function getOutput(input: MapInput, editUrl: string, tittle: string): MapOutput { + return { + editable: true, + editUrl, + defaultTitle: tittle, + editApp: MAPS_APP_ID, + editPath: input.savedObjectId, + }; +} + +export class MapEmbeddable extends Embeddable { + public readonly type = MAP_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private readonly mapConfig: ConfigSchema; + private readonly services: any; + constructor( + initialInput: MapInput, + { + parent, + services, + mapConfig, + editUrl, + savedMapAttributes, + }: { + parent?: IContainer; + services: any; + mapConfig: ConfigSchema; + editUrl: string; + savedMapAttributes: MapSavedObjectAttributes; + } + ) { + super(initialInput, getOutput(initialInput, editUrl, savedMapAttributes.title), parent); + this.mapConfig = mapConfig; + this.services = services; + this.subscription = this.getInput$().subscribe(() => { + this.updateOutput(getOutput(this.input, editUrl, savedMapAttributes.title)); + }); + } + + public render(node: HTMLElement) { + this.node = node; + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + ReactDOM.render(, node); + } + + public reload() { + if (this.node) { + this.render(this.node); + } + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } + public getServiceSettings() { + return this.services; + } + public getMapConfig() { + return this.mapConfig; + } +} diff --git a/public/embeddable/map_embeddable_factory.tsx b/public/embeddable/map_embeddable_factory.tsx new file mode 100644 index 00000000..b1534687 --- /dev/null +++ b/public/embeddable/map_embeddable_factory.tsx @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + IContainer, + EmbeddableFactoryDefinition, + ErrorEmbeddable, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { MAP_EMBEDDABLE, MapInput, MapOutput, MapEmbeddable } from './map_embeddable'; +import { MAPS_APP_ICON, MAPS_APP_ID } from '../../common'; +import { ConfigSchema } from '../../common/config'; +import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; +import { MAPS_APP_DISPLAY_NAME } from '../../common/constants/shared'; +import { getTimeFilter } from '../services'; + +interface StartServices { + services: { + application: { + getUrlForApp: (appId: string, options?: { path?: string }) => string; + navigateToApp: (appId: string, options?: { path?: string }) => Promise; + }; + savedObjects: { + client: { + get: (type: string, id: string) => Promise; + }; + }; + }; + mapConfig: ConfigSchema; +} + +export class MapEmbeddableFactoryDefinition + implements EmbeddableFactoryDefinition +{ + public readonly type = MAP_EMBEDDABLE; + + public readonly savedObjectMetaData = { + name: MAPS_APP_DISPLAY_NAME, + type: MAP_EMBEDDABLE, + getIconForSavedObject: () => MAPS_APP_ICON, + }; + + constructor(private getStartServices: () => Promise) {} + + public async isEditable() { + return true; + } + + // Maps app will be created from visualization list + public canCreateNew() { + return false; + } + + public createFromSavedObject = async ( + savedObjectId: string, + input: Partial & { id: string }, + parent?: IContainer + ): Promise => { + try { + const { services, mapConfig } = await this.getStartServices(); + const url = services.application.getUrlForApp(MAPS_APP_ID, { + path: savedObjectId, + }); + const timeFilter = getTimeFilter(); + const savedMap = await services.savedObjects.client.get(MAP_EMBEDDABLE, savedObjectId); + const savedMapAttributes = savedMap.attributes as MapSavedObjectAttributes; + return new MapEmbeddable( + { + ...input, + savedObjectId, + title: savedMapAttributes.title, + }, + { + parent, + services, + mapConfig, + editUrl: url, + savedMapAttributes, + timeFilter, + } + ); + } catch (error) { + return new ErrorEmbeddable(error.message, input); + } + }; + + public async create(initialInput: MapInput, parent?: IContainer) { + return undefined; + } + + public getDisplayName() { + return i18n.translate('maps.displayName', { + defaultMessage: MAPS_APP_DISPLAY_NAME, + }); + } +} diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 7c11bb2d..cf160535 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -14,11 +14,13 @@ import { getTime, IOpenSearchDashboardsSearchResponse, isCompleteResponse, + TimeRange, + Query, } from '../../../../src/plugins/data/common'; import { layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; -import {GeoBounds, getBounds} from './map/boundary'; +import { GeoBounds, getBounds } from './map/boundary'; import { buildBBoxFilter } from './geo/filter'; interface MaplibreRef { @@ -28,8 +30,10 @@ interface MaplibreRef { export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, - { data, notifications }: MapServices, - filters: Filter[] = [] + { data, toastNotifications }: MapServices, + filters: Filter[] = [], + timeRange?: TimeRange, + query?: Query ): Promise => { return new Promise(async (resolve, reject) => { if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { @@ -42,17 +46,19 @@ export const prepareDataLayerSource = ( sourceFields.push(...sourceConfig.tooltipFields); } let buildQuery; + let selectedTimeRange; if (indexPattern) { - const timeFilters = getTime(indexPattern, mapState.timeRange); - buildQuery = buildOpenSearchQuery( - indexPattern, - [], - [ - ...filters, - ...(layer.source.filters ? layer.source.filters : []), - ...(timeFilters ? [timeFilters] : []), - ] - ); + if (timeRange) { + selectedTimeRange = timeRange; + } else { + selectedTimeRange = mapState.timeRange; + } + const timeFilters = getTime(indexPattern, selectedTimeRange); + buildQuery = buildOpenSearchQuery(indexPattern, query ? [query] : [], [ + ...filters, + ...(layer.source.filters ? layer.source.filters : []), + ...(timeFilters ? [timeFilters] : []), + ]); } const request = { params: { @@ -72,7 +78,7 @@ export const prepareDataLayerSource = ( search$.unsubscribe(); resolve({ dataSource, layer }); } else { - notifications.toasts.addWarning('An error has occurred when query dataSource'); + toastNotifications.addWarning('An error has occurred when query dataSource'); search$.unsubscribe(); reject(); } @@ -90,9 +96,14 @@ export const handleDataLayerRender = ( mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, - beforeLayerId: string | undefined + beforeLayerId: string | undefined, + timeRange?: TimeRange, + filtersFromDashboard?: Filter[], + query?: Query ) => { + // filters are passed from dashboard filters and geo bounding box filters const filters: Filter[] = []; + filters.push(...(filtersFromDashboard ? filtersFromDashboard : [])); const geoField = mapLayer.source.geoFieldName; const geoFieldType = mapLayer.source.geoFieldType; @@ -106,12 +117,14 @@ export const handleDataLayerRender = ( const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); filters.push(geoBoundingBoxFilter); - return prepareDataLayerSource(mapLayer, mapState, services, filters).then((result) => { - const { layer, dataSource } = result; - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + return prepareDataLayerSource(mapLayer, mapState, services, filters, timeRange, query).then( + (result) => { + const { layer, dataSource } = result; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + } } - }); + ); }; export const handleReferenceLayerRender = ( diff --git a/public/plugin.tsx b/public/plugin.tsx index ee892f07..ce777fdd 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -19,32 +19,42 @@ import { } from './types'; import { PLUGIN_NAME, - PLUGIN_NAVIGATION_BAR_ID, - PLUGIN_NAVIGATION_BAR_TILE, + MAPS_APP_ID, + MAPS_APP_DISPLAY_NAME, + PLUGIN_ID, } from '../common/constants/shared'; import { ConfigSchema } from '../common/config'; import { AppPluginSetupDependencies } from './types'; import { RegionMapVisualizationDependencies } from '../../../src/plugins/region_map/public'; import { VectorUploadOptions } from './components/vector_upload_options'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; +import { + MAPS_APP_ICON, + MAP_SAVED_OBJECT_TYPE, + APP_PATH, + MAPS_VISUALIZATION_DESCRIPTION, +} from '../common'; +import { MapEmbeddableFactoryDefinition } from './embeddable'; +import { setTimeFilter } from './services'; export class CustomImportMapPlugin - implements Plugin { + implements Plugin +{ readonly _initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this._initializerContext = initializerContext; } public setup( core: CoreSetup, - { regionMap }: AppPluginSetupDependencies + { regionMap, embeddable, visualizations }: AppPluginSetupDependencies ): CustomImportMapPluginSetup { const mapConfig: ConfigSchema = { ...this._initializerContext.config.get(), }; // Register an application into the side navigation menu core.application.register({ - id: PLUGIN_NAVIGATION_BAR_ID, - title: PLUGIN_NAVIGATION_BAR_TILE, + id: MAPS_APP_ID, + title: MAPS_APP_DISPLAY_NAME, order: 5100, category: { id: 'opensearch', @@ -56,7 +66,11 @@ export class CustomImportMapPlugin const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json const [coreStart, depsStart] = await core.getStartServices(); - const { navigation, data } = depsStart as AppPluginStartDependencies; + const { + navigation, + data, + embeddable: useEmbeddable, + } = depsStart as AppPluginStartDependencies; // make sure the index pattern list is up-to-date data.indexPatterns.clearCache(); @@ -73,12 +87,57 @@ export class CustomImportMapPlugin toastNotifications: coreStart.notifications.toasts, history: params.history, data, + embeddable: useEmbeddable, + scopedHistory: params.history, }; + params.element.classList.add('mapAppContainer'); // Render the application return renderApp(params, services, mapConfig); }, }); + const mapEmbeddableFactory = new MapEmbeddableFactoryDefinition(async () => { + const [coreStart, depsStart] = await core.getStartServices(); + const { navigation, data: useData } = depsStart as AppPluginStartDependencies; + return { + mapConfig, + services: { + ...coreStart, + navigation, + data: useData, + toastNotifications: coreStart.notifications.toasts, + }, + }; + }); + embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, mapEmbeddableFactory as any); + + visualizations.registerAlias({ + name: PLUGIN_ID, + title: MAPS_APP_DISPLAY_NAME, + description: MAPS_VISUALIZATION_DESCRIPTION, + icon: MAPS_APP_ICON, + aliasApp: MAPS_APP_ID, + aliasPath: APP_PATH.CREATE_MAP, + stage: 'production', + appExtensions: { + visualizations: { + docTypes: [MAP_SAVED_OBJECT_TYPE], + toListItem: ({ id, attributes, updated_at: updatedAt }) => ({ + description: attributes?.description, + editApp: MAPS_APP_ID, + editUrl: `${encodeURIComponent(id)}`, + icon: MAPS_APP_ICON, + id, + savedObjectType: MAP_SAVED_OBJECT_TYPE, + title: attributes?.title, + typeTitle: MAPS_APP_DISPLAY_NAME, + stage: 'production', + updated_at: updatedAt, + }), + }, + }, + }); + const customSetup = async () => { const [coreStart] = await core.getStartServices(); regionMap.addOptionTab({ @@ -108,7 +167,8 @@ export class CustomImportMapPlugin }; } - public start(core: CoreStart): CustomImportMapPluginStart { + public start(core: CoreStart, { data }: AppPluginStartDependencies): CustomImportMapPluginStart { + setTimeFilter(data.query.timefilter.timefilter); return {}; } diff --git a/public/services.ts b/public/services.ts index 25166d39..da4ce07b 100644 --- a/public/services.ts +++ b/public/services.ts @@ -4,6 +4,8 @@ */ import { CoreStart } from '../../../src/core/public'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; +import { TimefilterContract } from '../../../src/plugins/data/public'; export const postGeojson = async (requestBody: any, http: CoreStart['http']) => { try { @@ -28,3 +30,5 @@ export const getIndex = async (indexName: string, http: CoreStart['http']) => { return e; } }; + +export const [getTimeFilter, setTimeFilter] = createGetterSetter('TimeFilter'); diff --git a/public/types.ts b/public/types.ts index d34f7273..657d6c4c 100644 --- a/public/types.ts +++ b/public/types.ts @@ -8,16 +8,19 @@ import { CoreStart, SavedObjectsClient, ToastsStart, -} from 'opensearch-dashboards/public'; + ScopedHistory, +} from '../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; -import { DataPublicPluginStart } from '../../../src/plugins/data/public'; - +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; savedObjects: SavedObjectsClient; data: DataPublicPluginStart; + embeddable: EmbeddableStart; } export interface MapServices extends CoreStart { @@ -28,20 +31,24 @@ export interface MapServices extends CoreStart { toastNotifications: ToastsStart; history: AppMountParameters['history']; data: DataPublicPluginStart; + application: CoreStart['application']; + i18n: CoreStart['i18n']; + savedObjects: CoreStart['savedObjects']; + overlays: CoreStart['overlays']; + embeddable: EmbeddableStart; + scopedHistory: ScopedHistory; + chrome: CoreStart['chrome']; } // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomImportMapPluginSetup { - getGreeting: () => string; -} +export interface CustomImportMapPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomImportMapPluginStart {} -export interface AppPluginStartDependencies { - navigation: NavigationPublicPluginStart; -} - export interface AppPluginSetupDependencies { regionMap: RegionMapPluginSetup; + embeddable: EmbeddableSetup; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; } diff --git a/public/utils/breadcrumbs.ts b/public/utils/breadcrumbs.ts index daa69ea2..40308ce0 100644 --- a/public/utils/breadcrumbs.ts +++ b/public/utils/breadcrumbs.ts @@ -4,7 +4,7 @@ */ import { i18n } from '@osd/i18n'; -import {PLUGIN_NAVIGATION_BAR_ID} from '../../common'; +import { MAPS_APP_ID } from '../../common'; export function getMapsLandingBreadcrumbs(navigateToApp: any) { return [ @@ -12,7 +12,7 @@ export function getMapsLandingBreadcrumbs(navigateToApp: any) { text: i18n.translate('maps.listing.breadcrumb', { defaultMessage: 'Maps', }), - onClick: () => navigateToApp(PLUGIN_NAVIGATION_BAR_ID), + onClick: () => navigateToApp(MAPS_APP_ID), }, ]; } diff --git a/yarn.lock b/yarn.lock index f21443e3..7b5d9661 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,6 +127,32 @@ resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-test-renderer@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz#7b7f69ca98821ea5501b21ba24ea7b6139da2243" + integrity sha512-C7/5FBJ3g3sqUahguGi03O79b8afNeSD6T8/GU50oQrJCU0bVCCGQHaGKUbg2Ce8VQEEqTw8/HiS6lXHHdgkdQ== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "18.0.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" + integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" @@ -405,6 +431,11 @@ csscolorparser@~1.0.3: resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + cypress-file-upload@^5.0.8: version "5.0.8" resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" From 31acc44dd86a67869df70a2010308cd6ddb96456 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 20 Feb 2023 16:19:46 -0800 Subject: [PATCH 15/77] Align items in add new layer modal (#256) Signed-off-by: Junqiu Lei --- .../add_layer_panel/add_layer_panel.scss | 15 ++++++++++++++- .../add_layer_panel/add_layer_panel.tsx | 16 ++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/public/components/add_layer_panel/add_layer_panel.scss b/public/components/add_layer_panel/add_layer_panel.scss index 0209fa59..292153d0 100644 --- a/public/components/add_layer_panel/add_layer_panel.scss +++ b/public/components/add_layer_panel/add_layer_panel.scss @@ -8,5 +8,18 @@ } .addLayerDialog__description { - width: 272px; + width: 16 * $euiSizeM; +} + +.addLayer__types { + width: 5 * $euiSizeL; + height: 5 * $euiSizeL; +} + +.addLayer__modalBody { + padding: 0; + + .addLayer__selection { + width: 10 * $euiSizeL + } } diff --git a/public/components/add_layer_panel/add_layer_panel.tsx b/public/components/add_layer_panel/add_layer_panel.tsx index c06aa078..c1ebed8e 100644 --- a/public/components/add_layer_panel/add_layer_panel.tsx +++ b/public/components/add_layer_panel/add_layer_panel.tsx @@ -19,6 +19,7 @@ import { EuiKeyPadMenuItem, EuiSpacer, EuiText, + EuiKeyPadMenu, } from '@elastic/eui'; import './add_layer_panel.scss'; import { @@ -77,6 +78,7 @@ export const AddLayerPanel = ({ onMouseEnter={() => setHighlightItem(layerItem)} onMouseLeave={() => setHighlightItem(null)} onBlur={() => setHighlightItem(null)} + className={'addLayer__types'} > @@ -95,6 +97,7 @@ export const AddLayerPanel = ({ onMouseEnter={() => setHighlightItem(layerItem)} onMouseLeave={() => setHighlightItem(null)} onBlur={() => setHighlightItem(null)} + className="addLayer__types" > @@ -141,27 +144,24 @@ export const AddLayerPanel = ({

Add layer

- + - +
Data layer
- - {dataLayerItems} - + {dataLayerItems} +
Base layer
- - {baseLayersItems} + {baseLayersItems}
{highlightItem?.name ? highlightItem.name : 'Select a layer type'}
- {highlightItem?.description ? highlightItem.description From f3d21b9e7ebe6a68c483cccc23759b101198ed8e Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 20 Feb 2023 17:04:12 -0800 Subject: [PATCH 16/77] Add scroll bar when more layers added (#254) Signed-off-by: Junqiu Lei --- .../layer_control_panel.scss | 6 + yarn.lock | 116 +++++++++++++----- 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/public/components/layer_control_panel/layer_control_panel.scss b/public/components/layer_control_panel/layer_control_panel.scss index 27d19370..a2e5fc42 100644 --- a/public/components/layer_control_panel/layer_control_panel.scss +++ b/public/components/layer_control_panel/layer_control_panel.scss @@ -22,6 +22,12 @@ .euiListGroupItem__label { width: $euiSizeL * 6; } + + .euiDroppable { + overflow-y: auto; + overflow-x: hidden; + max-height: $euiSizeL * 8; + } } .layerControlPanel--hide { diff --git a/yarn.lock b/yarn.lock index 7b5d9661..6cd5ac0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,9 +8,9 @@ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@cypress/request@^2.88.10": - version "2.88.10" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" - integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== + version "2.88.11" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.11.tgz#5a4c7399bc2d7e7ed56e92ce5acb620c8b187047" + integrity sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -25,7 +25,7 @@ json-stringify-safe "~5.0.1" mime-types "~2.1.19" performance-now "^2.1.0" - qs "~6.5.2" + qs "~6.10.3" safe-buffer "^5.1.2" tough-cookie "~2.5.0" tunnel-agent "^0.6.0" @@ -68,9 +68,9 @@ integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== "@mapbox/tiny-sdf@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz#cdba698d3d65087643130f9af43a2b622ce0b372" - integrity sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw== + version "2.0.6" + resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz#9a1d33e5018093e88f6a4df2343e886056287282" + integrity sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA== "@mapbox/unitbezier@^0.0.1": version "0.0.1" @@ -91,7 +91,7 @@ "@opensearch-dashboards-test/opensearch-dashboards-test-library@git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main": version "1.0.4" - resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#9c0adaa6d492accd9f737aa905b2a2ca3be6840f" + resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#7066f7f48ed049fdae700c2542e23b927d2accf3" "@types/geojson@*", "@types/geojson@^7946.0.10": version "7946.0.10" @@ -113,9 +113,9 @@ "@types/pbf" "*" "@types/node@*": - version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" - integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + version "18.13.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" + integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== "@types/node@^14.14.31": version "14.18.36" @@ -250,9 +250,9 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + version "1.12.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" + integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== balanced-match@^1.0.0: version "1.0.2" @@ -307,6 +307,14 @@ cachedir@^2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -326,9 +334,9 @@ check-more-types@^2.24.0: integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== ci-info@^3.2.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" - integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== clean-stack@^2.0.0: version "2.2.0" @@ -661,11 +669,25 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + geojson-vt@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== +get-intrinsic@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -735,6 +757,18 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + http-signature@~1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" @@ -995,9 +1029,9 @@ minimatch@^3.1.1: brace-expansion "^1.1.7" minimist@^1.2.6, minimist@~1.2.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== ms@2.1.2: version "2.1.2" @@ -1021,6 +1055,11 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1086,9 +1125,9 @@ potpack@^1.0.2: integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ== prettier@^2.1.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" - integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== + version "2.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" + integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== pretty-bytes@^5.6.0: version "5.6.0" @@ -1124,14 +1163,16 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== +qs@~6.10.3: + version "6.10.5" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" + integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== + dependencies: + side-channel "^1.0.4" quickselect@^2.0.0: version "2.0.0" @@ -1220,6 +1261,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -1336,9 +1386,9 @@ tough-cookie@~2.5.0: punycode "^2.1.1" tslib@^2.1.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== tunnel-agent@^0.6.0: version "0.6.0" From 44adc21578da480643a8573367b531b03748bd94 Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Tue, 21 Feb 2023 08:45:37 -0600 Subject: [PATCH 17/77] Update Flights Default Map and it's cypress test (#269) --- cypress/integration/add_saved_object.spec.js | 4 +- .../sample_data/flights_saved_objects.ts | 137 ++++++++++++++++-- 2 files changed, 125 insertions(+), 16 deletions(-) diff --git a/cypress/integration/add_saved_object.spec.js b/cypress/integration/add_saved_object.spec.js index 12dc6528..81571bf2 100644 --- a/cypress/integration/add_saved_object.spec.js +++ b/cypress/integration/add_saved_object.spec.js @@ -27,7 +27,7 @@ describe('Add flights dataset saved object', () => { it('check if maps saved object of flights dataset can be found and open', () => { cy.visit(`${BASE_PATH}/app/maps-dashboards`); - cy.contains('[Flights] Maps Cancelled Flights Destination Location').click(); - cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Cancelled flights'); + cy.contains('[Flights] Flights Status on Maps Destination Location').click(); + cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Flights On Time'); }); }); diff --git a/server/services/sample_data/flights_saved_objects.ts b/server/services/sample_data/flights_saved_objects.ts index af8eb499..8f176dd1 100644 --- a/server/services/sample_data/flights_saved_objects.ts +++ b/server/services/sample_data/flights_saved_objects.ts @@ -11,7 +11,7 @@ const layerList = (config: ConfigSchema) => [ name: 'Default map', description: '', type: 'opensearch_vector_tile_map', - id: 'cad56fcc-be02-43ea-a1a6-1d17f437acf7', + id: '15bc3560-700e-479d-b49b-be5ece0451ce', zoomRange: [0, 22], opacity: 100, visibility: 'visible', @@ -23,11 +23,11 @@ const layerList = (config: ConfigSchema) => [ }, }, { - name: 'Cancelled flights', - description: 'Shows cancelled flights', + name: 'Flights On Time', + description: 'Shows flights that are on time', type: 'documents', - id: 'f3ae28ce-2494-4e50-ae31-4603cfcbfd7d', - zoomRange: [2, 22], + id: '033e870c-4195-48ce-8cc1-e428f0545ce4', + zoomRange: [0, 6], opacity: 70, visibility: 'visible', source: { @@ -35,7 +35,115 @@ const layerList = (config: ConfigSchema) => [ geoFieldType: 'geo_point', geoFieldName: 'DestLocation', documentRequestNumber: 1000, - tooltipFields: ['Carrier', 'Cancelled'], + tooltipFields: ['OriginAirportID', 'DestAirportID', 'FlightNum', 'Carrier', 'FlightTimeMin'], + showTooltips: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + useGeoBoundingBoxFilter: true, + filters: [ + { + meta: { + index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + alias: null, + negate: false, + disabled: false, + }, + query: { + match_phrase: { + FlightDelay: false, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + alias: null, + negate: false, + disabled: false, + }, + query: { + match_phrase: { + Cancelled: false, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + style: { + fillColor: '#36f20b', + borderColor: '#36f20b', + borderThickness: 1, + markerSize: 5, + }, + }, + { + name: 'Delayed Flights', + description: 'Shows flights that are having delay', + type: 'documents', + id: '7ccae1a8-770d-4565-8c91-6125764fd344', + zoomRange: [3, 6], + opacity: 70, + visibility: 'visible', + source: { + indexPatternRefName: 'opensearch_dashboards_sample_data_flights', + geoFieldType: 'geo_point', + geoFieldName: 'DestLocation', + documentRequestNumber: 1000, + tooltipFields: [ + 'OriginAirportID', + 'DestAirportID', + 'Carrier', + 'FlightDelayMin', + 'FlightDelayType', + ], + showTooltips: true, + indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + useGeoBoundingBoxFilter: true, + filters: [ + { + meta: { + index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + alias: null, + negate: false, + disabled: false, + }, + query: { + match_phrase: { + FlightDelay: true, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + style: { + fillColor: '#DA8B45', + borderColor: '#DA8B45', + borderThickness: 1, + markerSize: 5, + }, + }, + { + name: 'Cancelled Flights', + description: 'Shows flights that are cancelled', + type: 'documents', + id: '70a61cb4-bea5-4a7b-8f2b-e6debd4334dd', + zoomRange: [4, 22], + opacity: 70, + visibility: 'visible', + source: { + indexPatternRefName: 'opensearch_dashboards_sample_data_flights', + geoFieldType: 'geo_point', + geoFieldName: 'DestLocation', + documentRequestNumber: 1000, + tooltipFields: ['OriginAirportID', 'DestAirportID', 'FlightNum', 'Carrier'], showTooltips: true, indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', useGeoBoundingBoxFilter: true, @@ -59,8 +167,8 @@ const layerList = (config: ConfigSchema) => [ ], }, style: { - fillColor: '#CA8EAE', - borderColor: '#CA8EAE', + fillColor: '#f40a0a', + borderColor: '#f40a0a', borderThickness: 1, markerSize: 5, }, @@ -70,19 +178,20 @@ const layerList = (config: ConfigSchema) => [ export const getFlightsSavedObjects = (config: ConfigSchema) => { return [ { - id: '122713b0-9e70-11ed-9463-35a6f30dbef6', + id: '88a24e6c-0216-4f76-8bc7-c8db6c8705da', type: 'map', - updated_at: '2023-01-27T18:26:09.643Z', + updated_at: '2023-02-20T03:57:15.482Z', version: 'WzIzLDFd', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.flightsSpec.mapsCancelledFlights', { - defaultMessage: '[Flights] Maps Cancelled Flights Destination Location', + title: i18n.translate('home.sampleData.flightsSpec.flightsStatusDestinationLocationMaps', { + defaultMessage: '[Flights] Flights Status on Maps Destination Location', }), - description: 'Sample map to show cancelled flights location at destination', + description: + 'Sample map that shows flights at destination location that are on time, delayed and cancelled within a given time range.', layerList: JSON.stringify(layerList(config)), mapState: - '{"timeRange":{"from":"now-15d","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}', + '{"timeRange":{"from":"now-1w","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}', }, references: [], }, From 8d25b2bbdabbed953cc00ac502c20c86a3bf633a Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 21 Feb 2023 13:20:39 -0800 Subject: [PATCH 18/77] Refactor data handler into parent component (#271) Signed-off-by: Junqiu Lei --- .../document_layer_config_panel.tsx | 6 +- .../layer_config/layer_config_panel.tsx | 4 + .../layer_control_panel.tsx | 95 ++------------- .../map_container/map_container.tsx | 112 +++++++++++++----- public/components/map_page/map_page.tsx | 18 +-- .../components/map_top_nav/top_nav_menu.tsx | 13 +- public/embeddable/map_component.tsx | 2 +- public/model/layerRenderController.ts | 7 +- public/model/layersFunctions.ts | 21 ++++ 9 files changed, 146 insertions(+), 132 deletions(-) diff --git a/public/components/layer_config/documents_config/document_layer_config_panel.tsx b/public/components/layer_config/documents_config/document_layer_config_panel.tsx index 2a99abac..febf8793 100644 --- a/public/components/layer_config/documents_config/document_layer_config_panel.tsx +++ b/public/components/layer_config/documents_config/document_layer_config_panel.tsx @@ -55,7 +55,7 @@ export const DocumentLayerConfigPanel = (props: Props) => { ), - testSubj: 'dataTab', + testsubj: 'dataTab', }, { id: 'style--id', @@ -66,7 +66,7 @@ export const DocumentLayerConfigPanel = (props: Props) => { ), - testSubj: 'styleTab', + testsubj: 'styleTab', }, { id: 'settings--id', @@ -77,7 +77,7 @@ export const DocumentLayerConfigPanel = (props: Props) => { ), - testSubj: 'settingsTab', + testsubj: 'settingsTab', }, ]; return ; diff --git a/public/components/layer_config/layer_config_panel.tsx b/public/components/layer_config/layer_config_panel.tsx index 4dc9cc9f..52c8c146 100644 --- a/public/components/layer_config/layer_config_panel.tsx +++ b/public/components/layer_config/layer_config_panel.tsx @@ -42,6 +42,7 @@ interface Props { isLayerExists: Function; originLayerConfig: MapLayerSpecification | null; setOriginLayerConfig: Function; + setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; } export const LayerConfigPanel = ({ @@ -55,6 +56,7 @@ export const LayerConfigPanel = ({ isLayerExists, originLayerConfig, setOriginLayerConfig, + setIsUpdatingLayerRender, }: Props) => { const [isUpdateDisabled, setIsUpdateDisabled] = useState(false); const [unsavedModalVisible, setUnsavedModalVisible] = useState(false); @@ -84,9 +86,11 @@ export const LayerConfigPanel = ({ } }; const onUpdate = () => { + setIsUpdatingLayerRender(true); updateLayer(); closeLayerConfigPanel(false); setOriginLayerConfig(null); + setSelectedLayerConfig(undefined); if (isNewLayer) { setIsNewLayer(false); } diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 65b5761c..3a290c77 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -34,13 +34,8 @@ import { LAYER_PANEL_SHOW_LAYER_ICON, LAYER_VISIBILITY, } from '../../../common'; -import { referenceLayerTypeLookup } from '../../model/layersFunctions'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; -import { - handleDataLayerRender, - handleReferenceLayerRender, -} from '../../model/layerRenderController'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; import { moveLayers, removeLayers, updateLayerVisibility } from '../../model/map/layer_operations'; @@ -59,7 +54,10 @@ interface Props { mapState: MapState; zoom: number; mapConfig: ConfigSchema; - inDashboardMode: boolean; + isReadOnlyMode: boolean; + selectedLayerConfig: MapLayerSpecification | undefined; + setSelectedLayerConfig: (layerConfig: MapLayerSpecification | undefined) => void; + setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; } export const LayerControlPanel = memo( @@ -67,26 +65,17 @@ export const LayerControlPanel = memo( maplibreRef, setLayers, layers, - layersIndexPatterns, - setLayersIndexPatterns, - mapState, zoom, mapConfig, - inDashboardMode, + isReadOnlyMode, + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdatingLayerRender, }: Props) => { const { services } = useOpenSearchDashboards(); - const { - data: { indexPatterns }, - notifications, - } = services; const [isLayerConfigVisible, setIsLayerConfigVisible] = useState(false); const [isLayerControlVisible, setIsLayerControlVisible] = useState(true); - const [selectedLayerConfig, setSelectedLayerConfig] = useState< - MapLayerSpecification | undefined - >(); - const [initialLayersLoaded, setInitialLayersLoaded] = useState(false); - const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(false); const [isNewLayer, setIsNewLayer] = useState(false); const [isDeleteLayerModalVisible, setIsDeleteLayerModalVisible] = useState(false); const [originLayerConfig, setOriginLayerConfig] = useState(null); @@ -95,39 +84,6 @@ export const LayerControlPanel = memo( >(); const [visibleLayers, setVisibleLayers] = useState([]); - useEffect(() => { - if (!isUpdatingLayerRender && initialLayersLoaded) { - return; - } - if (layers.length <= 0) { - return; - } - - if (initialLayersLoaded) { - if (!selectedLayerConfig) { - return; - } - if (referenceLayerTypeLookup[selectedLayerConfig.type]) { - handleReferenceLayerRender(selectedLayerConfig, maplibreRef, undefined); - } else { - updateIndexPatterns(); - handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef, undefined); - } - setSelectedLayerConfig(undefined); - } else { - layers.forEach((layer) => { - const beforeLayerId = getMapBeforeLayerId(layer); - if (referenceLayerTypeLookup[layer.type]) { - handleReferenceLayerRender(layer, maplibreRef, beforeLayerId); - } else { - handleDataLayerRender(layer, mapState, services, maplibreRef, beforeLayerId); - } - }); - setInitialLayersLoaded(true); - } - setIsUpdatingLayerRender(false); - }, [layers]); - useEffect(() => { const getCurrentVisibleLayers = () => { return layers.filter( @@ -138,16 +94,6 @@ export const LayerControlPanel = memo( setVisibleLayers(getCurrentVisibleLayers()); }, [layers, zoom]); - // Get layer id from layers that is above the selected layer - function getMapBeforeLayerId(selectedLayer: MapLayerSpecification): string | undefined { - const selectedLayerIndex = layers.findIndex((layer) => layer.id === selectedLayer.id); - const beforeLayers = layers.slice(selectedLayerIndex + 1); - if (beforeLayers.length === 0) { - return undefined; - } - return beforeLayers[0]?.id; - } - const closeLayerConfigPanel = () => { setIsLayerConfigVisible(false); setTimeout(() => { @@ -178,7 +124,6 @@ export const LayerControlPanel = memo( }; } setLayers(layersClone); - setIsUpdatingLayerRender(true); }; const removeLayer = (layerId: string) => { @@ -199,7 +144,7 @@ export const LayerControlPanel = memo( const onClickLayerName = (layer: MapLayerSpecification) => { if (hasUnsavedChanges()) { - notifications.toasts.addWarning( + services.toastNotifications.addWarning( `You have unsaved changes for ${selectedLayerConfig?.name}` ); } else { @@ -261,25 +206,6 @@ export const LayerControlPanel = memo( return layersClone.reverse(); }; - const updateIndexPatterns = async () => { - if (!selectedLayerConfig) { - return; - } - if (referenceLayerTypeLookup[selectedLayerConfig.type]) { - return; - } - const findIndexPattern = layersIndexPatterns.find( - // @ts-ignore - (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId - ); - if (!findIndexPattern) { - // @ts-ignore - const newIndexPattern = await indexPatterns.get(selectedLayerConfig.source.indexPatternId); - const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; - setLayersIndexPatterns(cloneLayersIndexPatterns); - } - }; - const onLayerVisibilityChange = (layer: MapLayerSpecification) => { if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { layer.visibility = LAYER_VISIBILITY.NONE; @@ -322,7 +248,7 @@ export const LayerControlPanel = memo( return visibleLayers.includes(layer); }; - if (inDashboardMode) { + if (isReadOnlyMode) { return null; } @@ -480,6 +406,7 @@ export const LayerControlPanel = memo( isLayerExists={isLayerExists} originLayerConfig={originLayerConfig} setOriginLayerConfig={setOriginLayerConfig} + setIsUpdatingLayerRender={setIsUpdatingLayerRender} /> )} void; @@ -35,11 +43,13 @@ interface MapContainerProps { maplibreRef: React.MutableRefObject; mapState: MapState; mapConfig: ConfigSchema; - inDashboardMode: boolean; + isReadOnlyMode: boolean; timeRange?: TimeRange; refreshConfig?: RefreshInterval; filters?: Filter[]; query?: Query; + isUpdatingLayerRender: boolean; + setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; } export const MapContainer = ({ @@ -50,17 +60,22 @@ export const MapContainer = ({ maplibreRef, mapState, mapConfig, - inDashboardMode, + isReadOnlyMode, timeRange, refreshConfig, filters, query, + isUpdatingLayerRender, + setIsUpdatingLayerRender, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); const mapContainer = useRef(null); const [mounted, setMounted] = useState(false); const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); const [coordinates, setCoordinates] = useState(); + const [selectedLayerConfig, setSelectedLayerConfig] = useState< + MapLayerSpecification | undefined + >(); useEffect(() => { if (!mapContainer.current) return; @@ -93,7 +108,7 @@ export const MapContainer = ({ let resizeChecker: ResizeChecker; if (mapContainerElement) { resizeChecker = new ResizeChecker(mapContainerElement); - if (inDashboardMode) { + if (isReadOnlyMode) { resizeChecker.on( 'resize', throttle(() => { @@ -206,25 +221,6 @@ export const MapContainer = ({ }; }, [layers, mapState, services]); - // Update data layers when state bar time range, filters and query changes - useEffect(() => { - layers.forEach((layer: MapLayerSpecification) => { - if (referenceLayerTypeLookup[layer.type]) { - return; - } - handleDataLayerRender( - layer, - mapState, - services, - maplibreRef, - undefined, - timeRange, - filters, - query - ); - }); - }, [timeRange, mapState, filters]); - // Update data layers when state bar enable auto refresh useEffect(() => { let intervalId: NodeJS.Timeout | undefined; @@ -241,6 +237,65 @@ export const MapContainer = ({ return () => clearInterval(intervalId); }, [refreshConfig]); + useEffect(() => { + if (!mounted) { + return; + } + + if (layers.length <= 0) { + return; + } + + if (isUpdatingLayerRender || isReadOnlyMode) { + if (selectedLayerConfig) { + if (referenceLayerTypeLookup[selectedLayerConfig.type]) { + handleReferenceLayerRender(selectedLayerConfig, maplibreRef, undefined); + } else { + updateIndexPatterns(); + handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef, undefined); + } + } else { + getDataLayers(layers).forEach((layer: MapLayerSpecification) => { + const beforeLayerId = getMapBeforeLayerId(layers, layer.id); + handleDataLayerRender( + layer, + mapState, + services, + maplibreRef, + beforeLayerId, + timeRange, + filters, + query + ); + }); + getReferenceLayers(layers).forEach((layer: MapLayerSpecification) => { + const beforeLayerId = getMapBeforeLayerId(layers, layer.id); + handleReferenceLayerRender(layer, maplibreRef, beforeLayerId); + }); + } + setIsUpdatingLayerRender(false); + } + }, [layers, mounted, timeRange, filters, query, mapState, isReadOnlyMode]); + + const updateIndexPatterns = async () => { + if (!selectedLayerConfig) { + return; + } + if (referenceLayerTypeLookup[selectedLayerConfig.type]) { + return; + } + const findIndexPattern = layersIndexPatterns.find( + // @ts-ignore + (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId + ); + if (!findIndexPattern) { + // @ts-ignore + const newIndexPattern = await indexPatterns.get(selectedLayerConfig.source.indexPatternId); + const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; + setLayersIndexPatterns(cloneLayersIndexPatterns); + } + }; + return (
)}
diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index 0151d9a0..f6726825 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -37,7 +37,7 @@ interface MapComponentProps { mapConfig: ConfigSchema; mapIdFromSavedObject: string; timeRange?: TimeRange; - inDashboardMode: boolean; + isReadOnlyMode: boolean; refreshConfig?: RefreshInterval; filters?: Filter[]; query?: Query; @@ -46,7 +46,7 @@ export const MapComponent = ({ mapIdFromSavedObject, mapConfig, timeRange, - inDashboardMode, + isReadOnlyMode, refreshConfig, filters, query, @@ -61,6 +61,7 @@ export const MapComponent = ({ const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); const maplibreRef = useRef(null); const [mapState, setMapState] = useState(getInitialMapState()); + const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(true); useEffect(() => { if (mapIdFromSavedObject) { @@ -91,10 +92,10 @@ export const MapComponent = ({ return (
- {inDashboardMode ? null : ( + {isReadOnlyMode ? null : ( )} @@ -113,11 +115,13 @@ export const MapComponent = ({ maplibreRef={maplibreRef} mapState={mapState} mapConfig={mapConfig} - inDashboardMode={inDashboardMode} + isReadOnlyMode={isReadOnlyMode} timeRange={timeRange} refreshConfig={refreshConfig} filters={filters} query={query} + isUpdatingLayerRender={isUpdatingLayerRender} + setIsUpdatingLayerRender={setIsUpdatingLayerRender} />
); @@ -125,7 +129,5 @@ export const MapComponent = ({ export const MapPage = ({ mapConfig }: MapPageProps) => { const { id: mapId } = useParams<{ id: string }>(); - return ( - - ); + return ; }; diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index b5b9a184..7d688b51 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -24,21 +24,23 @@ interface MapTopNavMenuProps { maplibreRef: any; mapState: MapState; setMapState: (mapState: MapState) => void; - inDashboardMode: boolean; + isReadOnlyMode: boolean; timeRange?: TimeRange; originatingApp?: string; + setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; } export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, - inDashboardMode, + isReadOnlyMode, timeRange, layers, layersIndexPatterns, maplibreRef, mapState, setMapState, + setIsUpdatingLayerRender, }: MapTopNavMenuProps) => { const { services } = useOpenSearchDashboards(); const { @@ -96,6 +98,7 @@ export const MapTopNavMenu = ({ }; const handleQuerySubmit = ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => { + setIsUpdatingLayerRender(true); if (query) { setMapState({ ...mapState, query }); } @@ -105,7 +108,7 @@ export const MapTopNavMenu = ({ }; useEffect(() => { - if (!inDashboardMode) { + if (!isReadOnlyMode) { setDateFrom(mapState.timeRange.from); setDateTo(mapState.timeRange.to); } else { @@ -144,9 +147,9 @@ export const MapTopNavMenu = ({ config={config} setMenuMountPoint={setHeaderActionMenu} indexPatterns={layersIndexPatterns || []} - showSearchBar={!inDashboardMode} + showSearchBar={!isReadOnlyMode} showFilterBar={false} - showDatePicker={!inDashboardMode} + showDatePicker={!isReadOnlyMode} showQueryBar={true} showSaveQuery={true} showQueryInput={true} diff --git a/public/embeddable/map_component.tsx b/public/embeddable/map_component.tsx index a2fb58f4..afb420f2 100644 --- a/public/embeddable/map_component.tsx +++ b/public/embeddable/map_component.tsx @@ -41,7 +41,7 @@ export function MapEmbeddableComponentInner({ embeddable, input }: Props) { mapConfig={embeddable.getMapConfig()} mapIdFromSavedObject={input.savedObjectId} timeRange={timeRange} - inDashboardMode={true} + isReadOnlyMode={true} refreshConfig={refreshConfig} filters={filters} query={query} diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index cf160535..e4b276a2 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -4,7 +4,7 @@ */ import { Map as Maplibre } from 'maplibre-gl'; -import { DocumentLayerSpecification, MapLayerSpecification } from './mapLayerType'; +import { MapLayerSpecification } from './mapLayerType'; import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; import { buildOpenSearchQuery, @@ -92,7 +92,7 @@ export const prepareDataLayerSource = ( }; export const handleDataLayerRender = ( - mapLayer: DocumentLayerSpecification, + mapLayer: MapLayerSpecification, mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, @@ -101,6 +101,9 @@ export const handleDataLayerRender = ( filtersFromDashboard?: Filter[], query?: Query ) => { + if (mapLayer.type !== DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + return; + } // filters are passed from dashboard filters and geo bounding box filters const filters: Filter[] = []; filters.push(...(filtersFromDashboard ? filtersFromDashboard : [])); diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts index 71625459..52448abc 100644 --- a/public/model/layersFunctions.ts +++ b/public/model/layersFunctions.ts @@ -61,3 +61,24 @@ export const referenceLayerTypeLookup = { [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: true, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: false, }; + +export const getDataLayers = (layers: MapLayerSpecification[]) => { + return layers.filter((layer) => !referenceLayerTypeLookup[layer.type]); +}; + +export const getReferenceLayers = (layers: MapLayerSpecification[]) => { + return layers.filter((layer) => referenceLayerTypeLookup[layer.type]); +}; + +// Get layer id from layers that is above the selected layer +export const getMapBeforeLayerId = ( + layers: MapLayerSpecification[], + selectedLayerId: string +): string | undefined => { + const selectedLayerIndex = layers.findIndex((layer) => layer.id === selectedLayerId); + const beforeLayers = layers.slice(selectedLayerIndex + 1); + if (beforeLayers.length === 0) { + return undefined; + } + return beforeLayers[0]?.id; +}; From 8697636bc6753eb7c40c1dbc169ff02fc37e3980 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 22 Feb 2023 08:11:44 -0800 Subject: [PATCH 19/77] Add indexPatterns to map embeddable output for dashboard filters (#272) Signed-off-by: Junqiu Lei --- public/embeddable/map_embeddable.tsx | 29 ++++++++++++++++---- public/embeddable/map_embeddable_factory.tsx | 22 ++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/public/embeddable/map_embeddable.tsx b/public/embeddable/map_embeddable.tsx index 72e03d0b..6d593520 100644 --- a/public/embeddable/map_embeddable.tsx +++ b/public/embeddable/map_embeddable.tsx @@ -16,7 +16,7 @@ import { import { MapEmbeddableComponent } from './map_component'; import { ConfigSchema } from '../../common/config'; import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; -import { RefreshInterval } from '../../../../src/plugins/data/public'; +import { IndexPattern, RefreshInterval } from '../../../../src/plugins/data/public'; export const MAP_EMBEDDABLE = MAP_SAVED_OBJECT_TYPE; @@ -25,15 +25,28 @@ export interface MapInput extends EmbeddableInput { refreshConfig?: RefreshInterval; } -export type MapOutput = EmbeddableOutput; +export interface MapOutput extends EmbeddableOutput { + editable: boolean; + editUrl: string; + defaultTitle: string; + editApp: string; + editPath: string; + indexPatterns: IndexPattern[]; +} -function getOutput(input: MapInput, editUrl: string, tittle: string): MapOutput { +function getOutput( + input: MapInput, + editUrl: string, + tittle: string, + indexPatterns: IndexPattern[] +): MapOutput { return { editable: true, editUrl, defaultTitle: tittle, editApp: MAPS_APP_ID, editPath: input.savedObjectId, + indexPatterns, }; } @@ -51,19 +64,25 @@ export class MapEmbeddable extends Embeddable { mapConfig, editUrl, savedMapAttributes, + indexPatterns, }: { parent?: IContainer; services: any; mapConfig: ConfigSchema; editUrl: string; savedMapAttributes: MapSavedObjectAttributes; + indexPatterns: IndexPattern[]; } ) { - super(initialInput, getOutput(initialInput, editUrl, savedMapAttributes.title), parent); + super( + initialInput, + getOutput(initialInput, editUrl, savedMapAttributes.title, indexPatterns), + parent + ); this.mapConfig = mapConfig; this.services = services; this.subscription = this.getInput$().subscribe(() => { - this.updateOutput(getOutput(this.input, editUrl, savedMapAttributes.title)); + this.updateOutput(getOutput(this.input, editUrl, savedMapAttributes.title, indexPatterns)); }); } diff --git a/public/embeddable/map_embeddable_factory.tsx b/public/embeddable/map_embeddable_factory.tsx index b1534687..ce5ccc78 100644 --- a/public/embeddable/map_embeddable_factory.tsx +++ b/public/embeddable/map_embeddable_factory.tsx @@ -11,11 +11,12 @@ import { SavedObjectEmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { MAP_EMBEDDABLE, MapInput, MapOutput, MapEmbeddable } from './map_embeddable'; -import { MAPS_APP_ICON, MAPS_APP_ID } from '../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, MAPS_APP_ICON, MAPS_APP_ID } from '../../common'; import { ConfigSchema } from '../../common/config'; import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; import { MAPS_APP_DISPLAY_NAME } from '../../common/constants/shared'; -import { getTimeFilter } from '../services'; +import { MapLayerSpecification } from '../model/mapLayerType'; +import { IndexPattern } from '../../../../src/plugins/data/common'; interface StartServices { services: { @@ -28,6 +29,11 @@ interface StartServices { get: (type: string, id: string) => Promise; }; }; + data: { + indexPatterns: { + get: (id: string) => Promise; + }; + }; }; mapConfig: ConfigSchema; } @@ -64,9 +70,17 @@ export class MapEmbeddableFactoryDefinition const url = services.application.getUrlForApp(MAPS_APP_ID, { path: savedObjectId, }); - const timeFilter = getTimeFilter(); const savedMap = await services.savedObjects.client.get(MAP_EMBEDDABLE, savedObjectId); const savedMapAttributes = savedMap.attributes as MapSavedObjectAttributes; + const layerList: MapLayerSpecification[] = JSON.parse(savedMapAttributes.layerList as string); + const indexPatterns: IndexPattern[] = []; + for (const layer of layerList) { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const indexPatternId = layer.source.indexPatternId; + const indexPattern = await services.data.indexPatterns.get(indexPatternId); + indexPatterns.push(indexPattern); + } + } return new MapEmbeddable( { ...input, @@ -74,12 +88,12 @@ export class MapEmbeddableFactoryDefinition title: savedMapAttributes.title, }, { + indexPatterns, parent, services, mapConfig, editUrl: url, savedMapAttributes, - timeFilter, } ); } catch (error) { From 0636c12a9a0ca9e20f71abe776f212b20098b59d Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 22 Feb 2023 08:20:46 -0800 Subject: [PATCH 20/77] Refactor hide layer button (#252) Signed-off-by: Junqiu Lei --- .../hide_layer_button.test.tsx | 85 +++++++++++++++++++ .../layer_control_panel/hide_layer_button.tsx | 59 +++++++++++++ .../layer_control_panel.tsx | 59 ++++--------- public/model/documentLayerFunctions.ts | 2 - public/model/map/layer_operations.test.ts | 38 ++++----- public/model/map/layer_operations.ts | 6 +- 6 files changed, 185 insertions(+), 64 deletions(-) create mode 100644 public/components/layer_control_panel/hide_layer_button.test.tsx create mode 100644 public/components/layer_control_panel/hide_layer_button.tsx diff --git a/public/components/layer_control_panel/hide_layer_button.test.tsx b/public/components/layer_control_panel/hide_layer_button.test.tsx new file mode 100644 index 00000000..38c31f79 --- /dev/null +++ b/public/components/layer_control_panel/hide_layer_button.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const mockStyle = { + layers: [ + { + id: 'layer-1', + type: 'fill', + source: 'layer-1', + }, + ], +}; +// Need put this mock before import HideLayerButton, or it will not work +jest.mock('maplibre-gl', () => ({ + Map: jest.fn(() => ({ + on: jest.fn(), + off: jest.fn(), + getStyle: jest.fn(() => mockStyle), + setLayoutProperty: jest.fn(), + })), +})); + +import { EuiButtonIcon } from '@elastic/eui'; +import React from 'react'; +import { LAYER_VISIBILITY } from '../../../common'; +import { HideLayer } from './hide_layer_button'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import TestRenderer from 'react-test-renderer'; +import { Map as MapLibre } from 'maplibre-gl'; + +describe('HideLayerButton', () => { + it('should toggle layer visibility on button click', () => { + const exampleLayer: MapLayerSpecification = { + name: 'Layer 1', + id: 'layer-1', + type: 'opensearch_vector_tile_map', + description: 'Some description', + source: { + dataURL: 'https:foo.bar', + }, + style: { + styleURL: 'https://example.com/style.json', + }, + zoomRange: [0, 22], + visibility: LAYER_VISIBILITY.VISIBLE, + opacity: 100, + }; + + const map = new MapLibre({ + container: document.createElement('div'), + style: { + layers: [], + version: 8 as 8, + sources: {}, + }, + }); + + const maplibreRef = { + current: map, + }; + + const updateLayerVisibility = jest.fn(); + + const hideButton = TestRenderer.create( + + ); + + const button = hideButton.root.findByType(EuiButtonIcon); + + expect(button.props.title).toBe('Hide layer'); + expect(button.props.iconType).toBe('eyeClosed'); + + button.props.onClick(); + + expect(button.props.title).toBe('Show layer'); + expect(button.props.iconType).toBe('eye'); + expect(updateLayerVisibility).toBeCalledTimes(1); + }); +}); diff --git a/public/components/layer_control_panel/hide_layer_button.tsx b/public/components/layer_control_panel/hide_layer_button.tsx new file mode 100644 index 00000000..913f8b8f --- /dev/null +++ b/public/components/layer_control_panel/hide_layer_button.tsx @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; +import React, { useState } from 'react'; +import { Map as Maplibre } from 'maplibre-gl'; +import { + LAYER_PANEL_HIDE_LAYER_ICON, + LAYER_PANEL_SHOW_LAYER_ICON, + LAYER_VISIBILITY, +} from '../../../common'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { updateLayerVisibilityHandler } from '../../model/map/layer_operations'; + +interface MaplibreRef { + current: Maplibre | null; +} +interface HideLayerProps { + layer: MapLayerSpecification; + maplibreRef: MaplibreRef; + updateLayerVisibility: (layerId: string, visibility: string) => void; +} + +export const HideLayer = ({ layer, maplibreRef, updateLayerVisibility }: HideLayerProps) => { + const [layerVisibility, setLayerVisibility] = useState( + new Map([[layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE]]) + ); + + const onLayerVisibilityChange = () => { + let updatedVisibility: string; + if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { + updatedVisibility = LAYER_VISIBILITY.NONE; + } else { + updatedVisibility = LAYER_VISIBILITY.VISIBLE; + } + setLayerVisibility( + new Map(layerVisibility.set(layer.id, updatedVisibility === LAYER_VISIBILITY.VISIBLE)) + ); + updateLayerVisibility(layer.id, updatedVisibility); + updateLayerVisibilityHandler(maplibreRef.current!, layer.id, updatedVisibility); + }; + + return ( + + + + ); +}; diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 3a290c77..2e861eaa 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -28,18 +28,14 @@ import { IndexPattern } from '../../../../../src/plugins/data/public'; import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; import { MapLayerSpecification } from '../../model/mapLayerType'; -import { - LAYER_ICON_TYPE_MAP, - LAYER_PANEL_HIDE_LAYER_ICON, - LAYER_PANEL_SHOW_LAYER_ICON, - LAYER_VISIBILITY, -} from '../../../common'; +import { LAYER_ICON_TYPE_MAP } from '../../../common'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; -import { moveLayers, removeLayers, updateLayerVisibility } from '../../model/map/layer_operations'; +import { moveLayers, removeLayers } from '../../model/map/layer_operations'; import { DeleteLayerModal } from './delete_layer_modal'; +import { HideLayer } from './hide_layer_button'; interface MaplibreRef { current: Maplibre | null; @@ -126,6 +122,15 @@ export const LayerControlPanel = memo( setLayers(layersClone); }; + const updateLayerVisibility = (layerId: string, visibility: string) => { + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === layerId); + if (index > -1) { + layersClone[index].visibility = String(visibility); + setLayers(layersClone); + } + }; + const removeLayer = (layerId: string) => { const layersClone = [...layers]; const index = layersClone.findIndex((layer) => layer.id === layerId); @@ -157,11 +162,6 @@ export const LayerControlPanel = memo( return layers.findIndex((layer) => layer.name === name) > -1; }; - const [layerVisibility, setLayerVisibility] = useState(new Map([])); - layers.forEach((layer) => { - layerVisibility.set(layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE); - }); - const beforeMaplibreLayerID = (source: number, destination: number) => { if (source > destination) { // if layer is moved below, move current layer below given destination @@ -206,17 +206,6 @@ export const LayerControlPanel = memo( return layersClone.reverse(); }; - const onLayerVisibilityChange = (layer: MapLayerSpecification) => { - if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { - layer.visibility = LAYER_VISIBILITY.NONE; - setLayerVisibility(new Map(layerVisibility.set(layer.id, false))); - } else { - layer.visibility = LAYER_VISIBILITY.VISIBLE; - setLayerVisibility(new Map(layerVisibility.set(layer.id, true))); - } - updateLayerVisibility(maplibreRef.current!, layer.id, layer.visibility); - }; - const onDeleteLayerIconClick = (layer: MapLayerSpecification) => { setSelectedDeleteLayer(layer); setIsDeleteLayerModalVisible(true); @@ -338,25 +327,11 @@ export const LayerControlPanel = memo( - - onLayerVisibilityChange(layer)} - aria-label="Hide or show layer" - color="text" - title={ - layerVisibility.get(layer.id) ? 'Hide layer' : 'Show layer' - } - /> - + { const expectedLayerId: string = sourceId + '-circle'; expect(mockMap.getLayer(expectedLayerId).length).toBe(0); expect( - addCircleLayer((mockMap as unknown) as Maplibre, { + addCircleLayer(mockMap as unknown as Maplibre, { maxZoom: 10, minZoom: 2, opacity: 60, @@ -61,7 +61,7 @@ describe('Circle layer', () => { const sourceId: string = 'geojson-source'; // add layer first - const addedLayerId: string = addCircleLayer((mockMap as unknown) as Maplibre, { + const addedLayerId: string = addCircleLayer(mockMap as unknown as Maplibre, { maxZoom: 10, minZoom: 2, opacity: 60, @@ -73,7 +73,7 @@ describe('Circle layer', () => { fillColor: 'red', }); expect( - updateCircleLayer((mockMap as unknown) as Maplibre, { + updateCircleLayer(mockMap as unknown as Maplibre, { maxZoom: 12, minZoom: 4, opacity: 80, @@ -110,7 +110,7 @@ describe('Line layer', () => { const expectedLayerId: string = sourceId + '-line'; expect(mockMap.getLayer(expectedLayerId).length).toBe(0); expect( - addLineLayer((mockMap as unknown) as Maplibre, { + addLineLayer(mockMap as unknown as Maplibre, { color: 'red', maxZoom: 10, minZoom: 2, @@ -139,7 +139,7 @@ describe('Line layer', () => { const sourceId: string = 'geojson-source'; // add layer first - const addedLineLayerId: string = addLineLayer((mockMap as unknown) as Maplibre, { + const addedLineLayerId: string = addLineLayer(mockMap as unknown as Maplibre, { color: 'red', maxZoom: 10, minZoom: 2, @@ -149,7 +149,7 @@ describe('Line layer', () => { width: 2, }); expect( - updateLineLayer((mockMap as unknown) as Maplibre, { + updateLineLayer(mockMap as unknown as Maplibre, { color: 'blue', maxZoom: 12, minZoom: 4, @@ -183,7 +183,7 @@ describe('Polygon layer', () => { const expectedOutlineLayerId = expectedFillLayerId + '-outline'; expect(mockMap.getLayer(expectedFillLayerId).length).toBe(0); expect(mockMap.getLayer(expectedOutlineLayerId).length).toBe(0); - addPolygonLayer((mockMap as unknown) as Maplibre, { + addPolygonLayer(mockMap as unknown as Maplibre, { maxZoom: 10, minZoom: 2, opacity: 60, @@ -230,7 +230,7 @@ describe('Polygon layer', () => { const expectedFillLayerId = sourceId + '-fill'; const expectedOutlineLayerId = expectedFillLayerId + '-outline'; // add layer first - addPolygonLayer((mockMap as unknown) as Maplibre, { + addPolygonLayer(mockMap as unknown as Maplibre, { maxZoom: 10, minZoom: 2, opacity: 60, @@ -243,7 +243,7 @@ describe('Polygon layer', () => { expect(mockMap.getLayer(sourceId).length).toBe(2); // update polygon for test - updatePolygonLayer((mockMap as unknown) as Maplibre, { + updatePolygonLayer(mockMap as unknown as Maplibre, { maxZoom: 12, minZoom: 4, opacity: 80, @@ -288,7 +288,7 @@ describe('get layer', () => { it('should get layer successfully', function () { const mockLayer: MockLayer = new MockLayer('layer-1'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); - const actualLayers = getLayers((mockMap as unknown) as Maplibre, 'layer-1'); + const actualLayers = getLayers(mockMap as unknown as Maplibre, 'layer-1'); expect(actualLayers.length).toBe(1); expect(actualLayers[0].id).toBe(mockLayer.getProperty('id')); }); @@ -296,13 +296,13 @@ describe('get layer', () => { it('should confirm no layer exists', function () { const mockLayer: MockLayer = new MockLayer('layer-1'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); - expect(hasLayer((mockMap as unknown) as Maplibre, 'layer-2')).toBe(false); + expect(hasLayer(mockMap as unknown as Maplibre, 'layer-2')).toBe(false); }); it('should confirm layer exists', function () { const mockLayer: MockLayer = new MockLayer('layer-1'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer]); - expect(hasLayer((mockMap as unknown) as Maplibre, 'layer-1')).toBe(true); + expect(hasLayer(mockMap as unknown as Maplibre, 'layer-1')).toBe(true); }); }); @@ -312,7 +312,7 @@ describe('move layer', () => { const mockLayer2: MockLayer = new MockLayer('layer-11'); const mockLayer3: MockLayer = new MockLayer('layer-2'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); - moveLayers((mockMap as unknown) as Maplibre, 'layer-1'); + moveLayers(mockMap as unknown as Maplibre, 'layer-1'); const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); expect(reorderedLayer).toEqual(['layer-2', 'layer-1', 'layer-11']); }); @@ -321,7 +321,7 @@ describe('move layer', () => { const mockLayer2: MockLayer = new MockLayer('layer-2'); const mockLayer3: MockLayer = new MockLayer('layer-3'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); - moveLayers((mockMap as unknown) as Maplibre, 'layer-1', 'layer-2'); + moveLayers(mockMap as unknown as Maplibre, 'layer-1', 'layer-2'); const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); expect(reorderedLayer).toEqual(['layer-2', 'layer-1', 'layer-3']); }); @@ -330,7 +330,7 @@ describe('move layer', () => { const mockLayer2: MockLayer = new MockLayer('layer-2'); const mockLayer3: MockLayer = new MockLayer('layer-3'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); - moveLayers((mockMap as unknown) as Maplibre, 'layer-4', 'layer-2'); + moveLayers(mockMap as unknown as Maplibre, 'layer-4', 'layer-2'); const reorderedLayer: string[] = mockMap.getLayers().map((layer) => layer.getProperty('id')); expect(reorderedLayer).toEqual(['layer-1', 'layer-2', 'layer-3']); }); @@ -343,7 +343,7 @@ describe('delete layer', function () { const mockLayer3: MockLayer = new MockLayer('layer-2'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); mockMap.addSource('layer-1', 'geojson'); - removeLayers((mockMap as unknown) as Maplibre, 'layer-1'); + removeLayers(mockMap as unknown as Maplibre, 'layer-1'); expect(mockMap.getLayers().length).toBe(1); expect(mockMap.getSource('layer-1')).toBeDefined(); expect(mockMap.getLayers()[0].getProperty('id')).toBe('layer-2'); @@ -354,7 +354,7 @@ describe('delete layer', function () { const mockLayer3: MockLayer = new MockLayer('layer-2'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2, mockLayer3]); mockMap.addSource('layer-2', 'geojson'); - removeLayers((mockMap as unknown) as Maplibre, 'layer-2', true); + removeLayers(mockMap as unknown as Maplibre, 'layer-2', true); expect(mockMap.getLayers().length).toBe(2); expect(mockMap.getSource('layer-2')).toBeUndefined(); expect( @@ -369,7 +369,7 @@ describe('update visibility', function () { const mockLayer2: MockLayer = new MockLayer('layer-11'); mockLayer1.setProperty('visibility', 'none'); const mockMap: MockMaplibreMap = new MockMaplibreMap([mockLayer1, mockLayer2]); - updateLayerVisibility((mockMap as unknown) as Maplibre, 'layer-1', 'visible'); + updateLayerVisibilityHandler(mockMap as unknown as Maplibre, 'layer-1', 'visible'); expect(mockMap.getLayers().map((layer) => String(layer.getProperty('visibility')))).toEqual( Array(2).fill('visible') ); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 06241b2f..7061b1f8 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -42,7 +42,11 @@ export const removeLayers = (map: Maplibre, layerId: string, removeSource?: bool } }; -export const updateLayerVisibility = (map: Maplibre, layerId: string, visibility: string) => { +export const updateLayerVisibilityHandler = ( + map: Maplibre, + layerId: string, + visibility: string +) => { getLayers(map, layerId).forEach((layer) => { map.setLayoutProperty(layer.id, 'visibility', visibility); }); From c3ebf3ab470403e79356ca3a5d741f66d6983d3e Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 22 Feb 2023 08:27:26 -0800 Subject: [PATCH 21/77] Fix Node.js and Yarn installation in CI (#277) Signed-off-by: Miki --- .github/workflows/cypress-workflow.yml | 25 +++++++++++------------ .github/workflows/unit-tests-workflow.yml | 25 +++++++++++------------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 17d02994..85f7a049 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -76,23 +76,22 @@ jobs: ref: ${{ env.OPENSEARCH_DASHBOARDS_BRANCH }} path: OpenSearch-Dashboards - - name: Get node and yarn versions - id: versions_step - run: | - echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" - echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" - - - name: Setup node - uses: actions/setup-node@v1 + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: ${{ steps.versions_step.outputs.node_version }} + node-version-file: './OpenSearch-Dashboards/.nvmrc' registry-url: 'https://registry.npmjs.org' - - name: Install correct yarn version for OpenSearch Dashboards + - name: Install Yarn + # Need to use bash to avoid having a windows/linux specific step + shell: bash run: | - npm uninstall -g yarn - echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" - npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} + YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn") + echo "Installing yarn@$YARN_VERSION" + npm i -g yarn@$YARN_VERSION + + - run: node -v + - run: yarn -v - name: Set npm to use bash for shell if: ${{ matrix.os == 'windows-latest' }} diff --git a/.github/workflows/unit-tests-workflow.yml b/.github/workflows/unit-tests-workflow.yml index 96342e28..3ad6e833 100644 --- a/.github/workflows/unit-tests-workflow.yml +++ b/.github/workflows/unit-tests-workflow.yml @@ -38,23 +38,22 @@ jobs: ref: ${{ env.OPENSEARCH_DASHBOARDS_BRANCH }} path: OpenSearch-Dashboards - - name: Get node and yarn versions - id: versions_step - run: | - echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" - echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" - - - name: Setup node - uses: actions/setup-node@v1 + - name: Setup Node + uses: actions/setup-node@v3 with: - node-version: ${{ steps.versions_step.outputs.node_version }} + node-version-file: './OpenSearch-Dashboards/.nvmrc' registry-url: 'https://registry.npmjs.org' - - name: Install correct yarn version for OpenSearch Dashboards + - name: Install Yarn + # Need to use bash to avoid having a windows/linux specific step + shell: bash run: | - npm uninstall -g yarn - echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" - npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} + YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn") + echo "Installing yarn@$YARN_VERSION" + npm i -g yarn@$YARN_VERSION + + - run: node -v + - run: yarn -v - name: Move plugin to OpenSearch-Dashboard Plugins Directory run: mv dashboards-maps OpenSearch-Dashboards/plugins/dashboards-maps From f88aa158e6bffb2978914f0b3e21a78cbc3f2132 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 22 Feb 2023 11:23:22 -0800 Subject: [PATCH 22/77] Fix property value undefined check (#276) If property value is false, property is not added. Replace check with undefined to accept false value. Signed-off-by: Vijayan Balasubramanian --- public/model/documentLayerFunctions.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index a5577aea..e218bd75 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -34,7 +34,7 @@ const openSearchGeoJSONMap = new Map([ const getFieldValue = (data: any, name: string) => { if (!name) { - return null; + return undefined; } const keys = name.split('.'); return keys.reduce((pre, cur) => { @@ -42,10 +42,6 @@ const getFieldValue = (data: any, name: string) => { }, data); }; -const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { - return maplibreRef.current?.getStyle().layers || []; -}; - const getGeoFieldType = (layerConfig: DocumentLayerSpecification) => { return layerConfig?.source?.geoFieldType; }; @@ -84,8 +80,8 @@ const buildProperties = (document: any, fields: string[]) => { return property; } fields.forEach((field) => { - const fieldValue = getFieldValue(document._source, field); - if (fieldValue) { + const fieldValue: string | undefined = getFieldValue(document._source, field); + if (fieldValue !== undefined) { property[field] = fieldValue; } }); From 491715da23d05f2ca4e078492b702ba930f7b1ce Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 22 Feb 2023 13:02:02 -0800 Subject: [PATCH 23/77] Add 2.6.0 release notes (#286) Signed-off-by: Vijayan Balasubramanian --- ...h-dashboards-maps.release-notes-2.6.0.0.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md new file mode 100644 index 00000000..081990dd --- /dev/null +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md @@ -0,0 +1,22 @@ +## Version 2.6.0.0 Release Notes +Compatible with OpenSearch and OpenSearch Dashboards Version 2.6.0 + +### Features +* Add map as embeddable to dashboard ([#231](https://github.com/opensearch-project/dashboards-maps/pull/231)) +* Add maps saved object for sample datasets ([#240](https://github.com/opensearch-project/dashboards-maps/pull/240)) + +### Infrastructure +* [Cypress fix] Wait map saved before open maps listing ([#218](https://github.com/opensearch-project/dashboards-maps/pull/218)) + +### Refactoring +* Refactor add layer operations ([#222](https://github.com/opensearch-project/dashboards-maps/pull/222)) +* Refactor layer operations ([#224](https://github.com/opensearch-project/dashboards-maps/pull/224)) +* Refactor layer properties as own interface ([#225](https://github.com/opensearch-project/dashboards-maps/pull/225)) + +### Enhancements +* Fix popup display while zoomed out ([#226](https://github.com/opensearch-project/dashboards-maps/pull/226)) +* Limit max number of layers ([#216](https://github.com/opensearch-project/dashboards-maps/pull/216)) +* Add close button to tooltip hover ([#263](https://github.com/opensearch-project/dashboards-maps/pull/263)) +* Add scroll bar when more layers added ([#254](https://github.com/opensearch-project/dashboards-maps/pull/254)) +* Align items in add new layer modal ([#256](https://github.com/opensearch-project/dashboards-maps/pull/256)) +* Add indexPatterns to map embeddable output for dashboard filters ([#272](https://github.com/opensearch-project/dashboards-maps/pull/272)) From a6b8912b4e039bab4a704bbf4a6a380d76d0ba2d Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Thu, 23 Feb 2023 12:54:13 -0800 Subject: [PATCH 24/77] Fix custom layer render opacity config (#289) * Fix custom layer render opacity config Signed-off-by: Junqiu Lei * update release note Signed-off-by: Junqiu Lei --------- Signed-off-by: Junqiu Lei --- public/model/customLayerFunctions.ts | 1 + .../opensearch-dashboards-maps.release-notes-2.6.0.0.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index 5da7fe92..e5a0100b 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -73,6 +73,7 @@ const addNewLayer = ( layerConfig.zoomRange[0], layerConfig.zoomRange[1] ); + maplibreInstance.setPaintProperty(layerConfig.id, 'raster-opacity', layerConfig.opacity / 100); } }; diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md index 081990dd..575d16ae 100644 --- a/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md @@ -20,3 +20,6 @@ Compatible with OpenSearch and OpenSearch Dashboards Version 2.6.0 * Add scroll bar when more layers added ([#254](https://github.com/opensearch-project/dashboards-maps/pull/254)) * Align items in add new layer modal ([#256](https://github.com/opensearch-project/dashboards-maps/pull/256)) * Add indexPatterns to map embeddable output for dashboard filters ([#272](https://github.com/opensearch-project/dashboards-maps/pull/272)) + +### Bug Fixes +* Fix custom layer render opacity config ([#289](https://github.com/opensearch-project/dashboards-maps/pull/289)) From 431b30508e8a197106f66add8c789adaf7ef5300 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 27 Feb 2023 13:39:36 -0800 Subject: [PATCH 25/77] Show scroll bar when panel height reach container bottom (#294) Signed-off-by: Junqiu Lei --- .gitignore | 4 ++++ .../layer_control_panel/layer_control_panel.scss | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a4b274f0..4be21c66 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ node_modules/ target/ build/ coverage/ +cypress/videos/ +cypress/screenshots/ +yarn-error.log +.DS_Store diff --git a/public/components/layer_control_panel/layer_control_panel.scss b/public/components/layer_control_panel/layer_control_panel.scss index a2e5fc42..06382b5a 100644 --- a/public/components/layer_control_panel/layer_control_panel.scss +++ b/public/components/layer_control_panel/layer_control_panel.scss @@ -1,3 +1,5 @@ +@import "../../variables"; + .layerControlPanel--show { pointer-events: auto; width: $euiSizeL * 11; @@ -26,7 +28,10 @@ .euiDroppable { overflow-y: auto; overflow-x: hidden; - max-height: $euiSizeL * 8; + } + + .euiFlexGroup--directionColumn { + max-height: calc(100vh - #{$mapHeaderOffset} - #{$euiSizeL}); } } From 70d5f9ee407b7590b4036c8eefbd22b96d35bba7 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 28 Feb 2023 13:06:12 -0800 Subject: [PATCH 26/77] Enhance layer visibility status display (#299) * Gray out layer name and icon on invisible layer * Add i18n Signed-off-by: Junqiu Lei --- .../layer_control_panel.tsx | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 2e861eaa..e759522e 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -24,6 +24,7 @@ import { I18nProvider } from '@osd/i18n/react'; import { Map as Maplibre } from 'maplibre-gl'; import './layer_control_panel.scss'; import { isEqual } from 'lodash'; +import { i18n } from '@osd/i18n'; import { IndexPattern } from '../../../../../src/plugins/data/public'; import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; @@ -226,14 +227,24 @@ export const LayerControlPanel = memo( }; const getLayerTooltipContent = (layer: MapLayerSpecification) => { + if (layer.visibility !== 'visible') { + return i18n.translate('maps.layerControl.layerIsHidden', { + defaultMessage: 'Layer is hidden', + }); + } + if (zoom < layer.zoomRange[0] || zoom > layer.zoomRange[1]) { - return `Layer is not visible outside of zoom range ${layer.zoomRange[0]} - ${layer.zoomRange[1]}`; - } else { - return `Layer is visible within zoom range ${layer.zoomRange[0]} - ${layer.zoomRange[1]}`; + return i18n.translate('maps.layerControl.layerNotVisibleZoom', { + defaultMessage: `Layer is hidden outside of zoom range ${layer.zoomRange[0]}–${layer.zoomRange[1]}`, + }); } + return ''; }; - const layerIsVisible = (layer: MapLayerSpecification) => { + const layerIsVisible = (layer: MapLayerSpecification): boolean => { + if (layer.visibility !== 'visible') { + return false; + } return visibleLayers.includes(layer); }; @@ -310,16 +321,11 @@ export const LayerControlPanel = memo( /> - + onClickLayerName(layer)} showToolTip={false} From 2c3bab85d50654fdceeff09eeb9efdc2b619140a Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 28 Feb 2023 15:06:57 -0800 Subject: [PATCH 27/77] Add custom layer visibility config to render (#297) Signed-off-by: Junqiu Lei --- public/model/customLayerFunctions.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index e5a0100b..45da80c1 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -65,15 +65,17 @@ const addNewLayer = ( id: layerConfig.id, type: 'raster', source: layerConfig.id, + paint: { + 'raster-opacity': layerConfig.opacity / 100, + }, + layout: { + visibility: layerConfig.visibility === 'visible' ? 'visible' : 'none', + }, + minzoom: layerConfig.zoomRange[0], + maxzoom: layerConfig.zoomRange[1], }, beforeMbLayerId ); - maplibreInstance.setLayerZoomRange( - layerConfig.id, - layerConfig.zoomRange[0], - layerConfig.zoomRange[1] - ); - maplibreInstance.setPaintProperty(layerConfig.id, 'raster-opacity', layerConfig.opacity / 100); } }; From ebf060771fc0a3eea6507b4134fd9e6818e95575 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 2 Mar 2023 00:42:39 +0800 Subject: [PATCH 28/77] fix: fixed filters not reset when index pattern changed (#234) + fixed the issue of filters selection not restored when switch among index patterns + fixed the issue of not all geo field selection are restored when switch among index patterns Signed-off-by: Yulong Ruan --- .../document_layer_source.tsx | 124 +++++++++++++----- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index 31c5b66a..bd24b35e 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useMemo, useCallback, useEffect, useRef, useState } from 'react'; import { EuiComboBox, EuiFlexItem, @@ -32,6 +32,15 @@ interface Props { setIsUpdateDisabled: Function; } +interface MemorizedForm { + [indexPatternId: string]: + | { + filters?: Filter[]; + geoField?: IndexPatternField; + } + | undefined; +} + export const DocumentLayerSource = ({ setSelectedLayerConfig, selectedLayerConfig, @@ -47,12 +56,60 @@ export const DocumentLayerSource = ({ }, } = useOpenSearchDashboards(); const [indexPattern, setIndexPattern] = useState(); - const [geoFields, setGeoFields] = useState(); - const [selectedField, setSelectedField] = useState(); const [hasInvalidRequestNumber, setHasInvalidRequestNumber] = useState(false); const [showTooltips, setShowTooltips] = useState( selectedLayerConfig.source.showTooltips ); + const memorizedForm = useRef({}); + + const geoFields = useMemo(() => { + const acceptedFieldTypes = ['geo_point', 'geo_shape']; + return indexPattern?.fields.filter((field) => acceptedFieldTypes.indexOf(field.type) !== -1); + }, [indexPattern]); + + const selectedField = useMemo(() => { + return geoFields?.find((field) => field.name === selectedLayerConfig.source.geoFieldName); + }, [geoFields, selectedLayerConfig]); + + // We want to memorize the filters and geoField selection when a map layer config is opened + useEffect(() => { + if ( + indexPattern && + indexPattern.id && + indexPattern.id === selectedLayerConfig.source.indexPatternId + ) { + if (!memorizedForm.current[indexPattern.id]) { + memorizedForm.current[indexPattern.id] = { + filters: selectedLayerConfig.source.filters, + geoField: selectedField, + }; + } + } + }, [indexPattern, selectedLayerConfig, selectedField]); + + const onGeoFieldChange = useCallback( + (field: IndexPatternField | null) => { + if (field) { + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig.source, + geoFieldName: field.displayName, + geoFieldType: field.type, + }, + }); + // We'd like to memorize the geo field selection so that the selection + // can be restored when changing index pattern back and forth + if (indexPattern?.id) { + memorizedForm.current[indexPattern.id] = { + ...memorizedForm.current[indexPattern.id], + geoField: field, + }; + } + } + }, + [selectedLayerConfig, setSelectedLayerConfig, indexPattern] + ); const errorsMap = { datasource: ['Required'], @@ -135,8 +192,16 @@ export const DocumentLayerSource = ({ ...selectedLayerConfig, source: { ...selectedLayerConfig.source, filters }, }); + // We'd like to memorize the fields selection so that the selection + // can be restored when changing index pattern back and forth + if (indexPattern?.id) { + memorizedForm.current[indexPattern.id] = { + ...memorizedForm.current[indexPattern.id], + filters, + }; + } }, - [selectedLayerConfig] + [selectedLayerConfig, indexPattern] ); useEffect(() => { @@ -151,34 +216,31 @@ export const DocumentLayerSource = ({ selectIndexPattern(); }, [indexPatterns, selectedLayerConfig.source.indexPatternId]); - // Update the fields list every time the index pattern is modified. + // Handle the side effects of index pattern change useEffect(() => { - const acceptedFieldTypes = ['geo_point', 'geo_shape']; - const fields = indexPattern?.fields.filter( - (field) => acceptedFieldTypes.indexOf(field.type) !== -1 - ); - setGeoFields(fields); - fields?.filter((field) => field.displayName === selectedLayerConfig.source.geoFieldName); - const savedField = fields?.find( - (field) => field.name === selectedLayerConfig.source.geoFieldName - ); - setSelectedField(savedField); - }, [indexPattern]); + const source = { ...selectedLayerConfig.source }; + // when index pattern changed, reset filters and geo field + if (indexPattern && indexPattern.id !== selectedLayerConfig.source.indexPatternId) { + source.indexPatternId = indexPattern.id ?? ''; + source.indexPatternRefName = indexPattern.title; + // Use memorized filters, otherwise, set filter selection to empty + const filters = indexPattern.id ? memorizedForm.current[indexPattern.id]?.filters ?? [] : []; + source.filters = filters; - useEffect(() => { - const setLayerSource = () => { - if (!indexPattern || !selectedField) return; - const source = { - ...selectedLayerConfig.source, - indexPatternRefName: indexPattern?.title, - indexPatternId: indexPattern?.id, - geoFieldName: selectedField?.displayName, - geoFieldType: selectedField?.type, - }; - setSelectedLayerConfig({ ...selectedLayerConfig, source }); - }; - setLayerSource(); - }, [selectedField]); + // Use memorized geo field, otherwise, set geo filter to empty + const geoField = indexPattern.id + ? memorizedForm.current[indexPattern.id]?.geoField + : undefined; + if (geoField) { + source.geoFieldName = geoField.displayName; + source.geoFieldType = geoField.type as 'geo_point' | 'geo_shape'; + } + setSelectedLayerConfig({ + ...selectedLayerConfig, + source, + }); + } + }, [indexPattern]); useEffect(() => { setHasInvalidRequestNumber( @@ -258,7 +320,7 @@ export const DocumentLayerSource = ({ singleSelection={true} onChange={(option) => { const field = indexPattern?.getFieldByName(option[0].label); - setSelectedField(field || null); + onGeoFieldChange(field || null); }} sortMatchesBy="startsWith" placeholder={i18n.translate('documentLayer.selectDataFieldPlaceholder', { From dd1b293093843d3ed3addd4a5c8067c1abdfa9c3 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 1 Mar 2023 14:58:30 -0800 Subject: [PATCH 29/77] Fix color picker component issue (#305) * Fix color picker component issue Signed-off-by: Junqiu Lei --- common/index.ts | 6 +- .../documents_config/document_layer_style.tsx | 148 +++++++++++------- public/utils/getIntialConfig.ts | 4 +- 3 files changed, 97 insertions(+), 61 deletions(-) diff --git a/common/index.ts b/common/index.ts index 7a1dff36..a5d03575 100644 --- a/common/index.ts +++ b/common/index.ts @@ -24,6 +24,11 @@ export { }; export const DOCUMENTS_DEFAULT_MARKER_SIZE = 5; +export const DOCUMENTS_MIN_MARKER_SIZE = 0; +export const DOCUMENTS_MAX_MARKER_SIZE = 100; +export const DOCUMENTS_DEFAULT_MARKER_BORDER_THICKNESS = 1; +export const DOCUMENTS_MIN_MARKER_BORDER_THICKNESS = 0; +export const DOCUMENTS_MAX_MARKER_BORDER_THICKNESS = 100; export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; @@ -32,7 +37,6 @@ export const LAYER_PANEL_SHOW_LAYER_ICON = 'eye'; export const MAP_DATA_LAYER_DEFAULT_OPACITY = 70; export const MAP_DEFAULT_MAX_ZOOM = 22; export const MAP_DEFAULT_MIN_ZOOM = 0; -export const MAP_LAYER_DEFAULT_BORDER_THICKNESS = 1; export const MAP_LAYER_DEFAULT_MAX_OPACITY = 100; export const MAP_LAYER_DEFAULT_MIN_OPACITY = 0; export const MAP_LAYER_DEFAULT_NAME = 'Default map'; diff --git a/public/components/layer_config/documents_config/document_layer_style.tsx b/public/components/layer_config/documents_config/document_layer_style.tsx index 0adc6ec5..6d6a0b62 100644 --- a/public/components/layer_config/documents_config/document_layer_style.tsx +++ b/public/components/layer_config/documents_config/document_layer_style.tsx @@ -3,10 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { EuiColorPicker, - useColorPickerState, EuiFieldNumber, EuiFormLabel, EuiFormErrorText, @@ -17,18 +16,49 @@ import { EuiTitle, EuiFormRow, EuiForm, + useColorPickerState, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { DocumentLayerSpecification } from '../../../model/mapLayerType'; +import { + DOCUMENTS_MAX_MARKER_BORDER_THICKNESS, + DOCUMENTS_MAX_MARKER_SIZE, + DOCUMENTS_MIN_MARKER_BORDER_THICKNESS, + DOCUMENTS_MIN_MARKER_SIZE, +} from '../../../../common'; interface Props { selectedLayerConfig: DocumentLayerSpecification; setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; +} + +interface ColorPickerProps { + color: string; + setColor: (text: string, { isValid }: { isValid: boolean }) => void; + label: string; + colorError: (text: string, { isValid }: { isValid: boolean }) => void; } -export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig }: Props) => { - const [fillColor, setFillColor] = useState(selectedLayerConfig?.style?.fillColor); - const [borderColor, setBorderColor] = useState(selectedLayerConfig?.style?.borderColor); +const ColorPicker = memo(({ color, setColor, label, colorError }: ColorPickerProps) => { + return ( + + + + ); +}); + +export const DocumentLayerStyle = ({ + setSelectedLayerConfig, + selectedLayerConfig, + setIsUpdateDisabled, +}: Props) => { + const [fillColor, setFillColor, fillColorErrors] = useColorPickerState( + selectedLayerConfig?.style?.fillColor + ); + const [borderColor, setBorderColor, borderColorErrors] = useColorPickerState( + selectedLayerConfig?.style?.borderColor + ); const [hasInvalidThickness, setHasInvalidThickness] = useState(false); const [hasInvalidSize, setHasInvalidSize] = useState(false); const geoTypeToggleButtonGroupPrefix = 'geoTypeToggleButtonGroup'; @@ -36,30 +66,34 @@ export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig `${geoTypeToggleButtonGroupPrefix}__Point` ); + // It's used to update the style color when switch layer config between different document layers useEffect(() => { - setFillColor(selectedLayerConfig?.style?.fillColor); - setBorderColor(selectedLayerConfig?.style?.borderColor); - }, [selectedLayerConfig]); + setFillColor( + selectedLayerConfig?.style?.fillColor, + !!fillColorErrors ? { isValid: false } : { isValid: true } + ); + setBorderColor( + selectedLayerConfig?.style?.borderColor, + !!borderColorErrors ? { isValid: false } : { isValid: true } + ); + }, [selectedLayerConfig.id]); useEffect(() => { - setSelectedLayerConfig({ - ...selectedLayerConfig, - style: { - ...selectedLayerConfig?.style, - fillColor, - }, - }); - }, [fillColor]); + const disableUpdate = + !!fillColorErrors || !!borderColorErrors || hasInvalidSize || hasInvalidThickness; + setIsUpdateDisabled(disableUpdate); + }, [fillColorErrors, borderColorErrors, hasInvalidSize, hasInvalidThickness]); useEffect(() => { setSelectedLayerConfig({ ...selectedLayerConfig, style: { ...selectedLayerConfig?.style, + fillColor, borderColor, }, }); - }, [borderColor]); + }, [fillColor, borderColor]); const onBorderThicknessChange = (e: any) => { setSelectedLayerConfig({ @@ -82,25 +116,18 @@ export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig }; useEffect(() => { - if ( - selectedLayerConfig?.style?.borderThickness < 0 || - selectedLayerConfig?.style?.borderThickness > 100 - ) { - setHasInvalidThickness(true); - } else { - setHasInvalidThickness(false); - } + const borderThickness = selectedLayerConfig?.style?.borderThickness; + const invalidThickness = + borderThickness < DOCUMENTS_MIN_MARKER_BORDER_THICKNESS || + borderThickness > DOCUMENTS_MAX_MARKER_BORDER_THICKNESS; + setHasInvalidThickness(invalidThickness); }, [selectedLayerConfig?.style?.borderThickness]); useEffect(() => { - if ( - selectedLayerConfig?.style?.markerSize < 0 || - selectedLayerConfig?.style?.markerSize > 100 - ) { - setHasInvalidSize(true); - } else { - setHasInvalidSize(false); - } + const markerSize = selectedLayerConfig?.style?.markerSize; + const invalidSize = + markerSize < DOCUMENTS_MIN_MARKER_SIZE || markerSize > DOCUMENTS_MAX_MARKER_SIZE; + setHasInvalidSize(invalidSize); }, [selectedLayerConfig?.style?.markerSize]); const toggleButtonsGeoType = [ @@ -122,26 +149,6 @@ export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig setToggleGeoTypeIdSelected(optionId); }; - interface ColorPickerProps { - color: string; - setColor: Function; - label: string; - } - - const ColorPicker = ({ color, setColor, label }: ColorPickerProps) => { - return ( - - {}} - fullWidth={true} - /> - - ); - }; - interface WidthSelectorProps { size: number; onWidthChange: Function; @@ -192,8 +199,18 @@ export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Point` && ( - - + + - + - - + + ({ }, style: { ...getStyleColor(), - borderThickness: MAP_LAYER_DEFAULT_BORDER_THICKNESS, + borderThickness: DOCUMENTS_DEFAULT_MARKER_BORDER_THICKNESS, markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, }, }, From 26aea7abc50c5a91c1bada2c97795fe615b39002 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 1 Mar 2023 16:29:51 -0800 Subject: [PATCH 30/77] Move zoom and coordinates as separate component (#309) We don't have to render zoom/coordiantes if there is no change in maps movement. Added unit test to check render Signed-off-by: Vijayan Balasubramanian --- .../map_container/map_container.tsx | 16 ++------- .../map_container/mapsFooter.test.tsx | 33 +++++++++++++++++ .../components/map_container/mapsFooter.tsx | 35 +++++++++++++++++++ 3 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 public/components/map_container/mapsFooter.test.tsx create mode 100644 public/components/map_container/mapsFooter.tsx diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 5bd50581..4189231e 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -4,7 +4,6 @@ */ import React, { useEffect, useRef, useState } from 'react'; -import { EuiPanel } from '@elastic/eui'; import { LngLat, Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; @@ -34,6 +33,7 @@ import { getReferenceLayers, referenceLayerTypeLookup, } from '../../model/layersFunctions'; +import { MapsFooter } from './mapsFooter'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -298,19 +298,7 @@ export const MapContainer = ({ return (
- - - {coordinates && - `lat: ${coordinates.lat.toFixed(4)}, lon: ${coordinates.lng.toFixed(4)}, `} - zoom: {zoom} - - + {mounted && ( { + // setup a DOM element as a render target + container = document.createElement('div'); + document.body.appendChild(container); +}); + +afterEach(() => { + // cleanup on exiting + unmountComponentAtNode(container!); + container?.remove(); + container = null; +}); + +it('renders with or without coordinates', () => { + act(() => { + render(, container); + }); + expect(container?.textContent).toBe('zoom: 3'); + + act(() => { + const coordinates: LngLat = new LngLat(-73.974912, 40.773654); + render(, container); + }); + expect(container?.textContent).toBe('lat: 40.7737, lon: -73.9749, zoom: 3'); +}); diff --git a/public/components/map_container/mapsFooter.tsx b/public/components/map_container/mapsFooter.tsx new file mode 100644 index 00000000..95a90fa1 --- /dev/null +++ b/public/components/map_container/mapsFooter.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiPanel } from '@elastic/eui'; +import { isEqual } from 'lodash'; +import React, { memo } from 'react'; +import { LngLat } from 'maplibre-gl'; + +const coordinatesRoundOffDigit = 4; +interface MapFooterProps { + coordinates?: LngLat; + zoom: number; +} + +export const MapsFooter = memo(({ coordinates, zoom }: MapFooterProps) => { + return ( + + + {coordinates && + `lat: ${coordinates.lat.toFixed( + coordinatesRoundOffDigit + )}, lon: ${coordinates.lng.toFixed(coordinatesRoundOffDigit)}, `} + zoom: {zoom} + + + ); +}, isEqual); From 03b0ee4697593394a5695010797c00083f2a5331 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Thu, 2 Mar 2023 18:15:22 -0800 Subject: [PATCH 31/77] Introduce disable tooltip on hover property (#313) Added new property that allows users to disable tooltip on hover. The default value is false to keep backward compatability. We will revisit the ideal default value based on user's feedback in later release. Signed-off-by: Vijayan Balasubramanian --- common/index.ts | 1 + public/components/map_container/map_container.tsx | 10 ++++++++-- public/components/tooltip/create_tooltip.tsx | 6 ++++++ public/model/mapLayerType.ts | 1 + public/utils/getIntialConfig.ts | 2 ++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/common/index.ts b/common/index.ts index a5d03575..d039dd70 100644 --- a/common/index.ts +++ b/common/index.ts @@ -31,6 +31,7 @@ export const DOCUMENTS_MIN_MARKER_BORDER_THICKNESS = 0; export const DOCUMENTS_MAX_MARKER_BORDER_THICKNESS = 100; export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; +export const DOCUMENTS_DEFAULT_DISABLE_TOOLTIPS_ON_HOVER: boolean = false; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; export const LAYER_PANEL_HIDE_LAYER_ICON = 'eyeClosed'; export const LAYER_PANEL_SHOW_LAYER_ICON = 'eye'; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 4189231e..47dd9b7d 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -18,7 +18,12 @@ import { Query, } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; -import { createPopup, getPopupLocation, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; +import { + createPopup, + getPopupLocation, + isTooltipEnabledLayer, + isTooltipEnabledOnHover, +} from '../tooltip/create_tooltip'; import { handleDataLayerRender, handleReferenceLayerRender, @@ -153,11 +158,12 @@ export const MapContainer = ({ // remove previous popup hoverPopup?.remove(); + const tooltipEnabledLayersOnHover = layers.filter(isTooltipEnabledOnHover); const features = maplibreRef.current?.queryRenderedFeatures(e.point); if (features && maplibreRef.current) { hoverPopup = createPopup({ features, - layers: tooltipEnabledLayers, + layers: tooltipEnabledLayersOnHover, // enable close button to avoid occasional dangling tooltip that is not cleared during mouse leave action showCloseButton: true, showPagination: false, diff --git a/public/components/tooltip/create_tooltip.tsx b/public/components/tooltip/create_tooltip.tsx index d278e721..9b52c475 100644 --- a/public/components/tooltip/create_tooltip.tsx +++ b/public/components/tooltip/create_tooltip.tsx @@ -25,6 +25,12 @@ export function isTooltipEnabledLayer( ); } +export function isTooltipEnabledOnHover( + layer: MapLayerSpecification +): layer is DocumentLayerSpecification { + return isTooltipEnabledLayer(layer) && !layer.source?.disableTooltipsOnHover; +} + export function groupFeaturesByLayers( features: MapGeoJSONFeature[], layers: DocumentLayerSpecification[] diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index 0d558103..c2386ba3 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -42,6 +42,7 @@ export type DocumentLayerSpecification = { geoFieldName: string; documentRequestNumber: number; showTooltips: boolean; + disableTooltipsOnHover?: boolean; tooltipFields: string[]; useGeoBoundingBoxFilter: boolean; filters: Filter[]; diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts index cd78ec82..555e934b 100644 --- a/public/utils/getIntialConfig.ts +++ b/public/utils/getIntialConfig.ts @@ -18,6 +18,7 @@ import { MAP_REFERENCE_LAYER_DEFAULT_OPACITY, OPENSEARCH_MAP_LAYER, CUSTOM_MAP, + DOCUMENTS_DEFAULT_DISABLE_TOOLTIPS_ON_HOVER, } from '../../common'; import { MapState } from '../model/mapState'; import { ConfigSchema } from '../../common/config'; @@ -53,6 +54,7 @@ export const getLayerConfigMap = (mapConfig: ConfigSchema) => ({ documentRequestNumber: DOCUMENTS_DEFAULT_REQUEST_NUMBER, tooltipFields: DOCUMENTS_DEFAULT_TOOLTIPS, showTooltips: DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, + disableTooltipsOnHover: DOCUMENTS_DEFAULT_DISABLE_TOOLTIPS_ON_HOVER, }, style: { ...getStyleColor(), From eb7dfe9fe5e54b4bb796f242efc1324fa58436d9 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Fri, 3 Mar 2023 14:19:27 -0800 Subject: [PATCH 32/77] Move coordinates to footer (#315) This will avoid mapcontainer reload whenever mouse move action is called. This will also seprate mouse move from tootip and coordinates into separate action. Signed-off-by: Vijayan Balasubramanian --- .../map_container/map_container.tsx | 10 ++-- .../map_container/mapsFooter.test.tsx | 33 ------------ .../map_container/maps_footer.test.tsx | 52 +++++++++++++++++++ .../{mapsFooter.tsx => maps_footer.tsx} | 28 +++++++--- 4 files changed, 77 insertions(+), 46 deletions(-) delete mode 100644 public/components/map_container/mapsFooter.test.tsx create mode 100644 public/components/map_container/maps_footer.test.tsx rename public/components/map_container/{mapsFooter.tsx => maps_footer.tsx} (50%) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 47dd9b7d..fada3930 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -4,7 +4,7 @@ */ import React, { useEffect, useRef, useState } from 'react'; -import { LngLat, Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; +import { Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; @@ -38,7 +38,7 @@ import { getReferenceLayers, referenceLayerTypeLookup, } from '../../model/layersFunctions'; -import { MapsFooter } from './mapsFooter'; +import { MapsFooter } from './maps_footer'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -77,7 +77,6 @@ export const MapContainer = ({ const mapContainer = useRef(null); const [mounted, setMounted] = useState(false); const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); - const [coordinates, setCoordinates] = useState(); const [selectedLayerConfig, setSelectedLayerConfig] = useState< MapLayerSpecification | undefined >(); @@ -152,9 +151,6 @@ export const MapContainer = ({ } function onMouseMoveMap(e: MapEventType['mousemove']) { - // This is required to update coordinates on map only on mouse move - setCoordinates(e.lngLat.wrap()); - // remove previous popup hoverPopup?.remove(); @@ -304,7 +300,7 @@ export const MapContainer = ({ return (
- + {mounted && } {mounted && ( { - // setup a DOM element as a render target - container = document.createElement('div'); - document.body.appendChild(container); -}); - -afterEach(() => { - // cleanup on exiting - unmountComponentAtNode(container!); - container?.remove(); - container = null; -}); - -it('renders with or without coordinates', () => { - act(() => { - render(, container); - }); - expect(container?.textContent).toBe('zoom: 3'); - - act(() => { - const coordinates: LngLat = new LngLat(-73.974912, 40.773654); - render(, container); - }); - expect(container?.textContent).toBe('lat: 40.7737, lon: -73.9749, zoom: 3'); -}); diff --git a/public/components/map_container/maps_footer.test.tsx b/public/components/map_container/maps_footer.test.tsx new file mode 100644 index 00000000..9b3a1a00 --- /dev/null +++ b/public/components/map_container/maps_footer.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { act } from 'react-dom/test-utils'; + +import { MapsFooter } from './maps_footer'; +import { Map as Maplibre } from 'maplibre-gl'; + +let container: Element | null; +let mockMap: Maplibre; +const mockCallbackMap: Map void> = new Map void>(); +beforeEach(() => { + // setup a DOM element as a render target + container = document.createElement('div'); + document.body.appendChild(container); + mockCallbackMap.clear(); + mockMap = ({ + on: (eventType: string, callback: () => void) => { + mockCallbackMap.set(eventType, callback); + }, + off: (eventType: string, callback: () => void) => { + mockCallbackMap.delete(eventType); + }, + } as unknown) as Maplibre; +}); + +afterEach(() => { + // cleanup on exiting + unmountComponentAtNode(container!); + container?.remove(); + container = null; +}); + +it('renders map footer', () => { + act(() => { + render(, container); + }); + expect(container?.textContent).toBe('zoom: 2'); + expect(mockCallbackMap.size).toEqual(1); +}); + +it('clean up is called', () => { + act(() => { + render(, container); + }); + unmountComponentAtNode(container!); + expect(mockCallbackMap.size).toEqual(0); +}); diff --git a/public/components/map_container/mapsFooter.tsx b/public/components/map_container/maps_footer.tsx similarity index 50% rename from public/components/map_container/mapsFooter.tsx rename to public/components/map_container/maps_footer.tsx index 95a90fa1..25f6badc 100644 --- a/public/components/map_container/mapsFooter.tsx +++ b/public/components/map_container/maps_footer.tsx @@ -4,17 +4,33 @@ */ import { EuiPanel } from '@elastic/eui'; -import { isEqual } from 'lodash'; -import React, { memo } from 'react'; -import { LngLat } from 'maplibre-gl'; +import React, { memo, useEffect, useState } from 'react'; +import { LngLat, MapEventType } from 'maplibre-gl'; +import { Map as Maplibre } from 'maplibre-gl'; const coordinatesRoundOffDigit = 4; interface MapFooterProps { - coordinates?: LngLat; + map: Maplibre; zoom: number; } -export const MapsFooter = memo(({ coordinates, zoom }: MapFooterProps) => { +export const MapsFooter = memo(({ map, zoom }: MapFooterProps) => { + const [coordinates, setCoordinates] = useState(); + useEffect(() => { + function onMouseMoveMap(e: MapEventType['mousemove']) { + setCoordinates(e.lngLat.wrap()); + } + + if (map) { + map.on('mousemove', onMouseMoveMap); + } + return () => { + if (map) { + map.off('mousemove', onMouseMoveMap); + } + }; + }, []); + return ( { ); -}, isEqual); +}); From e3eef6b495c5ed574f11f7a97b37640c5d81cc49 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Fri, 3 Mar 2023 16:47:52 -0800 Subject: [PATCH 33/77] Update tooltip behavior change (#317) 1. Added switch to configure display tooltip on hover or not. 2. By default this behvior is "on" to be consistent with bwc. Signed-off-by: Vijayan Balasubramanian --- common/index.ts | 2 +- .../document_layer_source.tsx | 36 ++++++++++++++----- public/components/tooltip/create_tooltip.tsx | 2 +- public/model/mapLayerType.ts | 2 +- public/utils/getIntialConfig.ts | 4 +-- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/common/index.ts b/common/index.ts index d039dd70..c7110cce 100644 --- a/common/index.ts +++ b/common/index.ts @@ -31,7 +31,7 @@ export const DOCUMENTS_MIN_MARKER_BORDER_THICKNESS = 0; export const DOCUMENTS_MAX_MARKER_BORDER_THICKNESS = 100; export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; -export const DOCUMENTS_DEFAULT_DISABLE_TOOLTIPS_ON_HOVER: boolean = false; +export const DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER: boolean = true; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; export const LAYER_PANEL_HIDE_LAYER_ICON = 'eyeClosed'; export const LAYER_PANEL_SHOW_LAYER_ICON = 'eye'; diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index bd24b35e..af45bde0 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -15,6 +15,7 @@ import { EuiSpacer, EuiPanel, EuiForm, + EuiSwitch, EuiCheckbox, EuiFormRow, } from '@elastic/eui'; @@ -57,7 +58,7 @@ export const DocumentLayerSource = ({ } = useOpenSearchDashboards(); const [indexPattern, setIndexPattern] = useState(); const [hasInvalidRequestNumber, setHasInvalidRequestNumber] = useState(false); - const [showTooltips, setShowTooltips] = useState( + const [enableTooltips, setEnableTooltips] = useState( selectedLayerConfig.source.showTooltips ); const memorizedForm = useRef({}); @@ -249,12 +250,19 @@ export const DocumentLayerSource = ({ ); }, [selectedLayerConfig.source.documentRequestNumber]); - const onShowTooltipsChange = (event: { target: { checked: React.SetStateAction } }) => { - setShowTooltips(event.target.checked); + const onEnableTooltipsChange = (event: { target: { checked: React.SetStateAction } }) => { + setEnableTooltips(event.target.checked); const source = { ...selectedLayerConfig.source, showTooltips: event.target.checked }; setSelectedLayerConfig({ ...selectedLayerConfig, source }); }; + const onDisplayTooltipsOnHoverChange = (event: { + target: { checked: React.SetStateAction }; + }) => { + const source = { ...selectedLayerConfig.source, displayTooltipsOnHover: event.target.checked }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + const onToggleGeoBoundingBox = (e: React.ChangeEvent) => { const source = { ...selectedLayerConfig.source, useGeoBoundingBoxFilter: e.target.checked }; setSelectedLayerConfig({ ...selectedLayerConfig, source }); @@ -394,10 +402,12 @@ export const DocumentLayerSource = ({ @@ -411,12 +421,22 @@ export const DocumentLayerSource = ({ singleSelection={false} onChange={onTooltipSelectionChange} sortMatchesBy="startsWith" - placeholder={i18n.translate('documentLayer.selectDataFieldPlaceholder', { + placeholder={i18n.translate('documentLayer.addedTooltipFields', { defaultMessage: 'Add tooltip fields', })} fullWidth={true} /> + + + diff --git a/public/components/tooltip/create_tooltip.tsx b/public/components/tooltip/create_tooltip.tsx index 9b52c475..4e378aa4 100644 --- a/public/components/tooltip/create_tooltip.tsx +++ b/public/components/tooltip/create_tooltip.tsx @@ -28,7 +28,7 @@ export function isTooltipEnabledLayer( export function isTooltipEnabledOnHover( layer: MapLayerSpecification ): layer is DocumentLayerSpecification { - return isTooltipEnabledLayer(layer) && !layer.source?.disableTooltipsOnHover; + return isTooltipEnabledLayer(layer) && (layer.source?.displayTooltipsOnHover ?? true); } export function groupFeaturesByLayers( diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index c2386ba3..f6e097e9 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -42,7 +42,7 @@ export type DocumentLayerSpecification = { geoFieldName: string; documentRequestNumber: number; showTooltips: boolean; - disableTooltipsOnHover?: boolean; + displayTooltipsOnHover?: boolean; tooltipFields: string[]; useGeoBoundingBoxFilter: boolean; filters: Filter[]; diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts index 555e934b..1130b630 100644 --- a/public/utils/getIntialConfig.ts +++ b/public/utils/getIntialConfig.ts @@ -18,7 +18,7 @@ import { MAP_REFERENCE_LAYER_DEFAULT_OPACITY, OPENSEARCH_MAP_LAYER, CUSTOM_MAP, - DOCUMENTS_DEFAULT_DISABLE_TOOLTIPS_ON_HOVER, + DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER, } from '../../common'; import { MapState } from '../model/mapState'; import { ConfigSchema } from '../../common/config'; @@ -54,7 +54,7 @@ export const getLayerConfigMap = (mapConfig: ConfigSchema) => ({ documentRequestNumber: DOCUMENTS_DEFAULT_REQUEST_NUMBER, tooltipFields: DOCUMENTS_DEFAULT_TOOLTIPS, showTooltips: DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, - disableTooltipsOnHover: DOCUMENTS_DEFAULT_DISABLE_TOOLTIPS_ON_HOVER, + displayTooltipsOnHover: DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER, }, style: { ...getStyleColor(), From 28e0429c79ff32ff3f359aa48e9c48ea93442926 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 6 Mar 2023 13:10:37 -0800 Subject: [PATCH 34/77] Support adding static label to document layer (#322) * Support adding static label to document layer Signed-off-by: Junqiu Lei * Update label type Signed-off-by: Junqiu Lei --------- Signed-off-by: Junqiu Lei --- common/index.ts | 13 + .../document_layer_config_panel.tsx | 2 +- .../documents_config/style/color_picker.tsx | 52 +++ .../{ => style}/document_layer_style.tsx | 219 +++++++------ .../style/label_config.test.tsx | 201 ++++++++++++ .../documents_config/style/label_config.tsx | 310 ++++++++++++++++++ public/model/documentLayerFunctions.ts | 47 ++- public/model/map/layer_operations.test.ts | 93 ++++++ public/model/map/layer_operations.ts | 78 +++++ public/model/mapLayerType.ts | 9 + public/utils/getIntialConfig.ts | 16 + 11 files changed, 936 insertions(+), 104 deletions(-) create mode 100644 public/components/layer_config/documents_config/style/color_picker.tsx rename public/components/layer_config/documents_config/{ => style}/document_layer_style.tsx (51%) create mode 100644 public/components/layer_config/documents_config/style/label_config.test.tsx create mode 100644 public/components/layer_config/documents_config/style/label_config.tsx diff --git a/common/index.ts b/common/index.ts index c7110cce..e4bb2769 100644 --- a/common/index.ts +++ b/common/index.ts @@ -33,6 +33,19 @@ export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; export const DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER: boolean = true; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; +export const DOCUMENTS_DEFAULT_LABEL_ENABLES: boolean = false; +export const DOCUMENTS_DEFAULT_LABEL_VALUE: string = ''; +export const DOCUMENTS_DEFAULT_LABEL_TYPE: string = 'fixed'; +export const DOCUMENTS_DEFAULT_LABEL_SIZE: number = 20; +export const DOCUMENTS_MIN_LABEL_SIZE: number = 1; +export const DOCUMENTS_MAX_LABEL_SIZE: number = 100; +export const DOCUMENTS_DEFAULT_LABEL_COLOR: string = '#000000'; +export const DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR: string = '#FFFFFF'; +export const DOCUMENTS_DEFAULT_LABEL_BORDER_WIDTH: number = 20; +export const DOCUMENTS_NONE_LABEL_BORDER_WIDTH: number = 0; +export const DOCUMENTS_SMALL_LABEL_BORDER_WIDTH: number = 2; +export const DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH: number = 5; +export const DOCUMENTS_LARGE_LABEL_BORDER_WIDTH: number = 10; export const LAYER_PANEL_HIDE_LAYER_ICON = 'eyeClosed'; export const LAYER_PANEL_SHOW_LAYER_ICON = 'eye'; export const MAP_DATA_LAYER_DEFAULT_OPACITY = 70; diff --git a/public/components/layer_config/documents_config/document_layer_config_panel.tsx b/public/components/layer_config/documents_config/document_layer_config_panel.tsx index febf8793..e7f30c75 100644 --- a/public/components/layer_config/documents_config/document_layer_config_panel.tsx +++ b/public/components/layer_config/documents_config/document_layer_config_panel.tsx @@ -8,7 +8,7 @@ import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; import { DocumentLayerSpecification } from '../../../model/mapLayerType'; import { LayerBasicSettings } from '../layer_basic_settings'; import { DocumentLayerSource } from './document_layer_source'; -import { DocumentLayerStyle } from './document_layer_style'; +import { DocumentLayerStyle } from './style/document_layer_style'; interface Props { selectedLayerConfig: DocumentLayerSpecification; diff --git a/public/components/layer_config/documents_config/style/color_picker.tsx b/public/components/layer_config/documents_config/style/color_picker.tsx new file mode 100644 index 00000000..a91f88dc --- /dev/null +++ b/public/components/layer_config/documents_config/style/color_picker.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo, useEffect } from 'react'; +import { EuiColorPicker, EuiFormRow, useColorPickerState } from '@elastic/eui'; + +export interface ColorPickerProps { + originColor: string; + label: string; + selectedLayerConfigId: string; + setIsUpdateDisabled: Function; + onColorChange: (color: string) => void; +} + +export const ColorPicker = memo( + ({ + originColor, + label, + selectedLayerConfigId, + setIsUpdateDisabled, + onColorChange, + }: ColorPickerProps) => { + const [color, setColor, colorErrors] = useColorPickerState(originColor); + + useEffect(() => { + onColorChange(String(color)); + }, [color]); + + useEffect(() => { + setIsUpdateDisabled(!!colorErrors); + }, [colorErrors]); + + // It's used to update the style color when switch layer config between different document layers + useEffect(() => { + setColor(originColor, !!colorErrors ? { isValid: false } : { isValid: true }); + }, [selectedLayerConfigId]); + + return ( + + + + ); + } +); diff --git a/public/components/layer_config/documents_config/document_layer_style.tsx b/public/components/layer_config/documents_config/style/document_layer_style.tsx similarity index 51% rename from public/components/layer_config/documents_config/document_layer_style.tsx rename to public/components/layer_config/documents_config/style/document_layer_style.tsx index 6d6a0b62..55feee0d 100644 --- a/public/components/layer_config/documents_config/document_layer_style.tsx +++ b/public/components/layer_config/documents_config/style/document_layer_style.tsx @@ -3,29 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { memo, useEffect, useState } from 'react'; +import React, { ChangeEvent, useEffect, useState } from 'react'; import { - EuiColorPicker, EuiFieldNumber, EuiFormLabel, - EuiFormErrorText, - EuiFlexItem, EuiSpacer, EuiButtonGroup, EuiPanel, EuiTitle, EuiFormRow, EuiForm, - useColorPickerState, + EuiHorizontalRule, } from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; -import { DocumentLayerSpecification } from '../../../model/mapLayerType'; +import { i18n } from '@osd/i18n'; +import { DocumentLayerSpecification } from '../../../../model/mapLayerType'; import { DOCUMENTS_MAX_MARKER_BORDER_THICKNESS, DOCUMENTS_MAX_MARKER_SIZE, DOCUMENTS_MIN_MARKER_BORDER_THICKNESS, DOCUMENTS_MIN_MARKER_SIZE, -} from '../../../../common'; +} from '../../../../../common'; +import { LabelConfig } from './label_config'; +import { ColorPicker } from './color_picker'; interface Props { selectedLayerConfig: DocumentLayerSpecification; @@ -33,32 +32,11 @@ interface Props { setIsUpdateDisabled: Function; } -interface ColorPickerProps { - color: string; - setColor: (text: string, { isValid }: { isValid: boolean }) => void; - label: string; - colorError: (text: string, { isValid }: { isValid: boolean }) => void; -} - -const ColorPicker = memo(({ color, setColor, label, colorError }: ColorPickerProps) => { - return ( - - - - ); -}); - export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig, setIsUpdateDisabled, }: Props) => { - const [fillColor, setFillColor, fillColorErrors] = useColorPickerState( - selectedLayerConfig?.style?.fillColor - ); - const [borderColor, setBorderColor, borderColorErrors] = useColorPickerState( - selectedLayerConfig?.style?.borderColor - ); const [hasInvalidThickness, setHasInvalidThickness] = useState(false); const [hasInvalidSize, setHasInvalidSize] = useState(false); const geoTypeToggleButtonGroupPrefix = 'geoTypeToggleButtonGroup'; @@ -66,34 +44,10 @@ export const DocumentLayerStyle = ({ `${geoTypeToggleButtonGroupPrefix}__Point` ); - // It's used to update the style color when switch layer config between different document layers - useEffect(() => { - setFillColor( - selectedLayerConfig?.style?.fillColor, - !!fillColorErrors ? { isValid: false } : { isValid: true } - ); - setBorderColor( - selectedLayerConfig?.style?.borderColor, - !!borderColorErrors ? { isValid: false } : { isValid: true } - ); - }, [selectedLayerConfig.id]); - useEffect(() => { - const disableUpdate = - !!fillColorErrors || !!borderColorErrors || hasInvalidSize || hasInvalidThickness; + const disableUpdate = hasInvalidSize || hasInvalidThickness; setIsUpdateDisabled(disableUpdate); - }, [fillColorErrors, borderColorErrors, hasInvalidSize, hasInvalidThickness]); - - useEffect(() => { - setSelectedLayerConfig({ - ...selectedLayerConfig, - style: { - ...selectedLayerConfig?.style, - fillColor, - borderColor, - }, - }); - }, [fillColor, borderColor]); + }, [hasInvalidSize, hasInvalidThickness]); const onBorderThicknessChange = (e: any) => { setSelectedLayerConfig({ @@ -151,36 +105,64 @@ export const DocumentLayerStyle = ({ interface WidthSelectorProps { size: number; - onWidthChange: Function; + onWidthChange: (event: ChangeEvent) => void; label: string; hasInvalid: boolean; + min: number; + max: number; } - const WidthSelector = ({ label, onWidthChange, size, hasInvalid }: WidthSelectorProps) => { + const WidthSelector = ({ + label, + onWidthChange, + size, + hasInvalid, + min, + max, + }: WidthSelectorProps) => { return ( - - - px} - fullWidth={true} - /> - {hasInvalid && ( - - - - )} - + + px} + fullWidth={true} + min={min} + max={max} + /> ); }; + const onFillColorChange = (fillColor: string) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + fillColor, + }, + }); + }; + + const onBorderColorChange = (borderColor: string) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + borderColor, + }, + }); + }; + return ( @@ -200,69 +182,106 @@ export const DocumentLayerStyle = ({ {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Point` && ( )} {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Line` && ( )} {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Polygon` && ( )} + + ); diff --git a/public/components/layer_config/documents_config/style/label_config.test.tsx b/public/components/layer_config/documents_config/style/label_config.test.tsx new file mode 100644 index 00000000..90f6b077 --- /dev/null +++ b/public/components/layer_config/documents_config/style/label_config.test.tsx @@ -0,0 +1,201 @@ +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { LabelConfig } from './label_config'; +import { EuiCheckbox, EuiFormLabel } from '@elastic/eui'; +import { + DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, + DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, + DOCUMENTS_NONE_LABEL_BORDER_WIDTH, + DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, +} from '../../../../../common'; +import { DocumentLayerSpecification } from '../../../../model/mapLayerType'; + +describe('LabelConfig', () => { + let wrapper: ShallowWrapper; + + const setSelectedLayerConfigMock = jest.fn(); + const setIsUpdateDisabledMock = jest.fn(); + + const selectedLayerConfig: DocumentLayerSpecification = { + name: 'My Document Layer', + id: 'document-layer-1', + type: 'documents', + description: 'A layer of documents', + zoomRange: [5, 18], + opacity: 0.8, + visibility: 'visible', + source: { + indexPatternRefName: 'myIndexPattern', + indexPatternId: '123456', + geoFieldType: 'geo_point', + geoFieldName: 'location', + documentRequestNumber: 1000, + showTooltips: true, + tooltipFields: ['field1', 'field2', 'field3'], + useGeoBoundingBoxFilter: true, + filters: [], + }, + style: { + fillColor: '#FF0000', + borderColor: '#000000', + borderThickness: 2, + markerSize: 10, + label: { + enabled: true, + tittle: 'My Label', + tittleType: 'fixed', + color: '#FFFFFF', + size: 12, + borderColor: '#000000', + borderWidth: 2, + }, + }, + }; + + beforeEach(() => { + wrapper = shallow( + + ); + }); + + it('should render EuiCheckbox with correct props', () => { + const checkbox = wrapper.find(EuiCheckbox); + expect(checkbox.prop('label')).toEqual('Add label'); + expect(checkbox.prop('checked')).toEqual(true); + }); + + it('should call setSelectedLayerConfig with updated config when onChangeShowLabel is called', () => { + const checkbox = wrapper.find(EuiCheckbox); + checkbox.simulate('change', { target: { checked: false } }); + expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + enabled: false, + }, + }, + }); + }); + + it('should render EuiCheckbox with correct props when enableLabel is false', () => { + const newSelectedLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + enabled: false, + }, + }, + }; + wrapper.setProps({ selectedLayerConfig: newSelectedLayerConfig }); + const checkbox = wrapper.find(EuiCheckbox); + expect(checkbox.prop('checked')).toEqual(false); + }); + + it('should call setSelectedLayerConfig with updated config when onChangeLabelTittleType is called', () => { + const select = wrapper.find('EuiSelect').at(0); + select.simulate('change', { target: { value: 'by_field' } }); + expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + tittleType: 'by_field', + }, + }, + }); + }); + + it('should render EuiFieldText with correct props when labelTittleType is "fixed"', () => { + const fieldText = wrapper.find('EuiFieldText'); + expect(fieldText.prop('value')).toEqual('My Label'); + expect(fieldText.prop('disabled')).toEqual(false); + }); + + it('should call setSelectedLayerConfig with updated config when onStaticLabelChange is called', () => { + const fieldText = wrapper.find('EuiFieldText'); + fieldText.simulate('change', { target: { value: 'new label' } }); + expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + tittle: 'new label', + }, + }, + }); + }); + + it('should render EuiFieldNumber with correct props', () => { + const fieldNumber = wrapper.find('EuiFieldNumber'); + expect(fieldNumber.prop('value')).toEqual(12); + expect(fieldNumber.prop('append')).toEqual(px); + expect(fieldNumber.prop('fullWidth')).toEqual(true); + }); + + it('should call setSelectedLayerConfig with updated config when OnChangeLabelSize is called', () => { + const fieldNumber = wrapper.find('EuiFieldNumber'); + fieldNumber.simulate('change', { target: { value: 20 } }); + expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + size: 20, + }, + }, + }); + }); + + it('should render ColorPicker with correct props for label color', () => { + const colorPicker = wrapper.find({ label: 'Label color' }); + expect(colorPicker.prop('originColor')).toEqual('#FFFFFF'); + expect(colorPicker.prop('selectedLayerConfigId')).toEqual(selectedLayerConfig.id); + expect(colorPicker.prop('setIsUpdateDisabled')).toEqual(setIsUpdateDisabledMock); + }); + + it('should render ColorPicker with correct props for label border color', () => { + const colorPicker = wrapper.find({ label: 'Label border color' }); + expect(colorPicker.prop('originColor')).toEqual('#000000'); + expect(colorPicker.prop('selectedLayerConfigId')).toEqual(selectedLayerConfig.id); + expect(colorPicker.prop('setIsUpdateDisabled')).toEqual(setIsUpdateDisabledMock); + }); + + it('should render EuiSelect with correct props for label border width', () => { + const select = wrapper.find('EuiSelect').at(1); + expect(select.prop('options')).toEqual([ + { value: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, text: 'None' }, + { value: DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, text: 'Small' }, + { value: DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, text: 'Medium' }, + { value: DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, text: 'Large' }, + ]); + expect(select.prop('value')).toEqual(2); + expect(select.prop('disabled')).toEqual(false); + expect(select.prop('fullWidth')).toEqual(true); + }); + + it('should call setSelectedLayerConfig with updated config when onChangeLabelBorderWidth is called', () => { + const select = wrapper.find('EuiSelect').at(1); + select.simulate('change', { target: { value: 3 } }); + expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + borderWidth: 3, + }, + }, + }); + }); +}); diff --git a/public/components/layer_config/documents_config/style/label_config.tsx b/public/components/layer_config/documents_config/style/label_config.tsx new file mode 100644 index 00000000..44131853 --- /dev/null +++ b/public/components/layer_config/documents_config/style/label_config.tsx @@ -0,0 +1,310 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { + EuiFormRow, + EuiFieldText, + EuiFlexItem, + EuiCheckbox, + EuiSelect, + EuiFlexGroup, + EuiFieldNumber, + EuiFormLabel, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { DocumentLayerSpecification } from '../../../../model/mapLayerType'; +import { ColorPicker } from './color_picker'; +import { + DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR, + DOCUMENTS_DEFAULT_LABEL_COLOR, + DOCUMENTS_DEFAULT_LABEL_SIZE, + DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, + DOCUMENTS_MAX_LABEL_SIZE, + DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, + DOCUMENTS_MIN_LABEL_SIZE, + DOCUMENTS_NONE_LABEL_BORDER_WIDTH, + DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, +} from '../../../../../common'; + +interface LabelProps { + selectedLayerConfig: DocumentLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; +} + +export const LabelConfig = ({ + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdateDisabled, +}: LabelProps) => { + const labelTittleTypeOptions = [ + { + value: 'fixed', + text: i18n.translate('maps.documents.label.fixedTittle', { defaultMessage: 'Fixed' }), + }, + // TODO: add support for using index pattern field as label + ]; + + const labelBorderWidthOptions = [ + { + value: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelNoneBorderWidth', { + defaultMessage: 'None', + }), + }, + { + value: DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelSmallBorderWidth', { + defaultMessage: 'Small', + }), + }, + { + value: DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelMediumBorderWidth', { + defaultMessage: 'Medium', + }), + }, + { + value: DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelLargeBorderWidth', { + defaultMessage: 'Large', + }), + }, + ]; + + const [inValidLabelTittle, setInValidLabelTittle] = React.useState(false); + const [invalidLabelSize, setInvalidLabelSize] = React.useState(false); + + useEffect(() => { + if (selectedLayerConfig.style?.label?.enabled) { + if (selectedLayerConfig.style.label?.tittleType === 'fixed') { + if (selectedLayerConfig.style.label?.tittle === '') { + setInValidLabelTittle(true); + } else { + setInValidLabelTittle(false); + } + } + if ( + selectedLayerConfig.style?.label?.size < DOCUMENTS_MIN_LABEL_SIZE || + selectedLayerConfig.style?.label?.size > DOCUMENTS_MAX_LABEL_SIZE + ) { + setInvalidLabelSize(true); + } else { + setInvalidLabelSize(false); + } + } + }, [selectedLayerConfig]); + + useEffect(() => { + if (inValidLabelTittle || invalidLabelSize) { + setIsUpdateDisabled(true); + } else { + setIsUpdateDisabled(false); + } + }, [inValidLabelTittle, invalidLabelSize]); + + const onChangeShowLabel = (e: any) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + enabled: Boolean(e.target.checked), + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + const onChangeLabelTittleType = (e: any) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + tittleType: String(e.target.value), + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + const onStaticLabelTittleChange = (e: { target: { value: any } }) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + tittle: String(e.target.value), + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + const OnChangeLabelSize = (e: { target: { value: any } }) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + size: Number(e.target.value), + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + const onChangeLabelBorderWidth = (e: any) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + borderWidth: Number(e.target.value), + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + const onChangeLabelBorderColor = (color: string) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + borderColor: color, + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + const onChangeLabelColor = (color: string) => { + const newLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style.label, + color, + }, + }, + }; + setSelectedLayerConfig(newLayerConfig); + }; + + return ( + <> + + + + {selectedLayerConfig.style?.label?.enabled && ( + <> + + + + + + + {selectedLayerConfig.style?.label?.tittleType === 'fixed' && ( + + )} + + + + + px} + fullWidth={true} + min={DOCUMENTS_MIN_LABEL_SIZE} + max={DOCUMENTS_MAX_LABEL_SIZE} + /> + + + + + + + + )} + + ); +}; diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index e218bd75..036020e7 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -12,10 +12,15 @@ import { addCircleLayer, addLineLayer, addPolygonLayer, + addSymbolLayer, hasLayer, + hasSymbolLayer, updateCircleLayer, updateLineLayer, updatePolygonLayer, + updateSymbolLayer, + removeSymbolLayer, + createSymbolLayerSpecification, } from './map/layer_operations'; interface MaplibreRef { @@ -220,6 +225,43 @@ const updateLayer = ( } }; +// The function to render label for document layer +const renderLabelLayer = ( + layerConfig: DocumentLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + const hasLabelLayer = hasSymbolLayer(maplibreRef.current!, layerConfig.id); + // If the label set to enabled, add the label layer + if (layerConfig.style?.label?.enabled) { + const symbolLayerSpec = createSymbolLayerSpecification(layerConfig); + if (hasLabelLayer) { + updateSymbolLayer(maplibreRef.current!, symbolLayerSpec); + } else { + addSymbolLayer(maplibreRef.current!, symbolLayerSpec, beforeLayerId); + } + } else { + // If the label set to disabled, remove the label layer if it exists + if (hasLabelLayer) { + removeSymbolLayer(maplibreRef.current!, layerConfig.id); + } + } +}; + +// The function to render point, line and shape layer for document layer +const renderMarkerLayer = ( + maplibreRef: MaplibreRef, + layerConfig: DocumentLayerSpecification, + data: any, + beforeLayerId: string | undefined +) => { + if (hasLayer(maplibreRef.current!, layerConfig.id)) { + updateLayer(layerConfig, maplibreRef, data); + } else { + addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); + } +}; + export const DocumentLayerFunctions = { render: ( maplibreRef: MaplibreRef, @@ -227,8 +269,7 @@ export const DocumentLayerFunctions = { data: any, beforeLayerId: string | undefined ) => { - return hasLayer(maplibreRef.current!, layerConfig.id) - ? updateLayer(layerConfig, maplibreRef, data) - : addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); + renderMarkerLayer(maplibreRef, layerConfig, data, beforeLayerId); + renderLabelLayer(layerConfig, maplibreRef, beforeLayerId); }, }; diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 7778cc1c..925c0825 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -14,6 +14,8 @@ import { updateLineLayer, updatePolygonLayer, updateLayerVisibilityHandler, + addSymbolLayer, + updateSymbolLayer, } from './layer_operations'; import { Map as Maplibre } from 'maplibre-gl'; import { MockMaplibreMap } from './__mocks__/map'; @@ -284,6 +286,97 @@ describe('Polygon layer', () => { }); }); +describe('Symbol layer', () => { + it('should add symbol layer successfully', () => { + const mockMap = new MockMaplibreMap([]); + const sourceId: string = 'symbol-layer-source'; + const expectedLayerId = sourceId + '-symbol'; + addSymbolLayer(mockMap as unknown as Maplibre, { + sourceId, + visibility: 'visible', + textFont: ['Noto Sans Regular'], + textField: 'test text', + textColor: '#af938a', + textSize: 12, + minZoom: 2, + maxZoom: 10, + opacity: 60, + symbolBorderWidth: 2, + symbolBorderColor: '#D6BF57', + }); + + const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; + expect(layer.getProperty('visibility')).toBe('visible'); + expect(layer.getProperty('source')).toBe(sourceId); + expect(layer.getProperty('type')).toBe('symbol'); + expect(layer.getProperty('minZoom')).toBe(2); + expect(layer.getProperty('maxZoom')).toBe(10); + expect(layer.getProperty('text-font')).toEqual(['Noto Sans Regular']); + expect(layer.getProperty('text-field')).toBe('test text'); + expect(layer.getProperty('text-opacity')).toBe(0.6); + expect(layer.getProperty('text-color')).toBe('#af938a'); + expect(layer.getProperty('text-size')).toBe(12); + expect(layer.getProperty('text-halo-width')).toBe(2); + expect(layer.getProperty('text-halo-color')).toBe('#D6BF57'); + }); + + it('should update symbol layer successfully', () => { + const mockMap = new MockMaplibreMap([]); + const sourceId: string = 'symbol-layer-source'; + const expectedLayerId = sourceId + '-symbol'; + // add layer first + addSymbolLayer(mockMap as unknown as Maplibre, { + sourceId, + visibility: 'visible', + textFont: ['Noto Sans Regular'], + textSize: 12, + textColor: '#251914', + textField: 'test text', + minZoom: 2, + maxZoom: 10, + opacity: 60, + symbolBorderWidth: 2, + symbolBorderColor: '#D6BF57', + }); + + expect(mockMap.getLayer(expectedLayerId).length).toBe(1); + // update symbol for test + const updatedText = 'updated text'; + const updatedTextColor = '#29d95b'; + const updatedTextSize = 14; + const updatedVisibility = 'none'; + const updatedOpacity = 80; + const updatedMinZoom = 4; + const updatedMaxZoom = 12; + const updatedSymbolBorderColor = '#D6BF57'; + const updatedSymbolBorderWidth = 4; + updateSymbolLayer(mockMap as unknown as Maplibre, { + sourceId, + visibility: updatedVisibility, + textFont: ['Noto Sans Regular'], + textSize: updatedTextSize, + textColor: updatedTextColor, + textField: updatedText, + minZoom: updatedMinZoom, + maxZoom: updatedMaxZoom, + opacity: updatedOpacity, + symbolBorderWidth: updatedSymbolBorderWidth, + symbolBorderColor: updatedSymbolBorderColor, + }); + const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; + expect(layer.getProperty('source')).toBe(sourceId); + expect(layer.getProperty('minZoom')).toBe(updatedMinZoom); + expect(layer.getProperty('maxZoom')).toBe(updatedMaxZoom); + expect(layer.getProperty('visibility')).toBe(updatedVisibility); + expect(layer.getProperty('text-field')).toBe(updatedText); + expect(layer.getProperty('text-opacity')).toBe(updatedOpacity / 100); + expect(layer.getProperty('text-color')).toBe(updatedTextColor); + expect(layer.getProperty('text-size')).toBe(updatedTextSize); + expect(layer.getProperty('text-halo-width')).toBe(updatedSymbolBorderWidth); + expect(layer.getProperty('text-halo-color')).toBe(updatedSymbolBorderColor); + }); +}); + describe('get layer', () => { it('should get layer successfully', function () { const mockLayer: MockLayer = new MockLayer('layer-1'); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 7061b1f8..eec63e32 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { LayerSpecification, Map as Maplibre } from 'maplibre-gl'; +import { DocumentLayerSpecification } from '../mapLayerType'; export const getLayers = (map: Maplibre, dashboardMapsLayerId?: string): LayerSpecification[] => { const layers: LayerSpecification[] = map.getStyle().layers; @@ -42,6 +43,10 @@ export const removeLayers = (map: Maplibre, layerId: string, removeSource?: bool } }; +export const removeMbLayer = (map: Maplibre, mbLayerId: string) => { + map.removeLayer(mbLayerId); +}; + export const updateLayerVisibilityHandler = ( map: Maplibre, layerId: string, @@ -222,3 +227,76 @@ const updatePolygonFillLayer = ( map.setPaintProperty(fillLayerId, 'fill-color', specification.fillColor); return fillLayerId; }; + +export interface SymbolLayerSpecification extends Layer { + visibility: string; + textFont: string[]; + textField: string; + textSize: number; + textColor: string; + symbolBorderWidth: number; + symbolBorderColor: string; +} + +export const createSymbolLayerSpecification = ( + layerConfig: DocumentLayerSpecification +): SymbolLayerSpecification => { + if (!layerConfig.style.label) { + throw new Error('Label style is not defined'); + } + return { + sourceId: layerConfig.id, + visibility: layerConfig.visibility, + textFont: ['Noto Sans Regular'], + textField: layerConfig.style.label.tittle, + textSize: layerConfig.style.label.size, + textColor: layerConfig.style.label.color, + minZoom: layerConfig.zoomRange[0], + maxZoom: layerConfig.zoomRange[1], + opacity: layerConfig.opacity, + symbolBorderWidth: layerConfig.style.label.borderWidth, + symbolBorderColor: layerConfig.style.label.borderColor, + }; +}; + +export const hasSymbolLayer = (map: Maplibre, layerId: string) => { + return !!map.getLayer(layerId + '-symbol'); +}; + +export const removeSymbolLayer = (map: Maplibre, layerId: string) => { + map.removeLayer(layerId + '-symbol'); +}; + +export const addSymbolLayer = ( + map: Maplibre, + specification: SymbolLayerSpecification, + beforeId?: string +): string => { + const symbolLayerId = specification.sourceId + '-symbol'; + map.addLayer( + { + id: symbolLayerId, + type: 'symbol', + source: specification.sourceId, + }, + beforeId + ); + return updateSymbolLayer(map, specification); +}; + +export const updateSymbolLayer = ( + map: Maplibre, + specification: SymbolLayerSpecification +): string => { + const symbolLayerId = specification.sourceId + '-symbol'; + map.setLayoutProperty(symbolLayerId, 'text-font', specification.textFont); + map.setLayoutProperty(symbolLayerId, 'text-field', specification.textField); + map.setLayoutProperty(symbolLayerId, 'visibility', specification.visibility); + map.setPaintProperty(symbolLayerId, 'text-opacity', specification.opacity / 100); + map.setLayerZoomRange(symbolLayerId, specification.minZoom, specification.maxZoom); + map.setPaintProperty(symbolLayerId, 'text-color', specification.textColor); + map.setLayoutProperty(symbolLayerId, 'text-size', specification.textSize); + map.setPaintProperty(symbolLayerId, 'text-halo-width', specification.symbolBorderWidth); + map.setPaintProperty(symbolLayerId, 'text-halo-color', specification.symbolBorderColor); + return symbolLayerId; +}; diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index f6e097e9..07832662 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -52,6 +52,15 @@ export type DocumentLayerSpecification = { borderColor: string; borderThickness: number; markerSize: number; + label?: { + enabled: boolean; + tittle: string; + tittleType: 'fixed' | 'by_field'; + color: string; + size: number; + borderColor: string; + borderWidth: number; + }; }; }; diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts index 1130b630..8d2374e3 100644 --- a/public/utils/getIntialConfig.ts +++ b/public/utils/getIntialConfig.ts @@ -19,6 +19,13 @@ import { OPENSEARCH_MAP_LAYER, CUSTOM_MAP, DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER, + DOCUMENTS_DEFAULT_LABEL_ENABLES, + DOCUMENTS_DEFAULT_LABEL_VALUE, + DOCUMENTS_DEFAULT_LABEL_TYPE, + DOCUMENTS_DEFAULT_LABEL_SIZE, + DOCUMENTS_DEFAULT_LABEL_COLOR, + DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR, + DOCUMENTS_NONE_LABEL_BORDER_WIDTH, } from '../../common'; import { MapState } from '../model/mapState'; import { ConfigSchema } from '../../common/config'; @@ -60,6 +67,15 @@ export const getLayerConfigMap = (mapConfig: ConfigSchema) => ({ ...getStyleColor(), borderThickness: DOCUMENTS_DEFAULT_MARKER_BORDER_THICKNESS, markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, + label: { + enabled: DOCUMENTS_DEFAULT_LABEL_ENABLES, + tittle: DOCUMENTS_DEFAULT_LABEL_VALUE, + tittleType: DOCUMENTS_DEFAULT_LABEL_TYPE, + size: DOCUMENTS_DEFAULT_LABEL_SIZE, + borderWidth: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, + color: DOCUMENTS_DEFAULT_LABEL_COLOR, + borderColor: DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR, + }, }, }, [CUSTOM_MAP.type]: { From 16fca69f4e48077b2befdddab5f394c755ed6968 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Mon, 6 Mar 2023 14:11:19 -0800 Subject: [PATCH 35/77] Add geo shape query filter (#319) Added Geo Shape Query Filter to build geo shape query for given shape , relations and label. Signed-off-by: Vijayan Balasubramanian --- public/model/geo/filter.test.ts | 57 ++++++++++++++++++++++++++- public/model/geo/filter.ts | 41 ++++++++++++++++++- public/model/layerRenderController.ts | 2 + 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/public/model/geo/filter.test.ts b/public/model/geo/filter.test.ts index d35ca6c8..212b58c1 100644 --- a/public/model/geo/filter.test.ts +++ b/public/model/geo/filter.test.ts @@ -5,8 +5,9 @@ import { LngLat } from 'maplibre-gl'; import { GeoBounds } from '../map/boundary'; -import { FilterMeta, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; -import { buildBBoxFilter } from './filter'; +import { FilterMeta, FILTERS, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; +import { buildBBoxFilter, buildSpatialGeometryFilter, GeoShapeFilter } from './filter'; +import { Polygon } from 'geojson'; describe('test bounding box filter', function () { it('should return valid bounding box', function () { @@ -40,3 +41,55 @@ describe('test bounding box filter', function () { expect(actualGeoBoundingBoxFilter.meta.params).toEqual(expectedBounds); }); }); + +describe('test geo shape filter', function () { + it('should return valid geo shape query', function () { + const mockPolygon: Polygon = { + type: 'Polygon', + coordinates: [ + [ + [74.006, 40.7128], + [71.0589, 42.3601], + [73.7562, 42.6526], + [74.006, 40.7128], + ], + ], + }; + const mockLabel: string = 'mypolygon'; + const fieldName: string = 'location'; + + const geoShapeFilter: GeoShapeFilter = buildSpatialGeometryFilter( + fieldName, + mockPolygon, + mockLabel, + 'INTERSECTS' + ); + const expectedFilter: GeoShapeFilter = { + meta: { + alias: mockLabel, + disabled: false, + negate: false, + key: 'location', + type: FILTERS.SPATIAL_FILTER, + }, + geo_shape: { + ignore_unmapped: true, + location: { + relation: 'INTERSECTS', + shape: { + type: 'Polygon', + coordinates: [ + [ + [74.006, 40.7128], + [71.0589, 42.3601], + [73.7562, 42.6526], + [74.006, 40.7128], + ], + ], + }, + }, + }, + }; + expect(geoShapeFilter).toEqual(expectedFilter); + }); +}); diff --git a/public/model/geo/filter.ts b/public/model/geo/filter.ts index b41f219c..ee6afdcf 100644 --- a/public/model/geo/filter.ts +++ b/public/model/geo/filter.ts @@ -4,9 +4,22 @@ */ import { LatLon } from '@opensearch-project/opensearch/api/types'; -import { FilterMeta, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; +import { Polygon } from 'geojson'; +import { + Filter, + FilterMeta, + FILTERS, + GeoBoundingBoxFilter, +} from '../../../../../src/plugins/data/common'; import { GeoBounds } from '../map/boundary'; +export type FilterRelations = 'INTERSECTS' | 'DISJOINT' | 'WITHIN'; + +export type GeoShapeFilter = Filter & { + meta: FilterMeta; + geo_shape: any; +}; + export const buildBBoxFilter = ( fieldName: string, mapBounds: GeoBounds, @@ -36,3 +49,29 @@ export const buildBBoxFilter = ( }, }; }; + +export const buildSpatialGeometryFilter = ( + fieldName: string, + filterShape: Polygon, + filterLabel: string, + relation: FilterRelations +): GeoShapeFilter => { + const meta: FilterMeta = { + negate: false, + key: fieldName, + alias: filterLabel, + type: FILTERS.SPATIAL_FILTER, + disabled: false, + }; + + return { + meta, + geo_shape: { + ignore_unmapped: true, + [fieldName]: { + relation, + shape: filterShape, + }, + }, + }; +}; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index e4b276a2..2eb4ad0d 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -16,6 +16,7 @@ import { isCompleteResponse, TimeRange, Query, + FILTERS, } from '../../../../src/plugins/data/common'; import { layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; @@ -116,6 +117,7 @@ export const handleDataLayerRender = ( alias: null, disabled: !mapLayer.source.useGeoBoundingBoxFilter || geoFieldType !== 'geo_point', negate: false, + type: FILTERS.GEO_BOUNDING_BOX, }; const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); filters.push(geoBoundingBoxFilter); From eb0bc0dc67c5b002e4d464e949cc3d3b9bbcca52 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Mon, 6 Mar 2023 14:12:12 -0800 Subject: [PATCH 36/77] Refactor tooltip setup as component (#320) Added property to decide whether tooltip to display features or not. This will be required if tooltip is used to draw shape. Signed-off-by: Vijayan Balasubramanian --- common/index.ts | 4 + .../map_container/map_container.tsx | 86 +++---------------- .../components/tooltip/display_features.tsx | 82 ++++++++++++++++++ 3 files changed, 97 insertions(+), 75 deletions(-) create mode 100644 public/components/tooltip/display_features.tsx diff --git a/common/index.ts b/common/index.ts index e4bb2769..4d5d7206 100644 --- a/common/index.ts +++ b/common/index.ts @@ -142,3 +142,7 @@ export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: 'document', [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe', }; + +export enum TOOLTIP_STATE { + DISPLAY_FEATURES = 'DISPLAY_FEATURES', +} diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index fada3930..565ac12f 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -4,26 +4,20 @@ */ import React, { useEffect, useRef, useState } from 'react'; -import { Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; +import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; -import { MAP_INITIAL_STATE, DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, MAP_INITIAL_STATE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { + Filter, IndexPattern, + Query, RefreshInterval, TimeRange, - Filter, - Query, } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; -import { - createPopup, - getPopupLocation, - isTooltipEnabledLayer, - isTooltipEnabledOnHover, -} from '../tooltip/create_tooltip'; import { handleDataLayerRender, handleReferenceLayerRender, @@ -39,6 +33,8 @@ import { referenceLayerTypeLookup, } from '../../model/layersFunctions'; import { MapsFooter } from './maps_footer'; +import { DisplayFeatures } from '../tooltip/display_features'; +import { TOOLTIP_STATE } from '../../../common/index'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -80,6 +76,8 @@ export const MapContainer = ({ const [selectedLayerConfig, setSelectedLayerConfig] = useState< MapLayerSpecification | undefined >(); + // start with display feature + const [tooltipState, setTooltipState] = useState(TOOLTIP_STATE.DISPLAY_FEATURES); useEffect(() => { if (!mapContainer.current) return; @@ -129,71 +127,6 @@ export const MapContainer = ({ }; }, []); - // Create onClick tooltip for each layer features that has tooltip enabled - useEffect(() => { - let clickPopup: Popup | null = null; - let hoverPopup: Popup | null = null; - - // We don't want to show layer information in the popup for the map tile layer - const tooltipEnabledLayers = layers.filter(isTooltipEnabledLayer); - - function onClickMap(e: MapEventType['click']) { - // remove previous popup - clickPopup?.remove(); - - const features = maplibreRef.current?.queryRenderedFeatures(e.point); - if (features && maplibreRef.current) { - clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); - clickPopup - ?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)) - .addTo(maplibreRef.current); - } - } - - function onMouseMoveMap(e: MapEventType['mousemove']) { - // remove previous popup - hoverPopup?.remove(); - - const tooltipEnabledLayersOnHover = layers.filter(isTooltipEnabledOnHover); - const features = maplibreRef.current?.queryRenderedFeatures(e.point); - if (features && maplibreRef.current) { - hoverPopup = createPopup({ - features, - layers: tooltipEnabledLayersOnHover, - // enable close button to avoid occasional dangling tooltip that is not cleared during mouse leave action - showCloseButton: true, - showPagination: false, - showLayerSelection: false, - }); - hoverPopup - ?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)) - .addTo(maplibreRef.current); - } - } - - if (maplibreRef.current) { - const map = maplibreRef.current; - map.on('click', onClickMap); - // reset cursor to default when user is no longer hovering over a clickable feature - map.on('mouseleave', () => { - map.getCanvas().style.cursor = ''; - hoverPopup?.remove(); - }); - map.on('mouseenter', () => { - map.getCanvas().style.cursor = 'pointer'; - }); - // add tooltip when users mouse move over a point - map.on('mousemove', onMouseMoveMap); - } - - return () => { - if (maplibreRef.current) { - maplibreRef.current.off('click', onClickMap); - maplibreRef.current.off('mousemove', onMouseMoveMap); - } - }; - }, [layers]); - // Handle map bounding box change, it should update the search if "request data around map extent" was enabled useEffect(() => { function renderLayers() { @@ -317,6 +250,9 @@ export const MapContainer = ({ setIsUpdatingLayerRender={setIsUpdatingLayerRender} /> )} + {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && ( + + )}
); diff --git a/public/components/tooltip/display_features.tsx b/public/components/tooltip/display_features.tsx new file mode 100644 index 00000000..5e389be9 --- /dev/null +++ b/public/components/tooltip/display_features.tsx @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre, MapEventType, Popup } from 'maplibre-gl'; +import React, { memo, useEffect, Fragment } from 'react'; +import { + createPopup, + getPopupLocation, + isTooltipEnabledLayer, + isTooltipEnabledOnHover, +} from './create_tooltip'; +import { MapLayerSpecification } from '../../model/mapLayerType'; + +interface Props { + map: Maplibre; + layers: MapLayerSpecification[]; +} + +export const DisplayFeatures = memo(({ map, layers }: Props) => { + useEffect(() => { + let clickPopup: Popup | null = null; + let hoverPopup: Popup | null = null; + + // We don't want to show layer information in the popup for the map tile layer + const tooltipEnabledLayers = layers.filter(isTooltipEnabledLayer); + + function onClickMap(e: MapEventType['click']) { + // remove previous popup + clickPopup?.remove(); + + const features = map.queryRenderedFeatures(e.point); + if (features && map) { + clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); + clickPopup?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)).addTo(map); + } + } + + function onMouseMoveMap(e: MapEventType['mousemove']) { + // remove previous popup + hoverPopup?.remove(); + + const tooltipEnabledLayersOnHover = layers.filter(isTooltipEnabledOnHover); + const features = map.queryRenderedFeatures(e.point); + if (features && map) { + hoverPopup = createPopup({ + features, + layers: tooltipEnabledLayersOnHover, + // enable close button to avoid occasional dangling tooltip that is not cleared during mouse leave action + showCloseButton: true, + showPagination: false, + showLayerSelection: false, + }); + hoverPopup?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)).addTo(map); + } + } + + if (map) { + map.on('click', onClickMap); + // reset cursor to default when user is no longer hovering over a clickable feature + map.on('mouseleave', () => { + map.getCanvas().style.cursor = ''; + hoverPopup?.remove(); + }); + map.on('mouseenter', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + // add tooltip when users mouse move over a point + map.on('mousemove', onMouseMoveMap); + } + + return () => { + if (map) { + map.off('click', onClickMap); + map.off('mousemove', onMouseMoveMap); + } + }; + }, [layers]); + + return ; +}); From 9fffd01c5b8149cef0ae4c7ed102f32011086c70 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 8 Mar 2023 09:30:51 -0800 Subject: [PATCH 37/77] Add shape filter UI button (#329) * Add spatial filter toolbar This will add polygon button which provides context menu to select parameters like relation, label. Add cancel button if it starts drawing. Signed-off-by: Vijayan Balasubramanian --- common/index.ts | 18 +++ .../layer_config/layer_basic_settings.tsx | 1 - .../map_container/map_container.scss | 7 ++ .../map_container/map_container.tsx | 24 +++- .../filter_by_polygon.test.tsx.snap | 113 ++++++++++++++++++ .../filter_toolbar.test.tsx.snap | 41 +++++++ .../spatial_filter/filter_by_polygon.test.tsx | 23 ++++ .../spatial_filter/filter_by_polygon.tsx | 89 ++++++++++++++ .../filter_input_panel.test.tsx | 49 ++++++++ .../spatial_filter/filter_input_panel.tsx | 94 +++++++++++++++ .../spatial_filter/filter_toolbar.test.tsx | 24 ++++ .../toolbar/spatial_filter/filter_toolbar.tsx | 47 ++++++++ .../spatial_filter/spatial_filter.scss | 21 ++++ public/images/polygon.svg | 1 + public/model/mapLayerType.ts | 31 ++--- 15 files changed, 557 insertions(+), 26 deletions(-) create mode 100644 public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap create mode 100644 public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap create mode 100644 public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx create mode 100644 public/components/toolbar/spatial_filter/filter_by_polygon.tsx create mode 100644 public/components/toolbar/spatial_filter/filter_input_panel.test.tsx create mode 100644 public/components/toolbar/spatial_filter/filter_input_panel.tsx create mode 100644 public/components/toolbar/spatial_filter/filter_toolbar.test.tsx create mode 100644 public/components/toolbar/spatial_filter/filter_toolbar.tsx create mode 100644 public/components/toolbar/spatial_filter/spatial_filter.scss create mode 100644 public/images/polygon.svg diff --git a/common/index.ts b/common/index.ts index 4d5d7206..6086d5c5 100644 --- a/common/index.ts +++ b/common/index.ts @@ -145,4 +145,22 @@ export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = { export enum TOOLTIP_STATE { DISPLAY_FEATURES = 'DISPLAY_FEATURES', + FILTER_DRAW_SHAPE = 'FILTER_DRAW_SHAPE', } + +export enum FILTER_DRAW_MODE { + NONE = 'none', // draw filter is inactive + POLYGON = 'polygon', // Filter is active and set to draw polygon +} + +export interface DrawFilterProperties { + relation?: string; + mode: FILTER_DRAW_MODE; + filterLabel?: string; +} + +export const DRAW_FILTER_SHAPE_TITLE = 'DRAW SHAPE'; +export const DRAW_FILTER_POLYGON_DEFAULT_LABEL = 'polygon'; +export const DRAW_FILTER_POLYGON = 'Draw Polygon'; +export const DRAW_FILTER_POLYGON_RELATIONS = ['intersects', 'disjoint', 'within']; + diff --git a/public/components/layer_config/layer_basic_settings.tsx b/public/components/layer_config/layer_basic_settings.tsx index e226853c..2dbbffc6 100644 --- a/public/components/layer_config/layer_basic_settings.tsx +++ b/public/components/layer_config/layer_basic_settings.tsx @@ -25,7 +25,6 @@ import { MAP_LAYER_DEFAULT_OPACITY_STEP, MAX_LAYER_NAME_LIMIT, } from '../../../common'; -import { layersTypeNameMap } from '../../model/layersFunctions'; interface Props { selectedLayerConfig: MapLayerSpecification; diff --git a/public/components/map_container/map_container.scss b/public/components/map_container/map_container.scss index a4ee3065..d659b5a9 100644 --- a/public/components/map_container/map_container.scss +++ b/public/components/map_container/map_container.scss @@ -30,3 +30,10 @@ bottom: $euiSizeM; right: $euiSizeS; } + +.SpatialFilterToolbar-container { + z-index: 1; + position: absolute; + top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 5); + right: $euiSizeS; +} diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 565ac12f..8d510124 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -8,7 +8,7 @@ import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; -import { DASHBOARDS_MAPS_LAYER_TYPE, MAP_INITIAL_STATE } from '../../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { Filter, @@ -34,7 +34,8 @@ import { } from '../../model/layersFunctions'; import { MapsFooter } from './maps_footer'; import { DisplayFeatures } from '../tooltip/display_features'; -import { TOOLTIP_STATE } from '../../../common/index'; +import { TOOLTIP_STATE } from '../../../common'; +import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -78,6 +79,9 @@ export const MapContainer = ({ >(); // start with display feature const [tooltipState, setTooltipState] = useState(TOOLTIP_STATE.DISPLAY_FEATURES); + const [filterProperties, setFilterProperties] = useState({ + mode: FILTER_DRAW_MODE.NONE, + }); useEffect(() => { if (!mapContainer.current) return; @@ -212,6 +216,14 @@ export const MapContainer = ({ } }, [layers, mounted, timeRange, filters, query, mapState, isReadOnlyMode]); + useEffect(() => { + const currentTooltipState: TOOLTIP_STATE = + filterProperties?.mode === FILTER_DRAW_MODE.NONE + ? TOOLTIP_STATE.DISPLAY_FEATURES + : TOOLTIP_STATE.FILTER_DRAW_SHAPE; + setTooltipState(currentTooltipState); + }, [filterProperties]); + const updateIndexPatterns = async () => { if (!selectedLayerConfig) { return; @@ -253,6 +265,14 @@ export const MapContainer = ({ {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && ( )} +
+ {mounted && ( + + )} +
); diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap new file mode 100644 index 00000000..13cc521c --- /dev/null +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders filter by polygon button 1`] = ` + + + + } + closePopover={[Function]} + data-test-subj="drawPolygonPopOver" + display="inlineBlock" + hasArrow={true} + id="drawPolygonId" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "id": 0, + "title": "DRAW SHAPE", + }, + ] + } + size="m" + /> + +`; + +exports[`renders filter by polygon in middle of drawing 1`] = ` + + + + } + closePopover={[Function]} + data-test-subj="drawPolygonPopOver" + display="inlineBlock" + hasArrow={true} + id="drawPolygonId" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "id": 0, + "title": "DRAW SHAPE", + }, + ] + } + size="m" + /> + +`; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap new file mode 100644 index 00000000..fbafe8dd --- /dev/null +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders spatial filter before drawing 1`] = ` + + + + + +`; + +exports[`renders spatial filter while drawing 1`] = ` + + + + Cancel + + + + + + +`; diff --git a/public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx b/public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx new file mode 100644 index 00000000..4d00f984 --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { shallow } from 'enzyme'; +import React from 'react'; +import { FilterByPolygon } from './filter_by_polygon'; + +it('renders filter by polygon button', () => { + const mockCallback = jest.fn(); + const polygonComponent = shallow( + + ); + expect(polygonComponent).toMatchSnapshot(); +}); + +it('renders filter by polygon in middle of drawing', () => { + const mockCallback = jest.fn(); + const polygonComponent = shallow( + + ); + expect(polygonComponent).toMatchSnapshot(); +}); diff --git a/public/components/toolbar/spatial_filter/filter_by_polygon.tsx b/public/components/toolbar/spatial_filter/filter_by_polygon.tsx new file mode 100644 index 00000000..6ef49f2c --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_by_polygon.tsx @@ -0,0 +1,89 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiContextMenu, EuiPanel, EuiButtonIcon } from '@elastic/eui'; +import { FilterInputPanel } from './filter_input_panel'; +import polygon from '../../../images/polygon.svg'; +import { + DrawFilterProperties, + DRAW_FILTER_POLYGON, + DRAW_FILTER_POLYGON_DEFAULT_LABEL, + DRAW_FILTER_POLYGON_RELATIONS, + DRAW_FILTER_SHAPE_TITLE, +} from '../../../../common'; +import { FILTER_DRAW_MODE } from '../../../../common'; + +interface FilterByPolygonProps { + setDrawFilterProperties: (properties: DrawFilterProperties) => void; + isDrawActive: boolean; +} + +export const FilterByPolygon = ({ + setDrawFilterProperties, + isDrawActive, +}: FilterByPolygonProps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const onSubmit = (input: { relation: string; label: string; mode: FILTER_DRAW_MODE }) => { + setDrawFilterProperties({ + mode: input.mode, + relation: input.relation, + filterLabel: input.label, + }); + closePopover(); + }; + + const panels = [ + { + id: 0, + title: DRAW_FILTER_SHAPE_TITLE, + content: ( + + ), + }, + ]; + + const drawPolygonButton = ( + + + + ); + return ( + + + + ); +}; diff --git a/public/components/toolbar/spatial_filter/filter_input_panel.test.tsx b/public/components/toolbar/spatial_filter/filter_input_panel.test.tsx new file mode 100644 index 00000000..2d9fab2a --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_input_panel.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiButton, EuiForm, EuiFormRow, EuiPanel, EuiSelect } from '@elastic/eui'; + +import { FilterInputPanel } from './filter_input_panel'; +import { FILTER_DRAW_MODE } from '../../../../common'; +import { shallow } from 'enzyme'; + +it('renders filter input panel', () => { + const mockRelations: string[] = ['sample', 'list', 'of', 'relations']; + const mockCallback = jest.fn(); + const wrapper = shallow( + + ); + + const panel = wrapper.find(EuiPanel); + expect(panel.find(EuiForm).find(EuiFormRow).length).toEqual(3); + wrapper.find(EuiButton).simulate('click', {}); + expect(mockCallback).toHaveBeenCalledTimes(1); + const actualRelations = wrapper.find(EuiSelect).prop('options'); + expect(actualRelations).toEqual([ + { + value: 'sample', + text: 'sample', + }, + { + value: 'list', + text: 'list', + }, + { + value: 'of', + text: 'of', + }, + { + value: 'relations', + text: 'relations', + }, + ]); +}); diff --git a/public/components/toolbar/spatial_filter/filter_input_panel.tsx b/public/components/toolbar/spatial_filter/filter_input_panel.tsx new file mode 100644 index 00000000..1842bf87 --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_input_panel.tsx @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiFieldText, EuiForm, EuiFormRow, EuiPanel, EuiSelect } from '@elastic/eui'; +import { FILTER_DRAW_MODE } from '../../../../common'; + +const getSpatialRelationshipItems = ( + relations: string[] +): Array<{ value: string; text: string }> => { + return relations.map((relation) => { + return { + value: relation, + text: relation, + }; + }); +}; + +export interface FilterInputProps { + drawLabel: string; + defaultFilterLabel: string; + mode: FILTER_DRAW_MODE; + readonly relations: string[]; + onSubmit: ({ + relation, + label, + mode, + }: { + relation: string; + label: string; + mode: FILTER_DRAW_MODE; + }) => void; +} + +export const FilterInputPanel = ({ + drawLabel, + defaultFilterLabel, + mode, + relations, + onSubmit, +}: FilterInputProps) => { + const [filterLabel, setFilterLabel] = useState(defaultFilterLabel); + const [spatialRelation, setSpatialRelation] = useState(relations[0]); + + const updateSpatialFilterProperties = () => { + onSubmit({ + relation: spatialRelation, + label: filterLabel, + mode, + }); + }; + + return ( + + + + { + setFilterLabel(event.target.value); + }} + compressed + /> + + + + { + setSpatialRelation(event.target.value); + }} + value={spatialRelation} + compressed + /> + + + + {drawLabel} + + + + + ); +}; diff --git a/public/components/toolbar/spatial_filter/filter_toolbar.test.tsx b/public/components/toolbar/spatial_filter/filter_toolbar.test.tsx new file mode 100644 index 00000000..02154635 --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_toolbar.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { SpatialFilterToolbar } from './filter_toolbar'; + +it('renders spatial filter before drawing', () => { + const mockCallback = jest.fn(); + const toolbarComponent = shallow( + + ); + expect(toolbarComponent).toMatchSnapshot(); +}); + +it('renders spatial filter while drawing', () => { + const mockCallback = jest.fn(); + const toolbarComponent = shallow( + + ); + expect(toolbarComponent).toMatchSnapshot(); +}); diff --git a/public/components/toolbar/spatial_filter/filter_toolbar.tsx b/public/components/toolbar/spatial_filter/filter_toolbar.tsx new file mode 100644 index 00000000..9fc6bbaa --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_toolbar.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FilterByPolygon } from './filter_by_polygon'; +import { FILTER_DRAW_MODE, DrawFilterProperties } from '../../../../common'; + +interface SpatialFilterToolBarProps { + setFilterProperties: (properties: DrawFilterProperties) => void; + isDrawActive: boolean; +} + +export const SpatialFilterToolbar = ({ + setFilterProperties, + isDrawActive, +}: SpatialFilterToolBarProps) => { + const onCancel = () => { + setFilterProperties({ + mode: FILTER_DRAW_MODE.NONE, + }); + }; + const filterIconGroups = ( + + + + ); + if (isDrawActive) { + return ( + + + + {'Cancel'} + + + {filterIconGroups} + + ); + } + return ( + + {filterIconGroups} + + ); +}; diff --git a/public/components/toolbar/spatial_filter/spatial_filter.scss b/public/components/toolbar/spatial_filter/spatial_filter.scss new file mode 100644 index 00000000..0025f923 --- /dev/null +++ b/public/components/toolbar/spatial_filter/spatial_filter.scss @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.spatialFilterToolbar__shape{ + position: relative; + + .euiButtonIcon { + border-color:rgba(0,0,0,0.9); + color:rgba(255,255,255,0.5); + width:30px; + height:30px; + } +} +.spatialFilterGroup__popoverPanel{ + max-width: 300px; + border: hidden; +} + + diff --git a/public/images/polygon.svg b/public/images/polygon.svg new file mode 100644 index 00000000..281b7b05 --- /dev/null +++ b/public/images/polygon.svg @@ -0,0 +1 @@ + image/svg+xml \ No newline at end of file diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index 07832662..d5ed0990 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -11,14 +11,17 @@ export type MapLayerSpecification = | DocumentLayerSpecification | CustomLayerSpecification; -export type OSMLayerSpecification = { +export type AbstractLayerSpecification = { name: string; id: string; - type: 'opensearch_vector_tile_map'; description: string; zoomRange: number[]; opacity: number; visibility: string; +}; + +export type OSMLayerSpecification = AbstractLayerSpecification & { + type: 'opensearch_vector_tile_map'; source: { dataURL: string; }; @@ -27,14 +30,8 @@ export type OSMLayerSpecification = { }; }; -export type DocumentLayerSpecification = { - name: string; - id: string; +export type DocumentLayerSpecification = AbstractLayerSpecification & { type: 'documents'; - description: string; - zoomRange: number[]; - opacity: number; - visibility: string; source: { indexPatternRefName: string; indexPatternId: string; @@ -66,14 +63,8 @@ export type DocumentLayerSpecification = { export type CustomLayerSpecification = CustomTMSLayerSpecification | CustomWMSLayerSpecification; -export type CustomTMSLayerSpecification = { - name: string; - id: string; +export type CustomTMSLayerSpecification = AbstractLayerSpecification & { type: 'custom_map'; - description: string; - zoomRange: number[]; - opacity: number; - visibility: string; source: { url: string; customType: 'tms'; @@ -81,14 +72,8 @@ export type CustomTMSLayerSpecification = { }; }; -export type CustomWMSLayerSpecification = { - name: string; - id: string; +export type CustomWMSLayerSpecification = AbstractLayerSpecification & { type: 'custom_map'; - description: string; - zoomRange: number[]; - opacity: number; - visibility: string; source: { url: string; customType: 'wms'; From 435f4e6f5875f7da75a305eacd551fa31c1e0011 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 9 Mar 2023 03:02:27 +0800 Subject: [PATCH 38/77] fix: layer filter setting been reset unexpectedly (#327) This is due to the inappropriate in-memory cache key been used to cache user selection of filters and geo filter selection. It was using the index pattern id as the cache key, but different layer may have the same index pattern selected. To fix the issue, we changed to use layerConfig id + index pattern id as the cache key Fixed #323 Signed-off-by: Yulong Ruan --- .../document_layer_source.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index af45bde0..baad3c34 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -62,6 +62,7 @@ export const DocumentLayerSource = ({ selectedLayerConfig.source.showTooltips ); const memorizedForm = useRef({}); + const cacheKey = `${selectedLayerConfig.id}/${indexPattern?.id}`; const geoFields = useMemo(() => { const acceptedFieldTypes = ['geo_point', 'geo_shape']; @@ -74,13 +75,9 @@ export const DocumentLayerSource = ({ // We want to memorize the filters and geoField selection when a map layer config is opened useEffect(() => { - if ( - indexPattern && - indexPattern.id && - indexPattern.id === selectedLayerConfig.source.indexPatternId - ) { - if (!memorizedForm.current[indexPattern.id]) { - memorizedForm.current[indexPattern.id] = { + if (indexPattern?.id && indexPattern.id === selectedLayerConfig.source.indexPatternId) { + if (!memorizedForm.current[cacheKey]) { + memorizedForm.current[cacheKey] = { filters: selectedLayerConfig.source.filters, geoField: selectedField, }; @@ -102,8 +99,8 @@ export const DocumentLayerSource = ({ // We'd like to memorize the geo field selection so that the selection // can be restored when changing index pattern back and forth if (indexPattern?.id) { - memorizedForm.current[indexPattern.id] = { - ...memorizedForm.current[indexPattern.id], + memorizedForm.current[cacheKey] = { + ...memorizedForm.current[cacheKey], geoField: field, }; } @@ -193,11 +190,11 @@ export const DocumentLayerSource = ({ ...selectedLayerConfig, source: { ...selectedLayerConfig.source, filters }, }); - // We'd like to memorize the fields selection so that the selection + // We'd like to memorize the filter selection so that the selection // can be restored when changing index pattern back and forth if (indexPattern?.id) { - memorizedForm.current[indexPattern.id] = { - ...memorizedForm.current[indexPattern.id], + memorizedForm.current[cacheKey] = { + ...memorizedForm.current[cacheKey], filters, }; } @@ -225,13 +222,11 @@ export const DocumentLayerSource = ({ source.indexPatternId = indexPattern.id ?? ''; source.indexPatternRefName = indexPattern.title; // Use memorized filters, otherwise, set filter selection to empty - const filters = indexPattern.id ? memorizedForm.current[indexPattern.id]?.filters ?? [] : []; + const filters = indexPattern.id ? memorizedForm.current[cacheKey]?.filters ?? [] : []; source.filters = filters; // Use memorized geo field, otherwise, set geo filter to empty - const geoField = indexPattern.id - ? memorizedForm.current[indexPattern.id]?.geoField - : undefined; + const geoField = indexPattern.id ? memorizedForm.current[cacheKey]?.geoField : undefined; if (geoField) { source.geoFieldName = geoField.displayName; source.geoFieldType = geoField.type as 'geo_point' | 'geo_shape'; @@ -250,7 +245,9 @@ export const DocumentLayerSource = ({ ); }, [selectedLayerConfig.source.documentRequestNumber]); - const onEnableTooltipsChange = (event: { target: { checked: React.SetStateAction } }) => { + const onEnableTooltipsChange = (event: { + target: { checked: React.SetStateAction }; + }) => { setEnableTooltips(event.target.checked); const source = { ...selectedLayerConfig.source, showTooltips: event.target.checked }; setSelectedLayerConfig({ ...selectedLayerConfig, source }); From d0f9e7b4fb6e3d841d4f825576ae5a80d88aec7c Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 8 Mar 2023 11:29:46 -0800 Subject: [PATCH 39/77] Update max supported layer count (#332) Since layer panel has scroll bar, we can allow user to add up to 100. Later, this will be moved to map settings. Signed-off-by: Vijayan Balasubramanian --- common/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/index.ts b/common/index.ts index 6086d5c5..9a8e2ea2 100644 --- a/common/index.ts +++ b/common/index.ts @@ -57,7 +57,7 @@ export const MAP_LAYER_DEFAULT_NAME = 'Default map'; export const MAP_LAYER_DEFAULT_OPACITY_STEP = 1; export const MAP_REFERENCE_LAYER_DEFAULT_OPACITY = 100; // Make this configurable from map settings -export const MAX_LAYER_LIMIT = 20; +export const MAX_LAYER_LIMIT = 100; export const MAX_LAYER_NAME_LIMIT = 35; export const MAX_LONGITUDE = 180; export const MIN_LONGITUDE = -180; From 45771cc04da66e8fc0ca9994af74baf1e9f0e206 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 8 Mar 2023 11:39:35 -0800 Subject: [PATCH 40/77] Refactor get filed options and add field label option on UI and (#328) Signed-off-by: Junqiu Lei --- common/index.ts | 1 - .../document_layer_config_panel.tsx | 7 +- .../document_layer_source.tsx | 105 ++----- .../style/document_layer_style.tsx | 4 + .../documents_config/style/label_config.scss | 3 + .../style/label_config.test.tsx | 91 ++++-- .../documents_config/style/label_config.tsx | 284 ++++++++---------- public/model/map/layer_operations.ts | 2 +- public/model/mapLayerType.ts | 5 +- public/utils/fields_options.test.ts | 80 +++++ public/utils/fields_options.ts | 54 ++++ public/utils/getIntialConfig.ts | 6 +- 12 files changed, 368 insertions(+), 274 deletions(-) create mode 100644 public/components/layer_config/documents_config/style/label_config.scss create mode 100644 public/utils/fields_options.test.ts create mode 100644 public/utils/fields_options.ts diff --git a/common/index.ts b/common/index.ts index 9a8e2ea2..49d3f6c2 100644 --- a/common/index.ts +++ b/common/index.ts @@ -34,7 +34,6 @@ export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; export const DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER: boolean = true; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; export const DOCUMENTS_DEFAULT_LABEL_ENABLES: boolean = false; -export const DOCUMENTS_DEFAULT_LABEL_VALUE: string = ''; export const DOCUMENTS_DEFAULT_LABEL_TYPE: string = 'fixed'; export const DOCUMENTS_DEFAULT_LABEL_SIZE: number = 20; export const DOCUMENTS_MIN_LABEL_SIZE: number = 1; diff --git a/public/components/layer_config/documents_config/document_layer_config_panel.tsx b/public/components/layer_config/documents_config/document_layer_config_panel.tsx index e7f30c75..f2eb86ea 100644 --- a/public/components/layer_config/documents_config/document_layer_config_panel.tsx +++ b/public/components/layer_config/documents_config/document_layer_config_panel.tsx @@ -3,12 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; import { DocumentLayerSpecification } from '../../../model/mapLayerType'; import { LayerBasicSettings } from '../layer_basic_settings'; import { DocumentLayerSource } from './document_layer_source'; import { DocumentLayerStyle } from './style/document_layer_style'; +import { IndexPattern } from '../../../../../../src/plugins/data/common'; interface Props { selectedLayerConfig: DocumentLayerSpecification; @@ -40,9 +41,13 @@ export const DocumentLayerConfigPanel = (props: Props) => { props.setIsUpdateDisabled(check(selectedLayerConfig, checkKeys) || isUpdateDisabled); }; + const [indexPattern, setIndexPattern] = useState(); + const newProps = { ...props, setIsUpdateDisabled, + indexPattern, + setIndexPattern, }; const tabs = [ diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index baad3c34..e729c679 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -21,16 +21,22 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; -import _, { Dictionary } from 'lodash'; import { Filter, IndexPattern, IndexPatternField } from '../../../../../../src/plugins/data/public'; import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../../types'; import { DocumentLayerSpecification } from '../../../model/mapLayerType'; +import { + formatFieldStringToComboBox, + formatFieldsStringToComboBox, + getFieldsOptions, +} from '../../../utils/fields_options'; interface Props { setSelectedLayerConfig: Function; selectedLayerConfig: DocumentLayerSpecification; setIsUpdateDisabled: Function; + indexPattern: IndexPattern | null | undefined; + setIndexPattern: Function; } interface MemorizedForm { @@ -46,6 +52,8 @@ export const DocumentLayerSource = ({ setSelectedLayerConfig, selectedLayerConfig, setIsUpdateDisabled, + indexPattern, + setIndexPattern, }: Props) => { const { services: { @@ -56,16 +64,15 @@ export const DocumentLayerSource = ({ }, }, } = useOpenSearchDashboards(); - const [indexPattern, setIndexPattern] = useState(); const [hasInvalidRequestNumber, setHasInvalidRequestNumber] = useState(false); const [enableTooltips, setEnableTooltips] = useState( selectedLayerConfig.source.showTooltips ); const memorizedForm = useRef({}); + const acceptedFieldTypes = ['geo_point', 'geo_shape']; const cacheKey = `${selectedLayerConfig.id}/${indexPattern?.id}`; const geoFields = useMemo(() => { - const acceptedFieldTypes = ['geo_point', 'geo_shape']; return indexPattern?.fields.filter((field) => acceptedFieldTypes.indexOf(field.type) !== -1); }, [indexPattern]); @@ -87,23 +94,21 @@ export const DocumentLayerSource = ({ const onGeoFieldChange = useCallback( (field: IndexPatternField | null) => { - if (field) { - setSelectedLayerConfig({ - ...selectedLayerConfig, - source: { - ...selectedLayerConfig.source, - geoFieldName: field.displayName, - geoFieldType: field.type, - }, - }); - // We'd like to memorize the geo field selection so that the selection - // can be restored when changing index pattern back and forth - if (indexPattern?.id) { - memorizedForm.current[cacheKey] = { - ...memorizedForm.current[cacheKey], - geoField: field, - }; - } + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig.source, + geoFieldName: field?.displayName || undefined, + geoFieldType: field?.type || undefined, + }, + }); + // We'd like to memorize the geo field selection so that the selection + // can be restored when changing index pattern back and forth + if (indexPattern?.id) { + memorizedForm.current[cacheKey] = { + ...memorizedForm.current[cacheKey], + geoField: field || undefined, + }; } }, [selectedLayerConfig, setSelectedLayerConfig, indexPattern] @@ -119,55 +124,6 @@ export const DocumentLayerSource = ({ setIsUpdateDisabled(disableUpdate); }, [setIsUpdateDisabled, indexPattern, selectedField, hasInvalidRequestNumber]); - const formatFieldToComboBox = (field?: IndexPatternField | null) => { - if (!field) return []; - return formatFieldsToComboBox([field]); - }; - - const formatFieldsToComboBox = (fields?: IndexPatternField[]) => { - if (!fields) return []; - - return fields?.map((field) => { - return { - label: field.displayName || field.name, - }; - }); - }; - - const tooltipFieldsOptions = () => { - const fieldList = indexPattern?.fields; - if (!fieldList) return []; - const fieldTypeMap: Dictionary = _.groupBy( - fieldList, - (field) => field.type - ); - - const fieldOptions: Array<{ label: string; options: Array<{ label: string }> }> = []; - let fieldsOfSameType: Array<{ label: string }> = []; - - Object.entries(fieldTypeMap).forEach(([fieldType, fieldEntries]) => { - for (const field of fieldEntries) { - fieldsOfSameType.push({ label: `${field.displayName || field.name}` }); - } - fieldOptions.push({ - label: `${fieldType}`, - options: fieldsOfSameType, - }); - fieldsOfSameType = []; - }); - return fieldOptions; - }; - - const formatTooltipFieldsToComboBox = (fields: string[]) => { - if (!fields) return []; - - return fields?.map((field) => { - return { - label: field, - }; - }); - }; - const onDocumentRequestNumberChange = (e: React.ChangeEvent) => { const value = e.target.value; const selectedNumber = parseInt(value, 10); @@ -320,11 +276,11 @@ export const DocumentLayerSource = ({ fullWidth={true} > { - const field = indexPattern?.getFieldByName(option[0].label); + const field = indexPattern?.getFieldByName(option[0]?.label); onGeoFieldChange(field || null); }} sortMatchesBy="startsWith" @@ -333,6 +289,7 @@ export const DocumentLayerSource = ({ })} data-test-subj={'geoFieldSelect'} fullWidth={true} + isClearable={false} />
@@ -411,8 +368,8 @@ export const DocumentLayerSource = ({ Tooltip fields { const [hasInvalidThickness, setHasInvalidThickness] = useState(false); const [hasInvalidSize, setHasInvalidSize] = useState(false); @@ -281,6 +284,7 @@ export const DocumentLayerStyle = ({ selectedLayerConfig={selectedLayerConfig} setSelectedLayerConfig={setSelectedLayerConfig} setIsUpdateDisabled={setIsUpdateDisabled} + indexPattern={indexPattern} /> diff --git a/public/components/layer_config/documents_config/style/label_config.scss b/public/components/layer_config/documents_config/style/label_config.scss new file mode 100644 index 00000000..7902d9c5 --- /dev/null +++ b/public/components/layer_config/documents_config/style/label_config.scss @@ -0,0 +1,3 @@ +.documentsLabel__title { + overflow: hidden +} diff --git a/public/components/layer_config/documents_config/style/label_config.test.tsx b/public/components/layer_config/documents_config/style/label_config.test.tsx index 90f6b077..e0bf09ae 100644 --- a/public/components/layer_config/documents_config/style/label_config.test.tsx +++ b/public/components/layer_config/documents_config/style/label_config.test.tsx @@ -9,6 +9,7 @@ import { DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, } from '../../../../../common'; import { DocumentLayerSpecification } from '../../../../model/mapLayerType'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common'; describe('LabelConfig', () => { let wrapper: ShallowWrapper; @@ -16,7 +17,7 @@ describe('LabelConfig', () => { const setSelectedLayerConfigMock = jest.fn(); const setIsUpdateDisabledMock = jest.fn(); - const selectedLayerConfig: DocumentLayerSpecification = { + const mockLayerConfig: DocumentLayerSpecification = { name: 'My Document Layer', id: 'document-layer-1', type: 'documents', @@ -42,8 +43,9 @@ describe('LabelConfig', () => { markerSize: 10, label: { enabled: true, - tittle: 'My Label', - tittleType: 'fixed', + titleByFixed: 'My Label', + titleByField: 'field1', + titleType: 'fixed', color: '#FFFFFF', size: 12, borderColor: '#000000', @@ -52,12 +54,28 @@ describe('LabelConfig', () => { }, }; + const mockIndexPattern = { + fields: [ + { + name: 'field1', + displayName: 'Field 1', + type: 'text', + }, + { + name: 'field2', + displayName: 'Field 2', + type: 'geo_point', + }, + ], + } as IndexPattern; + beforeEach(() => { wrapper = shallow( ); }); @@ -72,11 +90,11 @@ describe('LabelConfig', () => { const checkbox = wrapper.find(EuiCheckbox); checkbox.simulate('change', { target: { checked: false } }); expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ - ...selectedLayerConfig, + ...mockLayerConfig, style: { - ...selectedLayerConfig.style, + ...mockLayerConfig.style, label: { - ...selectedLayerConfig.style.label, + ...mockLayerConfig.style.label, enabled: false, }, }, @@ -85,11 +103,11 @@ describe('LabelConfig', () => { it('should render EuiCheckbox with correct props when enableLabel is false', () => { const newSelectedLayerConfig = { - ...selectedLayerConfig, + ...mockLayerConfig, style: { - ...selectedLayerConfig.style, + ...mockLayerConfig.style, label: { - ...selectedLayerConfig.style.label, + ...mockLayerConfig.style.label, enabled: false, }, }, @@ -99,37 +117,54 @@ describe('LabelConfig', () => { expect(checkbox.prop('checked')).toEqual(false); }); - it('should call setSelectedLayerConfig with updated config when onChangeLabelTittleType is called', () => { + it('should call setSelectedLayerConfig with updated config when onChangeLabelTitleType is called', () => { const select = wrapper.find('EuiSelect').at(0); select.simulate('change', { target: { value: 'by_field' } }); expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ - ...selectedLayerConfig, + ...mockLayerConfig, style: { - ...selectedLayerConfig.style, + ...mockLayerConfig.style, label: { - ...selectedLayerConfig.style.label, - tittleType: 'by_field', + ...mockLayerConfig.style.label, + titleType: 'by_field', }, }, }); }); - it('should render EuiFieldText with correct props when labelTittleType is "fixed"', () => { + it('should render EuiFieldText with correct props when labelTitleType is "fixed"', () => { const fieldText = wrapper.find('EuiFieldText'); expect(fieldText.prop('value')).toEqual('My Label'); expect(fieldText.prop('disabled')).toEqual(false); }); + it('should render EuiComboBox with correct props when labelTitleType is "By field', () => { + const newSelectedLayerConfig = { + ...mockLayerConfig, + style: { + ...mockLayerConfig.style, + label: { + ...mockLayerConfig.style.label, + titleType: 'by_field', + titleByField: 'Field 1', + }, + }, + }; + wrapper.setProps({ selectedLayerConfig: newSelectedLayerConfig }); + const fieldText = wrapper.find('EuiComboBox'); + expect(fieldText.prop('selectedOptions')).toEqual([{ label: 'Field 1' }]); + }); + it('should call setSelectedLayerConfig with updated config when onStaticLabelChange is called', () => { const fieldText = wrapper.find('EuiFieldText'); fieldText.simulate('change', { target: { value: 'new label' } }); expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ - ...selectedLayerConfig, + ...mockLayerConfig, style: { - ...selectedLayerConfig.style, + ...mockLayerConfig.style, label: { - ...selectedLayerConfig.style.label, - tittle: 'new label', + ...mockLayerConfig.style.label, + titleByFixed: 'new label', }, }, }); @@ -146,11 +181,11 @@ describe('LabelConfig', () => { const fieldNumber = wrapper.find('EuiFieldNumber'); fieldNumber.simulate('change', { target: { value: 20 } }); expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ - ...selectedLayerConfig, + ...mockLayerConfig, style: { - ...selectedLayerConfig.style, + ...mockLayerConfig.style, label: { - ...selectedLayerConfig.style.label, + ...mockLayerConfig.style.label, size: 20, }, }, @@ -160,14 +195,14 @@ describe('LabelConfig', () => { it('should render ColorPicker with correct props for label color', () => { const colorPicker = wrapper.find({ label: 'Label color' }); expect(colorPicker.prop('originColor')).toEqual('#FFFFFF'); - expect(colorPicker.prop('selectedLayerConfigId')).toEqual(selectedLayerConfig.id); + expect(colorPicker.prop('selectedLayerConfigId')).toEqual(mockLayerConfig.id); expect(colorPicker.prop('setIsUpdateDisabled')).toEqual(setIsUpdateDisabledMock); }); it('should render ColorPicker with correct props for label border color', () => { const colorPicker = wrapper.find({ label: 'Label border color' }); expect(colorPicker.prop('originColor')).toEqual('#000000'); - expect(colorPicker.prop('selectedLayerConfigId')).toEqual(selectedLayerConfig.id); + expect(colorPicker.prop('selectedLayerConfigId')).toEqual(mockLayerConfig.id); expect(colorPicker.prop('setIsUpdateDisabled')).toEqual(setIsUpdateDisabledMock); }); @@ -188,11 +223,11 @@ describe('LabelConfig', () => { const select = wrapper.find('EuiSelect').at(1); select.simulate('change', { target: { value: 3 } }); expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ - ...selectedLayerConfig, + ...mockLayerConfig, style: { - ...selectedLayerConfig.style, + ...mockLayerConfig.style, label: { - ...selectedLayerConfig.style.label, + ...mockLayerConfig.style.label, borderWidth: 3, }, }, diff --git a/public/components/layer_config/documents_config/style/label_config.tsx b/public/components/layer_config/documents_config/style/label_config.tsx index 44131853..3044fb44 100644 --- a/public/components/layer_config/documents_config/style/label_config.tsx +++ b/public/components/layer_config/documents_config/style/label_config.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo, ChangeEventHandler, ChangeEvent } from 'react'; import { EuiFormRow, EuiFieldText, @@ -13,8 +13,10 @@ import { EuiFlexGroup, EuiFieldNumber, EuiFormLabel, + EuiComboBox, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; +import { EuiComboBoxOptionOption } from '@opensearch-project/oui/src/eui_components/combo_box/types'; import { DocumentLayerSpecification } from '../../../../model/mapLayerType'; import { ColorPicker } from './color_picker'; import { @@ -28,222 +30,180 @@ import { DOCUMENTS_NONE_LABEL_BORDER_WIDTH, DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, } from '../../../../../common'; +import { formatFieldStringToComboBox, getFieldsOptions } from '../../../../utils/fields_options'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import './label_config.scss'; interface LabelProps { selectedLayerConfig: DocumentLayerSpecification; setSelectedLayerConfig: Function; setIsUpdateDisabled: Function; + indexPattern: IndexPattern | null | undefined; } +const labelTitleTypeOptions = [ + { + value: 'fixed', + text: i18n.translate('maps.documents.labelFixedTitle', { defaultMessage: 'Fixed' }), + }, + { + value: 'by_field', + text: i18n.translate('maps.documents.labelByFieldTitle', { defaultMessage: 'By field' }), + }, +]; + +const labelBorderWidthOptions = [ + { + value: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelNoneBorderWidth', { + defaultMessage: 'None', + }), + }, + { + value: DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelSmallBorderWidth', { + defaultMessage: 'Small', + }), + }, + { + value: DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelMediumBorderWidth', { + defaultMessage: 'Medium', + }), + }, + { + value: DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, + text: i18n.translate('maps.documents.labelLargeBorderWidth', { + defaultMessage: 'Large', + }), + }, +]; + export const LabelConfig = ({ selectedLayerConfig, setSelectedLayerConfig, setIsUpdateDisabled, + indexPattern, }: LabelProps) => { - const labelTittleTypeOptions = [ - { - value: 'fixed', - text: i18n.translate('maps.documents.label.fixedTittle', { defaultMessage: 'Fixed' }), - }, - // TODO: add support for using index pattern field as label - ]; - - const labelBorderWidthOptions = [ - { - value: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, - text: i18n.translate('maps.documents.labelNoneBorderWidth', { - defaultMessage: 'None', - }), - }, - { - value: DOCUMENTS_SMALL_LABEL_BORDER_WIDTH, - text: i18n.translate('maps.documents.labelSmallBorderWidth', { - defaultMessage: 'Small', - }), - }, - { - value: DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, - text: i18n.translate('maps.documents.labelMediumBorderWidth', { - defaultMessage: 'Medium', - }), - }, - { - value: DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, - text: i18n.translate('maps.documents.labelLargeBorderWidth', { - defaultMessage: 'Large', - }), - }, - ]; - - const [inValidLabelTittle, setInValidLabelTittle] = React.useState(false); - const [invalidLabelSize, setInvalidLabelSize] = React.useState(false); + const invalidLabelSize = useMemo(() => { + const { size = 0, enabled } = selectedLayerConfig.style?.label || {}; + return enabled && (size < DOCUMENTS_MIN_LABEL_SIZE || size > DOCUMENTS_MAX_LABEL_SIZE); + }, [selectedLayerConfig]); - useEffect(() => { - if (selectedLayerConfig.style?.label?.enabled) { - if (selectedLayerConfig.style.label?.tittleType === 'fixed') { - if (selectedLayerConfig.style.label?.tittle === '') { - setInValidLabelTittle(true); - } else { - setInValidLabelTittle(false); - } - } - if ( - selectedLayerConfig.style?.label?.size < DOCUMENTS_MIN_LABEL_SIZE || - selectedLayerConfig.style?.label?.size > DOCUMENTS_MAX_LABEL_SIZE - ) { - setInvalidLabelSize(true); - } else { - setInvalidLabelSize(false); - } - } + const invalidLabelTitle = useMemo(() => { + const label = selectedLayerConfig.style?.label; + return label && label.enabled + ? label.titleType === 'by_field' + ? label.titleByField === '' + : label.titleByFixed === '' + : false; }, [selectedLayerConfig]); useEffect(() => { - if (inValidLabelTittle || invalidLabelSize) { - setIsUpdateDisabled(true); - } else { - setIsUpdateDisabled(false); - } - }, [inValidLabelTittle, invalidLabelSize]); + setIsUpdateDisabled(invalidLabelTitle || invalidLabelSize); + }, [invalidLabelTitle, invalidLabelSize, setIsUpdateDisabled]); - const onChangeShowLabel = (e: any) => { - const newLayerConfig = { + const onChangeLabel = (propName: string, propValue: boolean | number | string) => { + setSelectedLayerConfig({ ...selectedLayerConfig, style: { ...selectedLayerConfig.style, label: { - ...selectedLayerConfig.style.label, - enabled: Boolean(e.target.checked), + ...selectedLayerConfig.style?.label, + [propName]: propValue, }, }, - }; - setSelectedLayerConfig(newLayerConfig); + }); }; - const onChangeLabelTittleType = (e: any) => { - const newLayerConfig = { - ...selectedLayerConfig, - style: { - ...selectedLayerConfig.style, - label: { - ...selectedLayerConfig.style.label, - tittleType: String(e.target.value), - }, - }, - }; - setSelectedLayerConfig(newLayerConfig); + const onChangeShowLabel = (event: ChangeEvent) => { + onChangeLabel('enabled', Boolean(event.target.checked)); }; - const onStaticLabelTittleChange = (e: { target: { value: any } }) => { - const newLayerConfig = { - ...selectedLayerConfig, - style: { - ...selectedLayerConfig.style, - label: { - ...selectedLayerConfig.style.label, - tittle: String(e.target.value), - }, - }, - }; - setSelectedLayerConfig(newLayerConfig); + const onChangeLabelTitleType: ChangeEventHandler = (event: ChangeEvent) => + onChangeLabel('titleType', event.target.value); + + const onFixedLabelTitleChange = (event: ChangeEvent) => { + onChangeLabel('titleByFixed', String(event.target.value)); }; - const OnChangeLabelSize = (e: { target: { value: any } }) => { - const newLayerConfig = { - ...selectedLayerConfig, - style: { - ...selectedLayerConfig.style, - label: { - ...selectedLayerConfig.style.label, - size: Number(e.target.value), - }, - }, - }; - setSelectedLayerConfig(newLayerConfig); + const OnChangeLabelSize = (event: ChangeEvent) => { + onChangeLabel('size', Number(event.target.value)); }; - const onChangeLabelBorderWidth = (e: any) => { - const newLayerConfig = { - ...selectedLayerConfig, - style: { - ...selectedLayerConfig.style, - label: { - ...selectedLayerConfig.style.label, - borderWidth: Number(e.target.value), - }, - }, - }; - setSelectedLayerConfig(newLayerConfig); + const onChangeLabelBorderWidth: ChangeEventHandler = (event: ChangeEvent) => { + onChangeLabel('borderWidth', Number(event.target.value)); }; const onChangeLabelBorderColor = (color: string) => { - const newLayerConfig = { - ...selectedLayerConfig, - style: { - ...selectedLayerConfig.style, - label: { - ...selectedLayerConfig.style.label, - borderColor: color, - }, - }, - }; - setSelectedLayerConfig(newLayerConfig); + onChangeLabel('borderColor', color); }; const onChangeLabelColor = (color: string) => { - const newLayerConfig = { - ...selectedLayerConfig, - style: { - ...selectedLayerConfig.style, - label: { - ...selectedLayerConfig.style.label, - color, - }, - }, - }; - setSelectedLayerConfig(newLayerConfig); + onChangeLabel('color', color); }; + const onChangeFieldLabelTitle = (options: EuiComboBoxOptionOption[]) => { + onChangeLabel('titleByField', options[0]?.label || ''); + }; + + const label = selectedLayerConfig.style?.label; + return ( <> - {selectedLayerConfig.style?.label?.enabled && ( + {label?.enabled && ( <> - - {selectedLayerConfig.style?.label?.tittleType === 'fixed' && ( + + {label?.titleType === 'fixed' && ( + )} + {selectedLayerConfig.style?.label?.titleType === 'by_field' && ( + )} @@ -271,7 +231,7 @@ export const LabelConfig = ({ /> diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index eec63e32..6bcfa4bf 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -248,7 +248,7 @@ export const createSymbolLayerSpecification = ( sourceId: layerConfig.id, visibility: layerConfig.visibility, textFont: ['Noto Sans Regular'], - textField: layerConfig.style.label.tittle, + textField: layerConfig.style.label.titleByFixed, textSize: layerConfig.style.label.size, textColor: layerConfig.style.label.color, minZoom: layerConfig.zoomRange[0], diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index d5ed0990..cb989d4e 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -51,8 +51,9 @@ export type DocumentLayerSpecification = AbstractLayerSpecification & { markerSize: number; label?: { enabled: boolean; - tittle: string; - tittleType: 'fixed' | 'by_field'; + titleByFixed: string; + titleByField: string; + titleType: 'fixed' | 'by_field'; color: string; size: number; borderColor: string; diff --git a/public/utils/fields_options.test.ts b/public/utils/fields_options.test.ts new file mode 100644 index 00000000..2235181f --- /dev/null +++ b/public/utils/fields_options.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getFieldsOptions } from './fields_options'; +import { IndexPattern } from '../../../../src/plugins/data/common'; + +describe('getFieldsOptions', () => { + const mockIndexPattern = { + fields: [ + { + name: 'field1', + displayName: 'Field 1', + type: 'text', + }, + { + name: 'field2', + displayName: 'Field 2', + type: 'number', + }, + { + name: 'field3', + displayName: 'Field 3', + type: 'geo_point', + }, + { + name: 'field4', + displayName: 'Field 4', + type: 'geo_shape', + }, + ], + } as IndexPattern; + + it('should return all fields options if no acceptedFieldTypes are specified', () => { + const expectedFieldsOptions = [ + { label: 'text', options: [{ label: 'Field 1' }] }, + { + label: 'number', + options: [{ label: 'Field 2' }], + }, + { + label: 'geo_point', + options: [{ label: 'Field 3' }], + }, + { + label: 'geo_shape', + options: [{ label: 'Field 4' }], + }, + ]; + const fieldsOptions = getFieldsOptions(mockIndexPattern); + expect(fieldsOptions).toEqual(expectedFieldsOptions); + }); + + it('should return only options for acceptedFieldTypes', () => { + const acceptedFieldTypes = ['geo_point', 'geo_shape']; + const expectedFieldsOptions = [ + { label: 'geo_point', options: [{ label: 'Field 3' }] }, + { + label: 'geo_shape', + options: [{ label: 'Field 4' }], + }, + ]; + const fieldsOptions = getFieldsOptions(mockIndexPattern, acceptedFieldTypes); + expect(fieldsOptions).toEqual(expectedFieldsOptions); + }); + + it('should return an empty array if indexPattern is null', () => { + const fieldsOptions = getFieldsOptions(null); + expect(fieldsOptions).toEqual([]); + }); + + it('should return an empty array if indexPattern fields are null', () => { + const mockIndexPatternWithoutFields = { + fields: null, + } as unknown as IndexPattern; + const fieldsOptions = getFieldsOptions(mockIndexPatternWithoutFields); + expect(fieldsOptions).toEqual([]); + }); +}); diff --git a/public/utils/fields_options.ts b/public/utils/fields_options.ts new file mode 100644 index 00000000..a29ee065 --- /dev/null +++ b/public/utils/fields_options.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _, { Dictionary } from 'lodash'; +import { IndexPatternField } from '../../../../src/plugins/data/common'; +import { IndexPattern } from '../../../../src/plugins/data/public'; +export const getFieldsOptions = ( + indexPattern: IndexPattern | null | undefined, + acceptedFieldTypes: string[] = [] +) => { + const fieldList = indexPattern?.fields; + if (!fieldList) return []; + const fieldTypeMap: Dictionary = _.groupBy(fieldList, (field) => field.type); + + const fieldOptions: Array<{ label: string; options: Array<{ label: string }> }> = []; + + Object.entries(fieldTypeMap).forEach(([fieldType, fieldEntries]) => { + const fieldsOfSameType: Array<{ label: string }> = []; + for (const field of fieldEntries) { + if (acceptedFieldTypes.length === 0 || acceptedFieldTypes.includes(field.type)) { + fieldsOfSameType.push({ label: `${field.displayName || field.name}` }); + } + } + if (fieldsOfSameType.length > 0) { + fieldOptions.push({ + label: `${fieldType}`, + options: fieldsOfSameType, + }); + } + }); + return fieldOptions; +}; + +export const formatFieldsStringToComboBox = ( + fields: string[] | null | undefined +): Array<{ label: string }> => { + if (!fields) return []; + + return fields.map((field) => { + return { + label: field, + }; + }); +}; + +export const formatFieldStringToComboBox = ( + field: string | undefined +): Array<{ label: string }> => { + if (!field) return []; + + return formatFieldsStringToComboBox([field]); +}; diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts index 8d2374e3..0c60984d 100644 --- a/public/utils/getIntialConfig.ts +++ b/public/utils/getIntialConfig.ts @@ -20,7 +20,6 @@ import { CUSTOM_MAP, DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER, DOCUMENTS_DEFAULT_LABEL_ENABLES, - DOCUMENTS_DEFAULT_LABEL_VALUE, DOCUMENTS_DEFAULT_LABEL_TYPE, DOCUMENTS_DEFAULT_LABEL_SIZE, DOCUMENTS_DEFAULT_LABEL_COLOR, @@ -69,8 +68,9 @@ export const getLayerConfigMap = (mapConfig: ConfigSchema) => ({ markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, label: { enabled: DOCUMENTS_DEFAULT_LABEL_ENABLES, - tittle: DOCUMENTS_DEFAULT_LABEL_VALUE, - tittleType: DOCUMENTS_DEFAULT_LABEL_TYPE, + titleByFixed: '', + titleByField: '', + titleType: DOCUMENTS_DEFAULT_LABEL_TYPE, size: DOCUMENTS_DEFAULT_LABEL_SIZE, borderWidth: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, color: DOCUMENTS_DEFAULT_LABEL_COLOR, From 7a53e13f7d87383373f10e5618c02d390f0c4e25 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 8 Mar 2023 14:12:48 -0800 Subject: [PATCH 41/77] Add rectangle as custom mode (#247) * Add dependency to mapbox-gl-draw * Add rectangle mode New draw mode that extends polygon but behaves like rectangle. On First click, coordinate is set. On Second click the rectangle is created as bounding box. This will be used for drawing bounding box. Signed-off-by: Vijayan Balasubramanian --- package.json | 4 + public/components/draw/modes/rectangle.ts | 149 ++++++++++++++++++++++ yarn.lock | 108 ++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 public/components/draw/modes/rectangle.ts diff --git a/package.json b/package.json index 74f13d2e..79f24ac3 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,13 @@ }, "dependencies": { "@cypress/skip-test": "^2.6.1", + "@mapbox/mapbox-gl-draw": "^1.4.0", "@opensearch-dashboards-test/opensearch-dashboards-test-library": "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main", + "@types/mapbox__mapbox-gl-draw": "^1.3.3", "@types/wellknown": "^0.5.4", "cypress-file-upload": "^5.0.8", + "geojson": "^0.5.0", + "install": "^0.13.0", "maplibre-gl": "^2.4.0", "prettier": "^2.1.1", "uuid": "3.3.2", diff --git a/public/components/draw/modes/rectangle.ts b/public/components/draw/modes/rectangle.ts new file mode 100644 index 00000000..cb685e3f --- /dev/null +++ b/public/components/draw/modes/rectangle.ts @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Feature, GeoJSON, Position } from 'geojson'; +import { DrawCustomMode, DrawFeature, DrawPolygon, MapMouseEvent } from '@mapbox/mapbox-gl-draw'; + +// converted to typescript from +// https://github.com/mapbox/geojson.io/blob/main/src/ui/draw/rectangle.js +const doubleClickZoom = { + enable: (ctx: any) => { + setTimeout(() => { + // First check we've got a map and some context. + if ( + !ctx.map || + !ctx.map.doubleClickZoom || + !ctx._ctx || + !ctx._ctx.store || + !ctx._ctx.store.getInitialConfigValue + ) + return; + // Now check initial state wasn't false (we leave it disabled if so) + if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return; + ctx.map.doubleClickZoom.enable(); + }, 0); + }, + disable(ctx: { map: { doubleClickZoom: { disable: () => void } } }) { + setTimeout(() => { + if (!ctx.map || !ctx.map.doubleClickZoom) return; + // Always disable here, as it's necessary in some cases. + ctx.map.doubleClickZoom.disable(); + }, 0); + }, +}; + +interface DrawRectangleState extends DrawPolygon { + startPoint: Position; + endPoint: Position; +} + +// TODO Convert this to class +export const DrawRectangle: DrawCustomMode = { + onSetup(): any { + const rectangleGeoJSON: GeoJSON = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [[]], + }, + }; + const rectangle: DrawFeature = this.newFeature(rectangleGeoJSON); + // @ts-ignore + this.addFeature(rectangle); + // @ts-ignore + this.clearSelectedFeatures(); + doubleClickZoom.disable(this); + // @ts-ignore + this.updateUIClasses({ mouse: 'add' }); + // @ts-ignore + this.setActionableState({ + trash: true, + }); + return rectangle; + }, + // Whenever a user clicks on the map, Draw will call `onClick` + onClick(state: DrawRectangleState, e: MapMouseEvent) { + // if state.startPoint exist, means its second click + // change to simple_select mode + if ( + state.startPoint && + state.startPoint[0] !== e.lngLat.lng && + state.startPoint[1] !== e.lngLat.lat + ) { + // @ts-ignore + this.updateUIClasses({ mouse: 'pointer' }); + state.endPoint = [e.lngLat.lng, e.lngLat.lat]; + // @ts-ignore + this.changeMode('simple_select', { featuresId: state.id }); + } + // on first click, save clicked point coords as starting for rectangle + const startPoint = [e.lngLat.lng, e.lngLat.lat]; + state.startPoint = startPoint; + }, + onMouseMove(state: DrawRectangleState, e: MapMouseEvent) { + // if startPoint, update the feature coordinates, using the bounding box concept + // we are simply using the startingPoint coordinates and the current Mouse Position + // coordinates to calculate the bounding box on the fly, which will be our rectangle + if (state.startPoint) { + state.updateCoordinate('0.0', state.startPoint[0], state.startPoint[1]); // minX, minY - the starting point + state.updateCoordinate('0.1', e.lngLat.lng, state.startPoint[1]); // maxX, minY + state.updateCoordinate('0.2', e.lngLat.lng, e.lngLat.lat); // maxX, maxY + state.updateCoordinate('0.3', state.startPoint[0], e.lngLat.lat); // minX,maxY + state.updateCoordinate('0.4', state.startPoint[0], state.startPoint[1]); // minX,minY - ending point (equals to starting point) + } + }, + onKeyUp(state: DrawRectangleState, e: KeyboardEvent) { + if (e.code === 'Escape') { + // change mode to simple select if escape is pressed + // @ts-ignore + this.changeMode('simple_select'); + } + }, + onStop(state: DrawRectangleState) { + doubleClickZoom.enable(this); + // @ts-ignore + this.updateUIClasses({ mouse: 'none' }); + // @ts-ignore + this.activateUIButton(); + + // check to see if we've deleted this feature + // @ts-ignore + if (this.getFeature(state.id) === undefined) return; + + // remove last added coordinate + state.removeCoordinate('0.4'); + if (state.isValid()) { + // @ts-ignore + this.map.fire('draw.create', { + features: [state.toGeoJSON()], + }); + } else { + // @ts-ignore + this.deleteFeature([state.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select', {}, { silent: true }); + } + }, + toDisplayFeatures( + state: DrawRectangleState, + geojson: Feature, + display: (geojson: Feature) => void + ) { + const isActivePolygon = geojson?.properties?.id === state.id; + geojson.properties!.active = isActivePolygon ? 'true' : 'false'; + if (!isActivePolygon) return display(geojson); + + // Only render the rectangular polygon if it has the starting point + if (!state.startPoint) return; + return display(geojson); + }, + onTrash(state: DrawRectangleState) { + // @ts-ignore + this.deleteFeature([state.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select'); + }, +}; diff --git a/yarn.lock b/yarn.lock index 6cd5ac0f..88cd112d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,6 +44,41 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@mapbox/extent@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@mapbox/extent/-/extent-0.4.0.tgz#3e591f32e1f0c3981c864239f7b0ac06e610f8a9" + integrity sha512-MSoKw3qPceGuupn04sdaJrFeLKvcSETVLZCGS8JA9x6zXQL3FWiKaIXYIZEDXd5jpXpWlRxinCZIN49yRy0C9A== + +"@mapbox/geojson-area@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10" + integrity sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA== + dependencies: + wgs84 "0.0.0" + +"@mapbox/geojson-coords@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-coords/-/geojson-coords-0.0.2.tgz#f73d5744c832de0f05c48899f16a4288cefb2606" + integrity sha512-YuVzpseee/P1T5BWyeVVPppyfmuXYHFwZHmybkqaMfu4BWlOf2cmMGKj2Rr92MwfSTOCSUA0PAsVGRG8akY0rg== + dependencies: + "@mapbox/geojson-normalize" "0.0.1" + geojson-flatten "^1.0.4" + +"@mapbox/geojson-extent@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-extent/-/geojson-extent-1.0.1.tgz#bd99a6b66ba98e63a29511c9cd1bbd1df4c1e203" + integrity sha512-hh8LEO3djT4fqfr8sSC6wKt+p0TMiu+KOLMBUiFOyj+zGq7+IXwQGl0ppCVDkyzCewyd9LoGe9zAvDxXrLfhLw== + dependencies: + "@mapbox/extent" "0.4.0" + "@mapbox/geojson-coords" "0.0.2" + rw "~0.1.4" + traverse "~0.6.6" + +"@mapbox/geojson-normalize@0.0.1", "@mapbox/geojson-normalize@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz#1da1e6b3a7add3ad29909b30f438f60581b7cd80" + integrity sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q== + "@mapbox/geojson-rewind@^0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz#591a5d71a9cd1da1a0bf3420b3bea31b0fc7946a" @@ -57,6 +92,19 @@ resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== +"@mapbox/mapbox-gl-draw@^1.4.0": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.4.1.tgz#96dcec4d3957150de854423ac15856fde43d1452" + integrity sha512-g6F49KZagF9269/IoF6vZJeail6qtoc5mVF3eVRikNT7UFnY0QASfe2y53mgE99s6GrHdpV+PZuFxaL71hkMhg== + dependencies: + "@mapbox/geojson-area" "^0.2.2" + "@mapbox/geojson-extent" "^1.0.1" + "@mapbox/geojson-normalize" "^0.0.1" + "@mapbox/point-geometry" "^0.1.0" + hat "0.0.3" + lodash.isequal "^4.5.0" + xtend "^4.0.2" + "@mapbox/mapbox-gl-supported@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz#c15367178d8bfe4765e6b47b542fe821ce259c7b" @@ -98,6 +146,21 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== +"@types/mapbox-gl@*": + version "2.7.10" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-2.7.10.tgz#a3a32a366bad8966c0a40b78209ed430ba018ce1" + integrity sha512-nMVEcu9bAcenvx6oPWubQSPevsekByjOfKjlkr+8P91vawtkxTnopDoXXq1Qn/f4cg3zt0Z2W9DVsVsKRNXJTw== + dependencies: + "@types/geojson" "*" + +"@types/mapbox__mapbox-gl-draw@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/mapbox__mapbox-gl-draw/-/mapbox__mapbox-gl-draw-1.3.3.tgz#b96cce3e3bcd3ed2c4243a848725c66328737797" + integrity sha512-VJYdbxPxLNd0rUmgD3h7lm743A5J0uVj03Wd7P3Z+plLhUmPYIipAS8F3DhiP1x5dhIo9r27spFp3oASovd1sg== + dependencies: + "@types/geojson" "*" + "@types/mapbox-gl" "*" + "@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz#488a9b76e8457d6792ea2504cdd4ecdd9860a27e" @@ -674,11 +737,21 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +geojson-flatten@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/geojson-flatten/-/geojson-flatten-1.1.1.tgz#601aae07ba6406281ebca683573dcda69eba04c7" + integrity sha512-k/6BCd0qAt7vdqdM1LkLfAy72EsLDy0laNwX0x2h49vfYCiQkRc4PSra8DNEdJ10EKRpwEvDXMb0dBknTJuWpQ== + geojson-vt@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== +geojson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" + integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== + get-intrinsic@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" @@ -769,6 +842,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hat@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a" + integrity sha512-zpImx2GoKXy42fVDSEad2BPKuSQdLcqsCYa48K3zHSzM/ugWuYjLDr8IXxpVuL7uCLHw56eaiLxCRthhOzf5ug== + http-signature@~1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" @@ -816,6 +894,11 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +install@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/install/-/install-0.13.0.tgz#6af6e9da9dd0987de2ab420f78e60d9c17260776" + integrity sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA== + is-ci@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" @@ -934,6 +1017,11 @@ listr2@^3.8.3: through "^2.3.8" wrap-ansi "^7.0.0" +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -1225,6 +1313,11 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +rw@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/rw/-/rw-0.1.4.tgz#4903cbd80248ae0ede685bf58fd236a7a9b29a3e" + integrity sha512-vSj3D96kMcjNyqPcp65wBRIDImGSrUuMxngNNxvw8MQaO+aQ6llzRPH7XcJy5zrpb3wU++045+Uz/IDIM684iw== + rxjs@^7.5.1: version "7.8.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" @@ -1385,6 +1478,11 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +traverse@~0.6.6: + version "0.6.7" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.7.tgz#46961cd2d57dd8706c36664acde06a248f1173fe" + integrity sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg== + tslib@^2.1.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" @@ -1463,6 +1561,11 @@ wellknown@^0.5.0: concat-stream "~1.5.0" minimist "~1.2.0" +wgs84@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/wgs84/-/wgs84-0.0.0.tgz#34fdc555917b6e57cf2a282ed043710c049cdc76" + integrity sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ== + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -1500,6 +1603,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 3a20e81a4056f9dcf6ff9c62321d28bf6a62a069 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Thu, 9 Mar 2023 17:51:44 -0800 Subject: [PATCH 42/77] Add tooltip to draw shape filter (#330) * Add tooltip to draw filter shape 1. Changes cursor 2. Add instruction on how to proceed. 3. Revert once cancel is selected. Signed-off-by: Vijayan Balasubramanian --- .../map_container/map_container.tsx | 11 ++- .../toolbar/spatial_filter/draw_tooltip.tsx | 80 +++++++++++++++++++ .../spatial_filter/filter_input_panel.tsx | 2 +- 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 public/components/toolbar/spatial_filter/draw_tooltip.tsx diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 8d510124..a9349296 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -8,7 +8,12 @@ import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; -import { DASHBOARDS_MAPS_LAYER_TYPE, DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; +import { + DASHBOARDS_MAPS_LAYER_TYPE, + DrawFilterProperties, + FILTER_DRAW_MODE, + MAP_INITIAL_STATE, +} from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { Filter, @@ -36,6 +41,7 @@ import { MapsFooter } from './maps_footer'; import { DisplayFeatures } from '../tooltip/display_features'; import { TOOLTIP_STATE } from '../../../common'; import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; +import { DrawTooltip } from '../toolbar/spatial_filter/draw_tooltip'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -265,6 +271,9 @@ export const MapContainer = ({ {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && ( )} + {mounted && Boolean(maplibreRef.current) && ( + + )}
{mounted && ( { + switch (mode) { + case FILTER_DRAW_MODE.POLYGON: + return i18n.translate('maps.drawFilterPolygon.tooltipContent', { + defaultMessage: 'Click to start shape. Click for vertex. Double click to finish.', + }); + default: + return i18n.translate('maps.drawFilterDefault.tooltipContent', { + defaultMessage: 'Click to start shape. Double click to finish.', + }); + } +}; + +export const DrawTooltip = ({ map, mode }: Props) => { + const hoverPopupRef = useRef( + new maplibregl.Popup({ + closeButton: false, + closeOnClick: false, + anchor: 'right', + maxWidth: 'max-content', + }) + ); + + useEffect(() => { + // remove previous popup + function onMouseMoveMap(e: MapEventType['mousemove']) { + map.getCanvas().style.cursor = 'crosshair'; // Changes cursor to '+' + hoverPopupRef.current + .setLngLat(e.lngLat) + .setOffset([X_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP, Y_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP]) // add some gap between cursor and pop up + .setText(getTooltipContent(mode)) + .addTo(map); + } + + function onMouseMoveOut() { + hoverPopupRef.current.remove(); + } + + function resetAction() { + map.getCanvas().style.cursor = ''; + hoverPopupRef.current.remove(); + // remove tooltip when users mouse move over a point + map.off('mousemove', onMouseMoveMap); + map.off('mouseout', onMouseMoveOut); + } + + if (map && mode === FILTER_DRAW_MODE.NONE) { + resetAction(); + } else { + // add tooltip when users mouse move over a point + map.on('mousemove', onMouseMoveMap); + map.on('mouseout', onMouseMoveOut); + } + return () => { + // remove tooltip when users mouse move over a point + // when component is unmounted + resetAction(); + }; + }, [mode]); + + return ; +}; diff --git a/public/components/toolbar/spatial_filter/filter_input_panel.tsx b/public/components/toolbar/spatial_filter/filter_input_panel.tsx index 1842bf87..68a4bbb5 100644 --- a/public/components/toolbar/spatial_filter/filter_input_panel.tsx +++ b/public/components/toolbar/spatial_filter/filter_input_panel.tsx @@ -53,7 +53,7 @@ export const FilterInputPanel = ({ }; return ( - + Date: Fri, 10 Mar 2023 10:07:41 -0800 Subject: [PATCH 43/77] Support adding field label to document layer (#336) Signed-off-by: Junqiu Lei --- common/index.ts | 9 +- .../document_layer_source.tsx | 77 ++++-- .../style/document_layer_style.tsx | 256 +++++++++--------- .../documents_config/style/label_config.scss | 2 +- .../style/label_config.test.tsx | 22 +- .../documents_config/style/label_config.tsx | 94 ++++--- public/model/documentLayerFunctions.ts | 9 +- public/model/layerRenderController.ts | 4 + public/model/map/__mocks__/layer.ts | 4 + public/model/map/layer_operations.test.ts | 51 +++- public/model/map/layer_operations.ts | 18 +- public/model/mapLayerType.ts | 6 +- public/utils/getIntialConfig.ts | 8 +- 13 files changed, 331 insertions(+), 229 deletions(-) diff --git a/common/index.ts b/common/index.ts index 49d3f6c2..8d165fe6 100644 --- a/common/index.ts +++ b/common/index.ts @@ -34,8 +34,12 @@ export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; export const DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER: boolean = true; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; export const DOCUMENTS_DEFAULT_LABEL_ENABLES: boolean = false; -export const DOCUMENTS_DEFAULT_LABEL_TYPE: string = 'fixed'; -export const DOCUMENTS_DEFAULT_LABEL_SIZE: number = 20; +export enum DOCUMENTS_LABEL_TEXT_TYPE { + BY_FIELD = 'by_field', + FIXED = 'fixed', +} +export const DOCUMENTS_DEFAULT_LABEL_TEXT_TYPE: string = DOCUMENTS_LABEL_TEXT_TYPE.BY_FIELD; +export const DOCUMENTS_DEFAULT_LABEL_SIZE: number = 15; export const DOCUMENTS_MIN_LABEL_SIZE: number = 1; export const DOCUMENTS_MAX_LABEL_SIZE: number = 100; export const DOCUMENTS_DEFAULT_LABEL_COLOR: string = '#000000'; @@ -162,4 +166,3 @@ export const DRAW_FILTER_SHAPE_TITLE = 'DRAW SHAPE'; export const DRAW_FILTER_POLYGON_DEFAULT_LABEL = 'polygon'; export const DRAW_FILTER_POLYGON = 'Draw Polygon'; export const DRAW_FILTER_POLYGON_RELATIONS = ['intersects', 'disjoint', 'within']; - diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index e729c679..c66ab09d 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -80,6 +80,13 @@ export const DocumentLayerSource = ({ return geoFields?.find((field) => field.name === selectedLayerConfig.source.geoFieldName); }, [geoFields, selectedLayerConfig]); + const hasInvalidTooltipFields = useMemo(() => { + return ( + selectedLayerConfig.source.tooltipFields?.length === 0 && + selectedLayerConfig.source.showTooltips + ); + }, [selectedLayerConfig.source.showTooltips, selectedLayerConfig.source.tooltipFields?.length]); + // We want to memorize the filters and geoField selection when a map layer config is opened useEffect(() => { if (indexPattern?.id && indexPattern.id === selectedLayerConfig.source.indexPatternId) { @@ -120,9 +127,16 @@ export const DocumentLayerSource = ({ }; useEffect(() => { - const disableUpdate = !indexPattern || !selectedField || hasInvalidRequestNumber; + const disableUpdate = + !indexPattern || !selectedField || hasInvalidRequestNumber || hasInvalidTooltipFields; setIsUpdateDisabled(disableUpdate); - }, [setIsUpdateDisabled, indexPattern, selectedField, hasInvalidRequestNumber]); + }, [ + setIsUpdateDisabled, + indexPattern, + selectedField, + hasInvalidRequestNumber, + hasInvalidTooltipFields, + ]); const onDocumentRequestNumberChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -364,33 +378,38 @@ export const DocumentLayerSource = ({ onChange={onEnableTooltipsChange} /> - - Tooltip fields - - - - - - + {enableTooltips && ( + <> + + Tooltip fields + + + + + + + + )} diff --git a/public/components/layer_config/documents_config/style/document_layer_style.tsx b/public/components/layer_config/documents_config/style/document_layer_style.tsx index 88060101..48ff038e 100644 --- a/public/components/layer_config/documents_config/style/document_layer_style.tsx +++ b/public/components/layer_config/documents_config/style/document_layer_style.tsx @@ -10,10 +10,9 @@ import { EuiSpacer, EuiButtonGroup, EuiPanel, - EuiTitle, EuiFormRow, EuiForm, - EuiHorizontalRule, + EuiCollapsibleNavGroup, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DocumentLayerSpecification } from '../../../../model/mapLayerType'; @@ -167,126 +166,141 @@ export const DocumentLayerStyle = ({ }; return ( - - -

Layer style

-
- - onChangeGeoTypeSelected(id)} - buttonSize="compressed" - /> - - - {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Point` && ( - - - - - - - )} - {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Line` && ( - - - - - )} - {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Polygon` && ( + <> + + + onChangeGeoTypeSelected(id)} + buttonSize="compressed" + /> + - - - + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Point` && ( + + + + + + + )} + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Line` && ( + + + + + )} + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Polygon` && ( + + + + + + )} - )} - - - -
+ +
+ + + + + + + + ); }; diff --git a/public/components/layer_config/documents_config/style/label_config.scss b/public/components/layer_config/documents_config/style/label_config.scss index 7902d9c5..6f7aaac5 100644 --- a/public/components/layer_config/documents_config/style/label_config.scss +++ b/public/components/layer_config/documents_config/style/label_config.scss @@ -1,3 +1,3 @@ -.documentsLabel__title { +.documentsLabel__text { overflow: hidden } diff --git a/public/components/layer_config/documents_config/style/label_config.test.tsx b/public/components/layer_config/documents_config/style/label_config.test.tsx index e0bf09ae..54f3c8ff 100644 --- a/public/components/layer_config/documents_config/style/label_config.test.tsx +++ b/public/components/layer_config/documents_config/style/label_config.test.tsx @@ -43,9 +43,9 @@ describe('LabelConfig', () => { markerSize: 10, label: { enabled: true, - titleByFixed: 'My Label', - titleByField: 'field1', - titleType: 'fixed', + textByFixed: 'My Label', + textByField: 'field1', + textType: 'fixed', color: '#FFFFFF', size: 12, borderColor: '#000000', @@ -117,7 +117,7 @@ describe('LabelConfig', () => { expect(checkbox.prop('checked')).toEqual(false); }); - it('should call setSelectedLayerConfig with updated config when onChangeLabelTitleType is called', () => { + it('should call setSelectedLayerConfig with updated config when onChangeLabelTextType is called', () => { const select = wrapper.find('EuiSelect').at(0); select.simulate('change', { target: { value: 'by_field' } }); expect(setSelectedLayerConfigMock).toHaveBeenCalledWith({ @@ -126,27 +126,26 @@ describe('LabelConfig', () => { ...mockLayerConfig.style, label: { ...mockLayerConfig.style.label, - titleType: 'by_field', + textType: 'by_field', }, }, }); }); - it('should render EuiFieldText with correct props when labelTitleType is "fixed"', () => { + it('should render EuiFieldText with correct props when labelTextType is "fixed"', () => { const fieldText = wrapper.find('EuiFieldText'); expect(fieldText.prop('value')).toEqual('My Label'); - expect(fieldText.prop('disabled')).toEqual(false); }); - it('should render EuiComboBox with correct props when labelTitleType is "By field', () => { + it('should render EuiComboBox with correct props when labelTextType is "By field', () => { const newSelectedLayerConfig = { ...mockLayerConfig, style: { ...mockLayerConfig.style, label: { ...mockLayerConfig.style.label, - titleType: 'by_field', - titleByField: 'Field 1', + textType: 'by_field', + textByField: 'Field 1', }, }, }; @@ -164,7 +163,7 @@ describe('LabelConfig', () => { ...mockLayerConfig.style, label: { ...mockLayerConfig.style.label, - titleByFixed: 'new label', + textByFixed: 'new label', }, }, }); @@ -215,7 +214,6 @@ describe('LabelConfig', () => { { value: DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, text: 'Large' }, ]); expect(select.prop('value')).toEqual(2); - expect(select.prop('disabled')).toEqual(false); expect(select.prop('fullWidth')).toEqual(true); }); diff --git a/public/components/layer_config/documents_config/style/label_config.tsx b/public/components/layer_config/documents_config/style/label_config.tsx index 3044fb44..da9a9b59 100644 --- a/public/components/layer_config/documents_config/style/label_config.tsx +++ b/public/components/layer_config/documents_config/style/label_config.tsx @@ -23,6 +23,8 @@ import { DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR, DOCUMENTS_DEFAULT_LABEL_COLOR, DOCUMENTS_DEFAULT_LABEL_SIZE, + DOCUMENTS_DEFAULT_LABEL_TEXT_TYPE, + DOCUMENTS_LABEL_TEXT_TYPE, DOCUMENTS_LARGE_LABEL_BORDER_WIDTH, DOCUMENTS_MAX_LABEL_SIZE, DOCUMENTS_MEDIUM_LABEL_BORDER_WIDTH, @@ -41,14 +43,14 @@ interface LabelProps { indexPattern: IndexPattern | null | undefined; } -const labelTitleTypeOptions = [ +const labelTextTypeOptions = [ { - value: 'fixed', - text: i18n.translate('maps.documents.labelFixedTitle', { defaultMessage: 'Fixed' }), + value: DOCUMENTS_LABEL_TEXT_TYPE.FIXED, + text: i18n.translate('maps.documents.labelFixedText', { defaultMessage: 'Fixed' }), }, { - value: 'by_field', - text: i18n.translate('maps.documents.labelByFieldTitle', { defaultMessage: 'By field' }), + value: DOCUMENTS_LABEL_TEXT_TYPE.BY_FIELD, + text: i18n.translate('maps.documents.labelByFieldText', { defaultMessage: 'Field value' }), }, ]; @@ -86,22 +88,27 @@ export const LabelConfig = ({ indexPattern, }: LabelProps) => { const invalidLabelSize = useMemo(() => { - const { size = 0, enabled } = selectedLayerConfig.style?.label || {}; + const { size = DOCUMENTS_DEFAULT_LABEL_SIZE, enabled } = selectedLayerConfig.style?.label || {}; return enabled && (size < DOCUMENTS_MIN_LABEL_SIZE || size > DOCUMENTS_MAX_LABEL_SIZE); }, [selectedLayerConfig]); - const invalidLabelTitle = useMemo(() => { - const label = selectedLayerConfig.style?.label; - return label && label.enabled - ? label.titleType === 'by_field' - ? label.titleByField === '' - : label.titleByFixed === '' - : false; + const invalidLabelText = useMemo(() => { + const { + enabled, + textType = DOCUMENTS_DEFAULT_LABEL_TEXT_TYPE, + textByField = '', + textByFixed = '', + } = selectedLayerConfig.style?.label || {}; + + return ( + enabled && + (textType === DOCUMENTS_LABEL_TEXT_TYPE.BY_FIELD ? textByField === '' : textByFixed === '') + ); }, [selectedLayerConfig]); useEffect(() => { - setIsUpdateDisabled(invalidLabelTitle || invalidLabelSize); - }, [invalidLabelTitle, invalidLabelSize, setIsUpdateDisabled]); + setIsUpdateDisabled(invalidLabelText || invalidLabelSize); + }, [invalidLabelText, invalidLabelSize, setIsUpdateDisabled]); const onChangeLabel = (propName: string, propValue: boolean | number | string) => { setSelectedLayerConfig({ @@ -120,11 +127,11 @@ export const LabelConfig = ({ onChangeLabel('enabled', Boolean(event.target.checked)); }; - const onChangeLabelTitleType: ChangeEventHandler = (event: ChangeEvent) => - onChangeLabel('titleType', event.target.value); + const onChangeLabelTextType: ChangeEventHandler = (event: ChangeEvent) => + onChangeLabel('textType', event.target.value); - const onFixedLabelTitleChange = (event: ChangeEvent) => { - onChangeLabel('titleByFixed', String(event.target.value)); + const onFixedLabelTextChange = (event: ChangeEvent) => { + onChangeLabel('textByFixed', String(event.target.value)); }; const OnChangeLabelSize = (event: ChangeEvent) => { @@ -143,8 +150,8 @@ export const LabelConfig = ({ onChangeLabel('color', color); }; - const onChangeFieldLabelTitle = (options: EuiComboBoxOptionOption[]) => { - onChangeLabel('titleByField', options[0]?.label || ''); + const onChangeLabelFieldText = (options: EuiComboBoxOptionOption[]) => { + onChangeLabel('textByField', options[0]?.label || ''); }; const label = selectedLayerConfig.style?.label; @@ -153,7 +160,7 @@ export const LabelConfig = ({ <> - - {label?.titleType === 'fixed' && ( + + {label?.textType === DOCUMENTS_LABEL_TEXT_TYPE.FIXED && ( )} - {selectedLayerConfig.style?.label?.titleType === 'by_field' && ( + {(!label?.textType || label?.textType === DOCUMENTS_LABEL_TEXT_TYPE.BY_FIELD) && ( )} @@ -222,7 +227,7 @@ export const LabelConfig = ({ placeholder={i18n.translate('maps.documents.labelSizePlaceholder', { defaultMessage: 'Select size', })} - value={selectedLayerConfig.style?.label?.size ?? DOCUMENTS_DEFAULT_LABEL_SIZE} + value={label?.size ?? DOCUMENTS_DEFAULT_LABEL_SIZE} onChange={OnChangeLabelSize} append={px} fullWidth={true} @@ -255,7 +260,6 @@ export const LabelConfig = ({ options={labelBorderWidthOptions} value={label?.borderWidth ?? DOCUMENTS_NONE_LABEL_BORDER_WIDTH} onChange={onChangeLabelBorderWidth} - disabled={!label?.enabled} fullWidth={true} /> diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index 036020e7..4cbb47d9 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -100,10 +100,17 @@ const getLayerSource = (data: any, layerConfig: DocumentLayerSpecification) => { data.forEach((item: any) => { const geoFieldValue = getFieldValue(item._source, geoFieldName); const geometry = buildGeometry(geoFieldType, geoFieldValue); + const fields: string[] = []; + if (layerConfig.source.tooltipFields) { + fields.push(...layerConfig.source.tooltipFields); + } + if (layerConfig.style.label?.textByField) { + fields.push(layerConfig.style.label.textByField); + } if (geometry) { const feature = { geometry, - properties: buildProperties(item, layerConfig.source.tooltipFields), + properties: buildProperties(item, fields), }; featureList.push(feature); } diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 2eb4ad0d..9d0ae664 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -46,6 +46,10 @@ export const prepareDataLayerSource = ( if (sourceConfig.showTooltips && sourceConfig.tooltipFields.length > 0) { sourceFields.push(...sourceConfig.tooltipFields); } + const label = layer.style.label; + if (label && label.enabled && label.textType === 'by_field' && label.textByField.length > 0) { + sourceFields.push(label.textByField); + } let buildQuery; let selectedTimeRange; if (indexPattern) { diff --git a/public/model/map/__mocks__/layer.ts b/public/model/map/__mocks__/layer.ts index 1f307ff3..f9323015 100644 --- a/public/model/map/__mocks__/layer.ts +++ b/public/model/map/__mocks__/layer.ts @@ -11,6 +11,10 @@ export class MockLayer { } public setProperty(name: string, value: any): this { + if (Array.isArray(value) && value.length !== 0 && value[0] === 'get') { + this.layerProperties.set(name, value[1]); + return this; + } this.layerProperties.set(name, value); return this; } diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 925c0825..7569a3bf 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -295,7 +295,7 @@ describe('Symbol layer', () => { sourceId, visibility: 'visible', textFont: ['Noto Sans Regular'], - textField: 'test text', + textByFixed: 'test text', textColor: '#af938a', textSize: 12, minZoom: 2, @@ -303,6 +303,8 @@ describe('Symbol layer', () => { opacity: 60, symbolBorderWidth: 2, symbolBorderColor: '#D6BF57', + textType: 'fixed', + textByField: '', }); const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; @@ -320,6 +322,41 @@ describe('Symbol layer', () => { expect(layer.getProperty('text-halo-color')).toBe('#D6BF57'); }); + it('should add symbol layer successfully when label text is field type', () => { + const mockMap = new MockMaplibreMap([]); + const sourceId: string = 'symbol-layer-source'; + const expectedLayerId = sourceId + '-symbol'; + addSymbolLayer(mockMap as unknown as Maplibre, { + sourceId, + visibility: 'visible', + textFont: ['Noto Sans Regular'], + textByFixed: '', + textColor: '#af938a', + textSize: 12, + minZoom: 2, + maxZoom: 10, + opacity: 60, + symbolBorderWidth: 2, + symbolBorderColor: '#D6BF57', + textType: 'by_field', + textByField: 'name_by_field', + }); + + const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; + expect(layer.getProperty('visibility')).toBe('visible'); + expect(layer.getProperty('source')).toBe(sourceId); + expect(layer.getProperty('type')).toBe('symbol'); + expect(layer.getProperty('minZoom')).toBe(2); + expect(layer.getProperty('maxZoom')).toBe(10); + expect(layer.getProperty('text-font')).toEqual(['Noto Sans Regular']); + expect(layer.getProperty('text-field')).toBe('name_by_field'); + expect(layer.getProperty('text-opacity')).toBe(0.6); + expect(layer.getProperty('text-color')).toBe('#af938a'); + expect(layer.getProperty('text-size')).toBe(12); + expect(layer.getProperty('text-halo-width')).toBe(2); + expect(layer.getProperty('text-halo-color')).toBe('#D6BF57'); + }); + it('should update symbol layer successfully', () => { const mockMap = new MockMaplibreMap([]); const sourceId: string = 'symbol-layer-source'; @@ -331,17 +368,19 @@ describe('Symbol layer', () => { textFont: ['Noto Sans Regular'], textSize: 12, textColor: '#251914', - textField: 'test text', + textByFixed: 'test text by fixed', minZoom: 2, maxZoom: 10, opacity: 60, symbolBorderWidth: 2, symbolBorderColor: '#D6BF57', + textType: 'fixed', + textByField: '', }); expect(mockMap.getLayer(expectedLayerId).length).toBe(1); // update symbol for test - const updatedText = 'updated text'; + const updatedTextByFixed = 'updated text'; const updatedTextColor = '#29d95b'; const updatedTextSize = 14; const updatedVisibility = 'none'; @@ -356,19 +395,21 @@ describe('Symbol layer', () => { textFont: ['Noto Sans Regular'], textSize: updatedTextSize, textColor: updatedTextColor, - textField: updatedText, + textByFixed: updatedTextByFixed, minZoom: updatedMinZoom, maxZoom: updatedMaxZoom, opacity: updatedOpacity, symbolBorderWidth: updatedSymbolBorderWidth, symbolBorderColor: updatedSymbolBorderColor, + textType: 'fixed', + textByField: '', }); const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; expect(layer.getProperty('source')).toBe(sourceId); expect(layer.getProperty('minZoom')).toBe(updatedMinZoom); expect(layer.getProperty('maxZoom')).toBe(updatedMaxZoom); expect(layer.getProperty('visibility')).toBe(updatedVisibility); - expect(layer.getProperty('text-field')).toBe(updatedText); + expect(layer.getProperty('text-field')).toBe(updatedTextByFixed); expect(layer.getProperty('text-opacity')).toBe(updatedOpacity / 100); expect(layer.getProperty('text-color')).toBe(updatedTextColor); expect(layer.getProperty('text-size')).toBe(updatedTextSize); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 6bcfa4bf..1f0f2ce2 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -229,13 +229,15 @@ const updatePolygonFillLayer = ( }; export interface SymbolLayerSpecification extends Layer { - visibility: string; - textFont: string[]; - textField: string; + textType: 'fixed' | 'by_field'; + textByFixed: string; + textByField: string; textSize: number; textColor: string; symbolBorderWidth: number; symbolBorderColor: string; + visibility: string; + textFont: string[]; } export const createSymbolLayerSpecification = ( @@ -248,7 +250,9 @@ export const createSymbolLayerSpecification = ( sourceId: layerConfig.id, visibility: layerConfig.visibility, textFont: ['Noto Sans Regular'], - textField: layerConfig.style.label.titleByFixed, + textByFixed: layerConfig.style.label.textByFixed, + textType: layerConfig.style.label.textType, + textByField: layerConfig.style.label.textByField, textSize: layerConfig.style.label.size, textColor: layerConfig.style.label.color, minZoom: layerConfig.zoomRange[0], @@ -289,8 +293,12 @@ export const updateSymbolLayer = ( specification: SymbolLayerSpecification ): string => { const symbolLayerId = specification.sourceId + '-symbol'; + if (specification.textType === 'by_field') { + map.setLayoutProperty(symbolLayerId, 'text-field', ['get', specification.textByField]); + } else { + map.setLayoutProperty(symbolLayerId, 'text-field', specification.textByFixed); + } map.setLayoutProperty(symbolLayerId, 'text-font', specification.textFont); - map.setLayoutProperty(symbolLayerId, 'text-field', specification.textField); map.setLayoutProperty(symbolLayerId, 'visibility', specification.visibility); map.setPaintProperty(symbolLayerId, 'text-opacity', specification.opacity / 100); map.setLayerZoomRange(symbolLayerId, specification.minZoom, specification.maxZoom); diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index cb989d4e..a2126537 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -51,9 +51,9 @@ export type DocumentLayerSpecification = AbstractLayerSpecification & { markerSize: number; label?: { enabled: boolean; - titleByFixed: string; - titleByField: string; - titleType: 'fixed' | 'by_field'; + textByFixed: string; + textByField: string; + textType: 'fixed' | 'by_field'; color: string; size: number; borderColor: string; diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts index 0c60984d..30585648 100644 --- a/public/utils/getIntialConfig.ts +++ b/public/utils/getIntialConfig.ts @@ -20,7 +20,7 @@ import { CUSTOM_MAP, DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER, DOCUMENTS_DEFAULT_LABEL_ENABLES, - DOCUMENTS_DEFAULT_LABEL_TYPE, + DOCUMENTS_DEFAULT_LABEL_TEXT_TYPE, DOCUMENTS_DEFAULT_LABEL_SIZE, DOCUMENTS_DEFAULT_LABEL_COLOR, DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR, @@ -68,9 +68,9 @@ export const getLayerConfigMap = (mapConfig: ConfigSchema) => ({ markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, label: { enabled: DOCUMENTS_DEFAULT_LABEL_ENABLES, - titleByFixed: '', - titleByField: '', - titleType: DOCUMENTS_DEFAULT_LABEL_TYPE, + textByFixed: '', + textByField: '', + textType: DOCUMENTS_DEFAULT_LABEL_TEXT_TYPE, size: DOCUMENTS_DEFAULT_LABEL_SIZE, borderWidth: DOCUMENTS_NONE_LABEL_BORDER_WIDTH, color: DOCUMENTS_DEFAULT_LABEL_COLOR, From 397e6661368750ba2096e97729a48871768b76b6 Mon Sep 17 00:00:00 2001 From: Heemin Kim Date: Mon, 13 Mar 2023 15:34:42 -0700 Subject: [PATCH 44/77] Add CHANGELOG (#342) Signed-off-by: Heemin Kim --- .github/workflows/changelog_verifier.yml | 18 +++++++++++ CHANGELOG.md | 41 ++++++++++++++++++++++++ CONTRIBUTING.md | 37 +++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 .github/workflows/changelog_verifier.yml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/changelog_verifier.yml b/.github/workflows/changelog_verifier.yml new file mode 100644 index 00000000..992a38b6 --- /dev/null +++ b/.github/workflows/changelog_verifier.yml @@ -0,0 +1,18 @@ +name: "Changelog Verifier" +on: + pull_request: + types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] + +jobs: + # Enforces the update of a changelog file on every pull request + verify-changelog: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.pull_request.head.sha }} + + - uses: dangoslen/changelog-enforcer@v3 + with: + skipLabels: "autocut, skip-changelog" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..79857340 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# CHANGELOG +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). See the [CONTRIBUTING guide](./CONTRIBUTING.md#Changelog) for instructions on how to add changelog entries. + +## [Unreleased 3.0](https://github.com/opensearch-project/dashboards-maps/compare/2.x...HEAD) +### Features +### Enhancements +### Bug Fixes +### Infrastructure +### Documentation +### Maintenance +### Refactoring + +## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.6...2.x) +### Features +* Support adding static label to document layer ([#322](https://github.com/opensearch-project/dashboards-maps/pull/322)) +* Add geo shape query filter ([#319](https://github.com/opensearch-project/dashboards-maps/pull/319)) +* Add shape filter UI button ([#329](https://github.com/opensearch-project/dashboards-maps/pull/329)) +* Add tooltip to draw shape filter ([#330](https://github.com/opensearch-project/dashboards-maps/pull/330)) +### Enhancements +* Enhance layer visibility status display ([#299](https://github.com/opensearch-project/dashboards-maps/pull/299)) +* Introduce disable tooltip on hover property ([#313](https://github.com/opensearch-project/dashboards-maps/pull/313)) +* Update tooltip behavior change ([#317](https://github.com/opensearch-project/dashboards-maps/pull/317)) +* Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) +### Bug Fixes +* Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) +* Show scroll bar when panel height reaches container bottom ([#295](https://github.com/opensearch-project/dashboards-maps/pull/295)) +* fix: fixed filters not reset when index pattern changed ([#234](https://github.com/opensearch-project/dashboards-maps/pull/234)) +* Add custom layer visibility config to render ([#297](https://github.com/opensearch-project/dashboards-maps/pull/297)) +* Fix color picker component issue ([#305](https://github.com/opensearch-project/dashboards-maps/pull/305)) +* fix: layer filter setting been reset unexpectedly ([#327](https://github.com/opensearch-project/dashboards-maps/commits/2.x)) +### Infrastructure +* Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) +### Documentation +### Maintenance +### Refactoring +* Move zoom and coordinates as separate component ([#309](https://github.com/opensearch-project/dashboards-maps/pull/309)) +* Move coordinates to footer ([#315](https://github.com/opensearch-project/dashboards-maps/pull/315)) +* Refactor tooltip setup as component ([#320](https://github.com/opensearch-project/dashboards-maps/pull/320)) +* Refactor get field options and add field label option on UI ([#328](https://github.com/opensearch-project/dashboards-maps/pull/328)) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cf6b6fb..5f39122a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ - [Contributing Code](#contributing-code) - [Developer Certificate of Origin](#developer-certificate-of-origin) - [License Headers](#license-headers) +- [Changelog](#changelog) - [Review Process](#review-process) ## Contributing to OpenSearch @@ -115,6 +116,42 @@ New files in your code contributions should contain the following license header # SPDX-License-Identifier: Apache-2.0 ``` +## Changelog + +OpenSearch maintains version specific changelog by enforcing a change to the ongoing [CHANGELOG](CHANGELOG.md) file adhering to the [Keep A Changelog](https://keepachangelog.com/en/1.0.0/) format. The purpose of the changelog is for the contributors and maintainers to incrementally build the release notes throughout the development process to avoid a painful and error-prone process of attempting to compile the release notes at release time. On each release the "unreleased" entries of the changelog are moved to the appropriate release notes document in the `./release-notes` folder. Also, incrementally building the changelog provides a concise, human-readable list of significant features that have been added to the unreleased version under development. + +### Which changes require a CHANGELOG entry? +Changelogs are intended for operators/administrators, developers integrating with libraries and APIs, and end-users interacting with OpenSearch Dashboards and/or the REST API (collectively referred to as "user"). In short, any change that a user of OpenSearch might want to be aware of should be included in the changelog. The changelog is _not_ intended to replace the git commit log that developers of OpenSearch itself rely upon. The following are some examples of changes that should be in the changelog: + +- A newly added feature +- A fix for a user-facing bug +- Dependency updates +- Fixes for security issues + +The following are some examples where a changelog entry is not necessary: + +- Adding, modifying, or fixing tests +- An incremental PR for a larger feature (such features should include _one_ changelog entry for the feature) +- Documentation changes or code refactoring +- Build-related changes + +Any PR that does not include a changelog entry will result in a failure of the validation workflow in GitHub. If the contributor and maintainers agree that no changelog entry is required, then the `skip-changelog` label can be applied to the PR which will result in the workflow passing. + +### How to add my changes to [CHANGELOG](CHANGELOG.md)? + +Adding in the change is two step process: +1. Add your changes to the corresponding section within the CHANGELOG file with dummy pull request information, publish the PR +2. Update the entry for your change in [`CHANGELOG.md`](CHANGELOG.md) and make sure that you reference the pull request there. + +### Where should I put my CHANGELOG entry? +Please review the [branching strategy](https://github.com/opensearch-project/.github/blob/main/RELEASING.md#opensearch-branching) document. The changelog on the `main` branch will contain sections for the _next major_ and _next minor_ releases. Your entry should go into the section it is intended to be released in. In practice, most changes to `main` will be backported to the next minor release so most entries will likely be in that section. + +The following examples assume the _next major_ release on main is 3.0, then _next minor_ release is 2.5, and the _current_ release is 2.4. + +- **Add a new feature to release in next minor:** Add a changelog entry to `[Unreleased 2.x]` on main, then backport to 2.x (including the changelog entry). +- **Introduce a breaking API change to release in next major:** Add a changelog entry to `[Unreleased 3.0]` on main, do not backport. +- **Upgrade a dependency to fix a CVE:** Add a changelog entry to `[Unreleased 2.x]` on main, then backport to 2.x (including the changelog entry), then backport to 2.4 and ensure the changelog entry is added to `[Unreleased 2.4.1]`. + ## Review Process We deeply appreciate everyone who takes the time to make a contribution. We will review all contributions as quickly as possible. As a reminder, [opening an issue](issues/new/choose) discussing your change before you make it is the best way to smooth the PR process. This will prevent a rejection because someone else is already working on the problem, or because the solution is incompatible with the architectural direction. From 02d7d9a939ea2c8e21c879ec9e588debe13b519b Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 14 Mar 2023 09:54:34 -0700 Subject: [PATCH 45/77] BWC for document layer label textType (#340) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + .../documents_config/style/label_config.tsx | 14 +++++++++++++- public/model/documentLayerFunctions.ts | 10 +++------- public/model/map/layer_operations.ts | 19 ++++++------------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79857340..b86b33d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Introduce disable tooltip on hover property ([#313](https://github.com/opensearch-project/dashboards-maps/pull/313)) * Update tooltip behavior change ([#317](https://github.com/opensearch-project/dashboards-maps/pull/317)) * Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) +* BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) * Show scroll bar when panel height reaches container bottom ([#295](https://github.com/opensearch-project/dashboards-maps/pull/295)) diff --git a/public/components/layer_config/documents_config/style/label_config.tsx b/public/components/layer_config/documents_config/style/label_config.tsx index da9a9b59..4745aa31 100644 --- a/public/components/layer_config/documents_config/style/label_config.tsx +++ b/public/components/layer_config/documents_config/style/label_config.tsx @@ -151,7 +151,19 @@ export const LabelConfig = ({ }; const onChangeLabelFieldText = (options: EuiComboBoxOptionOption[]) => { - onChangeLabel('textByField', options[0]?.label || ''); + const newSelectedLayerConfig = { + ...selectedLayerConfig, + style: { + ...selectedLayerConfig.style, + label: { + ...selectedLayerConfig.style?.label, + textByField: options[0]?.label || '', + // For backwards compatibility, set textType to BY_FIELD if textByField is set + textType: DOCUMENTS_LABEL_TEXT_TYPE.BY_FIELD, + }, + }, + }; + setSelectedLayerConfig(newSelectedLayerConfig); }; const label = selectedLayerConfig.style?.label; diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index 4cbb47d9..d1808ed7 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -233,11 +233,7 @@ const updateLayer = ( }; // The function to render label for document layer -const renderLabelLayer = ( - layerConfig: DocumentLayerSpecification, - maplibreRef: MaplibreRef, - beforeLayerId: string | undefined -) => { +const renderLabelLayer = (layerConfig: DocumentLayerSpecification, maplibreRef: MaplibreRef) => { const hasLabelLayer = hasSymbolLayer(maplibreRef.current!, layerConfig.id); // If the label set to enabled, add the label layer if (layerConfig.style?.label?.enabled) { @@ -245,7 +241,7 @@ const renderLabelLayer = ( if (hasLabelLayer) { updateSymbolLayer(maplibreRef.current!, symbolLayerSpec); } else { - addSymbolLayer(maplibreRef.current!, symbolLayerSpec, beforeLayerId); + addSymbolLayer(maplibreRef.current!, symbolLayerSpec); } } else { // If the label set to disabled, remove the label layer if it exists @@ -277,6 +273,6 @@ export const DocumentLayerFunctions = { beforeLayerId: string | undefined ) => { renderMarkerLayer(maplibreRef, layerConfig, data, beforeLayerId); - renderLabelLayer(layerConfig, maplibreRef, beforeLayerId); + renderLabelLayer(layerConfig, maplibreRef); }, }; diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 1f0f2ce2..78e3a27c 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -271,20 +271,13 @@ export const removeSymbolLayer = (map: Maplibre, layerId: string) => { map.removeLayer(layerId + '-symbol'); }; -export const addSymbolLayer = ( - map: Maplibre, - specification: SymbolLayerSpecification, - beforeId?: string -): string => { +export const addSymbolLayer = (map: Maplibre, specification: SymbolLayerSpecification): string => { const symbolLayerId = specification.sourceId + '-symbol'; - map.addLayer( - { - id: symbolLayerId, - type: 'symbol', - source: specification.sourceId, - }, - beforeId - ); + map.addLayer({ + id: symbolLayerId, + type: 'symbol', + source: specification.sourceId, + }); return updateSymbolLayer(map, specification); }; From e1a4b45ebfe54a6b609678a217774478d79d0c25 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 14 Mar 2023 13:02:02 -0700 Subject: [PATCH 46/77] Fix data query in dashboard mode when enable around map filter (#339) * Fix data query in dashboard mode when enable around map filter Signed-off-by: Junqiu Lei * Update CHANGELOG.md Signed-off-by: Junqiu Lei --------- Signed-off-by: Junqiu Lei --- CHANGELOG.md | 11 ++- .../map_container/map_container.tsx | 72 ++++--------------- public/model/layerRenderController.ts | 45 +++++++++++- public/model/layersFunctions.ts | 18 +++-- public/model/mapLayerType.ts | 4 ++ 5 files changed, 82 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b86b33d7..d6bb3cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,25 +18,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Add geo shape query filter ([#319](https://github.com/opensearch-project/dashboards-maps/pull/319)) * Add shape filter UI button ([#329](https://github.com/opensearch-project/dashboards-maps/pull/329)) * Add tooltip to draw shape filter ([#330](https://github.com/opensearch-project/dashboards-maps/pull/330)) + ### Enhancements * Enhance layer visibility status display ([#299](https://github.com/opensearch-project/dashboards-maps/pull/299)) * Introduce disable tooltip on hover property ([#313](https://github.com/opensearch-project/dashboards-maps/pull/313)) * Update tooltip behavior change ([#317](https://github.com/opensearch-project/dashboards-maps/pull/317)) * Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) * BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) + ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) * Show scroll bar when panel height reaches container bottom ([#295](https://github.com/opensearch-project/dashboards-maps/pull/295)) * fix: fixed filters not reset when index pattern changed ([#234](https://github.com/opensearch-project/dashboards-maps/pull/234)) * Add custom layer visibility config to render ([#297](https://github.com/opensearch-project/dashboards-maps/pull/297)) * Fix color picker component issue ([#305](https://github.com/opensearch-project/dashboards-maps/pull/305)) -* fix: layer filter setting been reset unexpectedly ([#327](https://github.com/opensearch-project/dashboards-maps/commits/2.x)) +* fix: layer filter setting been reset unexpectedly ([#327](https://github.com/opensearch-project/dashboards-maps/pull/327)) +* Fix data query in dashboard mode when enable around map filter ([#339](https://github.com/opensearch-project/dashboards-maps/pull/339)) + ### Infrastructure * Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) + ### Documentation + ### Maintenance + ### Refactoring * Move zoom and coordinates as separate component ([#309](https://github.com/opensearch-project/dashboards-maps/pull/309)) * Move coordinates to footer ([#315](https://github.com/opensearch-project/dashboards-maps/pull/315)) * Refactor tooltip setup as component ([#320](https://github.com/opensearch-project/dashboards-maps/pull/320)) -* Refactor get field options and add field label option on UI ([#328](https://github.com/opensearch-project/dashboards-maps/pull/328)) \ No newline at end of file +* Refactor get field options and add field label option on UI ([#328](https://github.com/opensearch-project/dashboards-maps/pull/328)) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index a9349296..1bc3a84d 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -8,12 +8,7 @@ import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; -import { - DASHBOARDS_MAPS_LAYER_TYPE, - DrawFilterProperties, - FILTER_DRAW_MODE, - MAP_INITIAL_STATE, -} from '../../../common'; +import { DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { Filter, @@ -24,19 +19,16 @@ import { } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { + renderDataLayers, + renderBaseLayers, handleDataLayerRender, - handleReferenceLayerRender, + handleBaseLayerRender, } from '../../model/layerRenderController'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { ResizeChecker } from '../../../../../src/plugins/opensearch_dashboards_utils/public'; import { MapServices } from '../../types'; import { ConfigSchema } from '../../../common/config'; -import { - getDataLayers, - getMapBeforeLayerId, - getReferenceLayers, - referenceLayerTypeLookup, -} from '../../model/layersFunctions'; +import { baseLayerTypeLookup } from '../../model/layersFunctions'; import { MapsFooter } from './maps_footer'; import { DisplayFeatures } from '../tooltip/display_features'; import { TOOLTIP_STATE } from '../../../common'; @@ -139,21 +131,11 @@ export const MapContainer = ({ // Handle map bounding box change, it should update the search if "request data around map extent" was enabled useEffect(() => { - function renderLayers() { - layers.forEach((layer: MapLayerSpecification) => { - // We don't send search query if the layer doesn't have "request data around map extent" enabled - if ( - layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS && - layer.source.useGeoBoundingBoxFilter - ) { - handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); - } - }); - } - // Rerender layers with 200ms debounce to avoid calling the search API too frequently, especially when // resizing the window, the "moveend" event could be fired constantly - const debouncedRenderLayers = debounce(renderLayers, 200); + const debouncedRenderLayers = debounce(() => { + renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + }, 200); if (maplibreRef.current) { maplibreRef.current.on('moveend', debouncedRenderLayers); @@ -171,52 +153,28 @@ export const MapContainer = ({ let intervalId: NodeJS.Timeout | undefined; if (refreshConfig && !refreshConfig.pause) { intervalId = setInterval(() => { - layers.forEach((layer: MapLayerSpecification) => { - if (referenceLayerTypeLookup[layer.type]) { - return; - } - handleDataLayerRender(layer, mapState, services, maplibreRef, undefined, timeRange); - }); + renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); }, refreshConfig.value); } return () => clearInterval(intervalId); }, [refreshConfig]); useEffect(() => { - if (!mounted) { - return; - } - - if (layers.length <= 0) { + if (!mounted || layers.length <= 0) { return; } if (isUpdatingLayerRender || isReadOnlyMode) { if (selectedLayerConfig) { - if (referenceLayerTypeLookup[selectedLayerConfig.type]) { - handleReferenceLayerRender(selectedLayerConfig, maplibreRef, undefined); + if (baseLayerTypeLookup[selectedLayerConfig.type]) { + handleBaseLayerRender(selectedLayerConfig, maplibreRef, undefined); } else { updateIndexPatterns(); handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef, undefined); } } else { - getDataLayers(layers).forEach((layer: MapLayerSpecification) => { - const beforeLayerId = getMapBeforeLayerId(layers, layer.id); - handleDataLayerRender( - layer, - mapState, - services, - maplibreRef, - beforeLayerId, - timeRange, - filters, - query - ); - }); - getReferenceLayers(layers).forEach((layer: MapLayerSpecification) => { - const beforeLayerId = getMapBeforeLayerId(layers, layer.id); - handleReferenceLayerRender(layer, maplibreRef, beforeLayerId); - }); + renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + renderBaseLayers(layers, maplibreRef); } setIsUpdatingLayerRender(false); } @@ -234,7 +192,7 @@ export const MapContainer = ({ if (!selectedLayerConfig) { return; } - if (referenceLayerTypeLookup[selectedLayerConfig.type]) { + if (baseLayerTypeLookup[selectedLayerConfig.type]) { return; } const findIndexPattern = layersIndexPatterns.find( diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 9d0ae664..caeca28f 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -18,7 +18,12 @@ import { Query, FILTERS, } from '../../../../src/plugins/data/common'; -import { layersFunctionMap } from './layersFunctions'; +import { + getDataLayers, + getMapBeforeLayerId, + getBaseLayers, + layersFunctionMap, +} from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; import { GeoBounds, getBounds } from './map/boundary'; @@ -136,10 +141,44 @@ export const handleDataLayerRender = ( ); }; -export const handleReferenceLayerRender = ( +export const handleBaseLayerRender = ( layer: MapLayerSpecification, maplibreRef: MaplibreRef, beforeLayerId: string | undefined -) => { +): void => { layersFunctionMap[layer.type].render(maplibreRef, layer, beforeLayerId); }; + +export const renderDataLayers = ( + layers: MapLayerSpecification[], + mapState: MapState, + services: MapServices, + maplibreRef: MaplibreRef, + timeRange?: TimeRange, + filtersFromDashboard?: Filter[], + query?: Query +): void => { + getDataLayers(layers).forEach((layer) => { + const beforeLayerId = getMapBeforeLayerId(layers, layer.id); + handleDataLayerRender( + layer, + mapState, + services, + maplibreRef, + beforeLayerId, + timeRange, + filtersFromDashboard, + query + ); + }); +}; + +export const renderBaseLayers = ( + layers: MapLayerSpecification[], + maplibreRef: MaplibreRef +): void => { + getBaseLayers(layers).forEach((layer) => { + const beforeLayerId = getMapBeforeLayerId(layers, layer.id); + handleBaseLayerRender(layer, maplibreRef, beforeLayerId); + }); +}; diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts index 52448abc..bb06bd09 100644 --- a/public/model/layersFunctions.ts +++ b/public/model/layersFunctions.ts @@ -11,7 +11,11 @@ import { } from '../../common'; import { OSMLayerFunctions } from './OSMLayerFunctions'; import { DocumentLayerFunctions } from './documentLayerFunctions'; -import { MapLayerSpecification } from './mapLayerType'; +import { + BaseLayerSpecification, + DataLayerSpecification, + MapLayerSpecification, +} from './mapLayerType'; import { CustomLayerFunctions } from './customLayerFunctions'; import { getLayers } from './map/layer_operations'; @@ -56,18 +60,20 @@ export const layersTypeIconMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_ICON.CUSTOM_MAP, }; -export const referenceLayerTypeLookup = { +export const baseLayerTypeLookup = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: true, [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: true, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: false, }; -export const getDataLayers = (layers: MapLayerSpecification[]) => { - return layers.filter((layer) => !referenceLayerTypeLookup[layer.type]); +export const getDataLayers = (layers: MapLayerSpecification[]): DataLayerSpecification[] => { + return layers.filter( + (layer) => !baseLayerTypeLookup[layer.type] + ) as DataLayerSpecification[]; }; -export const getReferenceLayers = (layers: MapLayerSpecification[]) => { - return layers.filter((layer) => referenceLayerTypeLookup[layer.type]); +export const getBaseLayers = (layers: MapLayerSpecification[]): BaseLayerSpecification[] => { + return layers.filter((layer) => baseLayerTypeLookup[layer.type]) as BaseLayerSpecification[]; }; // Get layer id from layers that is above the selected layer diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index a2126537..2ae4a0c3 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -11,6 +11,10 @@ export type MapLayerSpecification = | DocumentLayerSpecification | CustomLayerSpecification; +export type DataLayerSpecification = DocumentLayerSpecification; + +export type BaseLayerSpecification = OSMLayerSpecification | CustomLayerSpecification; + export type AbstractLayerSpecification = { name: string; id: string; From 4f108eca6df93c88d27a6b2aa358b7447bed1ead Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 15 Mar 2023 14:39:54 -0700 Subject: [PATCH 47/77] Add mapbox-gl draw mode (#347) * Add mapbox-gl draw mode if tooltip state is draw , mount mapbox-gl-draw and change mode accordingly. Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + common/index.ts | 9 ++- .../map_container/map_container.tsx | 8 +++ .../spatial_filter/draw_filter_shape.tsx | 63 +++++++++++++++++++ .../spatial_filter/filter_by_polygon.tsx | 4 +- 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 public/components/toolbar/spatial_filter/draw_filter_shape.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index d6bb3cf5..094b8389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Update tooltip behavior change ([#317](https://github.com/opensearch-project/dashboards-maps/pull/317)) * Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) * BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) +* Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/common/index.ts b/common/index.ts index 8d165fe6..1f73a398 100644 --- a/common/index.ts +++ b/common/index.ts @@ -156,6 +156,13 @@ export enum FILTER_DRAW_MODE { POLYGON = 'polygon', // Filter is active and set to draw polygon } +export const MAPBOX_GL_DRAW_CREATE_LISTENER = 'draw.create'; + +export enum MAPBOX_GL_DRAW_MODES { + DRAW_POLYGON = 'draw_polygon', + SIMPLE_SELECT = 'simple_select', +} + export interface DrawFilterProperties { relation?: string; mode: FILTER_DRAW_MODE; @@ -165,4 +172,4 @@ export interface DrawFilterProperties { export const DRAW_FILTER_SHAPE_TITLE = 'DRAW SHAPE'; export const DRAW_FILTER_POLYGON_DEFAULT_LABEL = 'polygon'; export const DRAW_FILTER_POLYGON = 'Draw Polygon'; -export const DRAW_FILTER_POLYGON_RELATIONS = ['intersects', 'disjoint', 'within']; +export const DRAW_FILTER_SPATIAL_RELATIONS = ['intersects', 'disjoint', 'within']; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 1bc3a84d..a4275e58 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -10,6 +10,7 @@ import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; +import { DrawFilterShape } from '../toolbar/spatial_filter/draw_filter_shape'; import { Filter, IndexPattern, @@ -232,6 +233,13 @@ export const MapContainer = ({ {mounted && Boolean(maplibreRef.current) && ( )} + {mounted && maplibreRef.current && tooltipState === TOOLTIP_STATE.FILTER_DRAW_SHAPE && ( + + )}
{mounted && ( void; +} + +export const DrawFilterShape = ({ + filterProperties, + map, + updateFilterProperties, +}: DrawFilterShapeProps) => { + const onDraw = (event: { features: Feature[] }) => { + updateFilterProperties({ + mode: FILTER_DRAW_MODE.NONE, + }); + }; + const mapboxDrawRef = useRef( + new MapboxDraw({ + displayControlsDefault: false, + }) + ); + + useEffect(() => { + if (map) { + map.addControl((mapboxDrawRef.current as unknown) as IControl, 'top-right'); + map.on(MAPBOX_GL_DRAW_CREATE_LISTENER, onDraw); + } + return () => { + if (map) { + map.off(MAPBOX_GL_DRAW_CREATE_LISTENER, onDraw); + // eslint-disable-next-line react-hooks/exhaustive-deps + map.removeControl((mapboxDrawRef.current as unknown) as IControl); + } + }; + }, []); + + useEffect(() => { + if (filterProperties.mode === FILTER_DRAW_MODE.POLYGON) { + mapboxDrawRef.current.changeMode(MAPBOX_GL_DRAW_MODES.DRAW_POLYGON); + } else { + // default mode + mapboxDrawRef.current.changeMode(MAPBOX_GL_DRAW_MODES.SIMPLE_SELECT); + } + }, [filterProperties.mode]); + + return ; +}; diff --git a/public/components/toolbar/spatial_filter/filter_by_polygon.tsx b/public/components/toolbar/spatial_filter/filter_by_polygon.tsx index 6ef49f2c..cf32c18a 100644 --- a/public/components/toolbar/spatial_filter/filter_by_polygon.tsx +++ b/public/components/toolbar/spatial_filter/filter_by_polygon.tsx @@ -11,7 +11,7 @@ import { DrawFilterProperties, DRAW_FILTER_POLYGON, DRAW_FILTER_POLYGON_DEFAULT_LABEL, - DRAW_FILTER_POLYGON_RELATIONS, + DRAW_FILTER_SPATIAL_RELATIONS, DRAW_FILTER_SHAPE_TITLE, } from '../../../../common'; import { FILTER_DRAW_MODE } from '../../../../common'; @@ -52,7 +52,7 @@ export const FilterByPolygon = ({ From 8744efb8a7dbe4c5ff1c965eaae397ec274cc55b Mon Sep 17 00:00:00 2001 From: raintygao Date: Sat, 18 Mar 2023 00:13:32 +0800 Subject: [PATCH 48/77] feat: add localization for opensearch base map (#312) * feat: add localization for base map Signed-off-by: raintygao * chore: update getlanguage order Signed-off-by: raintygao * chore: move constants to index Signed-off-by: raintygao * chore: update function name Signed-off-by: raintygao --------- Signed-off-by: raintygao --- common/index.ts | 4 ++++ common/util.ts | 9 +++++++++ public/model/OSMLayerFunctions.ts | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/common/index.ts b/common/index.ts index 1f73a398..7ecb3a21 100644 --- a/common/index.ts +++ b/common/index.ts @@ -146,6 +146,10 @@ export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe', }; +//refer https://github.com/opensearch-project/i18n-plugin/blob/main/DEVELOPER_GUIDE.md#new-locale for OSD supported languages +export const OSD_LANGUAGES = ['en', 'es', 'fr', 'de', 'ja', 'ko', 'zh']; // all these codes are also supported in vector tiles map +export const FALLBACK_LANGUAGE = 'en'; + export enum TOOLTIP_STATE { DISPLAY_FEATURES = 'DISPLAY_FEATURES', FILTER_DRAW_SHAPE = 'FILTER_DRAW_SHAPE', diff --git a/common/util.ts b/common/util.ts index afc2ef97..32529b0e 100644 --- a/common/util.ts +++ b/common/util.ts @@ -2,7 +2,16 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; +import { OSD_LANGUAGES, FALLBACK_LANGUAGE } from './index'; export const fromMBtoBytes = (sizeInMB: number) => { return sizeInMB * 1024 * 1024; }; + +export const getMapLanguage = () => { + const OSDLanguage = i18n.getLocale().toLowerCase(), + parts = OSDLanguage.split('-'); + const languageCode = parts.length > 1 ? parts[0] : OSDLanguage; + return OSD_LANGUAGES.includes(languageCode) ? languageCode : FALLBACK_LANGUAGE; +}; diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index 871904f0..96a0cc61 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -1,7 +1,8 @@ -import { Map as Maplibre, LayerSpecification } from 'maplibre-gl'; +import { Map as Maplibre, LayerSpecification, SymbolLayerSpecification } from 'maplibre-gl'; import { OSMLayerSpecification } from './mapLayerType'; import { getMaplibreBeforeLayerId } from './layersFunctions'; import { getLayers, hasLayer } from './map/layer_operations'; +import { getMapLanguage } from '../../common/util'; interface MaplibreRef { current: Maplibre | null; @@ -41,6 +42,17 @@ const updateLayerConfig = (layerConfig: OSMLayerSpecification, maplibreRef: Mapl handleStyleLayers(layerConfig, maplibreRef); }; +const setLanguage = (maplibreRef: MaplibreRef, styleLayer: LayerSpecification) => { + // if a layer contains label, we will set its language. + if (styleLayer.layout && (styleLayer as SymbolLayerSpecification).layout?.['text-field']) { + const language = getMapLanguage(); + maplibreRef.current?.setLayoutProperty(styleLayer.id, 'text-field', [ + 'get', + 'name:' + language, + ]); + } +}; + const addNewLayer = ( layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef, @@ -61,6 +73,7 @@ const addNewLayer = ( styleLayer.source = layerConfig.id; } maplibreRef.current?.addLayer(styleLayer, beforeMbLayerId); + setLanguage(maplibreRef, styleLayer); maplibreRef.current?.setLayoutProperty(styleLayer.id, 'visibility', layerConfig.visibility); maplibreRef.current?.setLayerZoomRange( styleLayer.id, From c5ee47afb911fa2d71ece285cc042087f88aa93c Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Fri, 17 Mar 2023 16:03:33 -0700 Subject: [PATCH 49/77] Avoid trigger tooltip from label (#350) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + public/components/tooltip/display_features.tsx | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 094b8389..7334b12b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) * BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) * Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) +* Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/public/components/tooltip/display_features.tsx b/public/components/tooltip/display_features.tsx index 5e389be9..cf9d597b 100644 --- a/public/components/tooltip/display_features.tsx +++ b/public/components/tooltip/display_features.tsx @@ -32,6 +32,10 @@ export const DisplayFeatures = memo(({ map, layers }: Props) => { const features = map.queryRenderedFeatures(e.point); if (features && map) { + // don't show tooltip from labels + if (features.length === 1 && features[0].layer?.type === 'symbol') { + return; + } clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); clickPopup?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)).addTo(map); } @@ -44,6 +48,10 @@ export const DisplayFeatures = memo(({ map, layers }: Props) => { const tooltipEnabledLayersOnHover = layers.filter(isTooltipEnabledOnHover); const features = map.queryRenderedFeatures(e.point); if (features && map) { + // don't show tooltip from labels + if (features.length === 1 && features[0].layer?.type === 'symbol') { + return; + } hoverPopup = createPopup({ features, layers: tooltipEnabledLayersOnHover, From d2c818fa996c59fd842b42b1231e8866cdc5526c Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 20 Mar 2023 14:35:45 -0700 Subject: [PATCH 50/77] Sync maplibre layer order after layers rendered (#353) * Sync maplibre layer order after layers rendered Signed-off-by: Junqiu Lei * Resolve feedback Signed-off-by: Junqiu Lei --------- Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + .../map_container/map_container.tsx | 13 +++- .../components/map_top_nav/top_nav_menu.tsx | 2 +- public/model/OSMLayerFunctions.ts | 18 +---- public/model/customLayerFunctions.ts | 43 ++++------ public/model/documentLayerFunctions.ts | 66 +++++++--------- public/model/layerRenderController.ts | 33 ++++---- public/model/layersFunctions.ts | 4 +- public/model/map/layer_operations.ts | 78 +++++++------------ 9 files changed, 104 insertions(+), 154 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7334b12b..860ac652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Fix color picker component issue ([#305](https://github.com/opensearch-project/dashboards-maps/pull/305)) * fix: layer filter setting been reset unexpectedly ([#327](https://github.com/opensearch-project/dashboards-maps/pull/327)) * Fix data query in dashboard mode when enable around map filter ([#339](https://github.com/opensearch-project/dashboards-maps/pull/339)) +* Sync maplibre layer order after layers rendered ([#353](https://github.com/opensearch-project/dashboards-maps/pull/353)) ### Infrastructure * Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index a4275e58..8f22cb3c 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -24,6 +24,7 @@ import { renderBaseLayers, handleDataLayerRender, handleBaseLayerRender, + orderLayers, } from '../../model/layerRenderController'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { ResizeChecker } from '../../../../../src/plugins/opensearch_dashboards_utils/public'; @@ -165,20 +166,28 @@ export const MapContainer = ({ return; } + const orderLayersAfterRenderLoaded = () => orderLayers(layers, maplibreRef.current!); + if (isUpdatingLayerRender || isReadOnlyMode) { if (selectedLayerConfig) { if (baseLayerTypeLookup[selectedLayerConfig.type]) { - handleBaseLayerRender(selectedLayerConfig, maplibreRef, undefined); + handleBaseLayerRender(selectedLayerConfig, maplibreRef); } else { updateIndexPatterns(); - handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef, undefined); + handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef); } } else { renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); renderBaseLayers(layers, maplibreRef); + // Because of async layer rendering, layers order is not guaranteed, so we need to order layers + // after all layers are rendered. + maplibreRef.current!.once('idle', orderLayersAfterRenderLoaded); } setIsUpdatingLayerRender(false); } + return () => { + maplibreRef.current!.off('idle', orderLayersAfterRenderLoaded); + }; }, [layers, mounted, timeRange, filters, query, mapState, isReadOnlyMode]); useEffect(() => { diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index 7d688b51..2f3cb9dc 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -92,7 +92,7 @@ export const MapTopNavMenu = ({ const refreshDataLayerRender = () => { layers.forEach((layer: MapLayerSpecification) => { if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); + handleDataLayerRender(layer, mapState, services, maplibreRef); } }); }; diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index 96a0cc61..8f382144 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -1,6 +1,5 @@ import { Map as Maplibre, LayerSpecification, SymbolLayerSpecification } from 'maplibre-gl'; import { OSMLayerSpecification } from './mapLayerType'; -import { getMaplibreBeforeLayerId } from './layersFunctions'; import { getLayers, hasLayer } from './map/layer_operations'; import { getMapLanguage } from '../../common/util'; @@ -53,11 +52,7 @@ const setLanguage = (maplibreRef: MaplibreRef, styleLayer: LayerSpecification) = } }; -const addNewLayer = ( - layerConfig: OSMLayerSpecification, - maplibreRef: MaplibreRef, - beforeLayerId: string | undefined -) => { +const addNewLayer = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { if (maplibreRef.current) { const { source, style } = layerConfig; maplibreRef.current.addSource(layerConfig.id, { @@ -65,14 +60,13 @@ const addNewLayer = ( url: source?.dataURL, }); fetchStyleLayers(style?.styleURL).then((styleLayers: LayerSpecification[]) => { - const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); styleLayers.forEach((styleLayer) => { styleLayer.id = styleLayer.id + '_' + layerConfig.id; // TODO: Add comments on why we skip background type if (styleLayer.type !== 'background') { styleLayer.source = layerConfig.id; } - maplibreRef.current?.addLayer(styleLayer, beforeMbLayerId); + maplibreRef.current?.addLayer(styleLayer); setLanguage(maplibreRef, styleLayer); maplibreRef.current?.setLayoutProperty(styleLayer.id, 'visibility', layerConfig.visibility); maplibreRef.current?.setLayerZoomRange( @@ -96,15 +90,11 @@ const addNewLayer = ( // Functions for OpenSearch maps vector tile layer export const OSMLayerFunctions = { - render: ( - maplibreRef: MaplibreRef, - layerConfig: OSMLayerSpecification, - beforeLayerId: string | undefined - ) => { + render: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { // If layer already exist in maplibre source, update layer config // else add new layer. return hasLayer(maplibreRef.current!, layerConfig.id) ? updateLayerConfig(layerConfig, maplibreRef) - : addNewLayer(layerConfig, maplibreRef, beforeLayerId); + : addNewLayer(layerConfig, maplibreRef); }, }; diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index 45da80c1..5de803c6 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -1,6 +1,5 @@ import { Map as Maplibre, AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; -import { getMaplibreBeforeLayerId } from './layersFunctions'; import { hasLayer, removeLayers } from './map/layer_operations'; interface MaplibreRef { @@ -44,11 +43,7 @@ const updateLayerConfig = (layerConfig: CustomLayerSpecification, maplibreRef: M } }; -const addNewLayer = ( - layerConfig: CustomLayerSpecification, - maplibreRef: MaplibreRef, - beforeLayerId: string | undefined -) => { +const addNewLayer = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { const maplibreInstance = maplibreRef.current; if (maplibreInstance) { const tilesURL = getCustomMapURL(layerConfig); @@ -59,23 +54,19 @@ const addNewLayer = ( tileSize: 256, attribution: layerSource?.attribution, }); - const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); - maplibreInstance.addLayer( - { - id: layerConfig.id, - type: 'raster', - source: layerConfig.id, - paint: { - 'raster-opacity': layerConfig.opacity / 100, - }, - layout: { - visibility: layerConfig.visibility === 'visible' ? 'visible' : 'none', - }, - minzoom: layerConfig.zoomRange[0], - maxzoom: layerConfig.zoomRange[1], + maplibreInstance.addLayer({ + id: layerConfig.id, + type: 'raster', + source: layerConfig.id, + paint: { + 'raster-opacity': layerConfig.opacity / 100, + }, + layout: { + visibility: layerConfig.visibility === 'visible' ? 'visible' : 'none', }, - beforeMbLayerId - ); + minzoom: layerConfig.zoomRange[0], + maxzoom: layerConfig.zoomRange[1], + }); } }; @@ -92,14 +83,10 @@ const getCustomMapURL = (layerConfig: CustomLayerSpecification) => { }; export const CustomLayerFunctions = { - render: ( - maplibreRef: MaplibreRef, - layerConfig: CustomLayerSpecification, - beforeLayerId: string | undefined - ) => { + render: (maplibreRef: MaplibreRef, layerConfig: CustomLayerSpecification) => { return hasLayer(maplibreRef.current!, layerConfig.id) ? updateLayerConfig(layerConfig, maplibreRef) - : addNewLayer(layerConfig, maplibreRef, beforeLayerId); + : addNewLayer(layerConfig, maplibreRef); }, remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { removeLayers(maplibreRef.current!, layerConfig.id, true); diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index d1808ed7..6ed22ef6 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -7,7 +7,6 @@ import { Map as Maplibre } from 'maplibre-gl'; import { parse } from 'wellknown'; import { DocumentLayerSpecification } from './mapLayerType'; import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; -import { getMaplibreBeforeLayerId } from './layersFunctions'; import { addCircleLayer, addLineLayer, @@ -131,56 +130,43 @@ const addNewLayer = ( if (!maplibreInstance) { return; } - const mbLayerBeforeId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); const source = getLayerSource(data, layerConfig); maplibreInstance.addSource(layerConfig.id, { type: 'geojson', data: source, }); - addCircleLayer( - maplibreInstance, - { - fillColor: layerConfig.style?.fillColor, + addCircleLayer(maplibreInstance, { + fillColor: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + outlineColor: layerConfig.style?.borderColor, + radius: layerConfig.style?.markerSize, + sourceId: layerConfig.id, + visibility: layerConfig.visibility, + width: layerConfig.style?.borderThickness, + }); + const geoFieldType = getGeoFieldType(layerConfig); + if (geoFieldType === 'geo_shape') { + addLineLayer(maplibreInstance, { + width: layerConfig.style?.borderThickness, + color: layerConfig.style?.fillColor, maxZoom: layerConfig.zoomRange[1], minZoom: layerConfig.zoomRange[0], opacity: layerConfig.opacity, - outlineColor: layerConfig.style?.borderColor, - radius: layerConfig.style?.markerSize, sourceId: layerConfig.id, visibility: layerConfig.visibility, + }); + addPolygonLayer(maplibreInstance, { width: layerConfig.style?.borderThickness, - }, - mbLayerBeforeId - ); - const geoFieldType = getGeoFieldType(layerConfig); - if (geoFieldType === 'geo_shape') { - addLineLayer( - maplibreInstance, - { - width: layerConfig.style?.borderThickness, - color: layerConfig.style?.fillColor, - maxZoom: layerConfig.zoomRange[1], - minZoom: layerConfig.zoomRange[0], - opacity: layerConfig.opacity, - sourceId: layerConfig.id, - visibility: layerConfig.visibility, - }, - mbLayerBeforeId - ); - addPolygonLayer( - maplibreInstance, - { - width: layerConfig.style?.borderThickness, - fillColor: layerConfig.style?.fillColor, - maxZoom: layerConfig.zoomRange[1], - minZoom: layerConfig.zoomRange[0], - opacity: layerConfig.opacity, - sourceId: layerConfig.id, - outlineColor: layerConfig.style?.borderColor, - visibility: layerConfig.visibility, - }, - mbLayerBeforeId - ); + fillColor: layerConfig.style?.fillColor, + maxZoom: layerConfig.zoomRange[1], + minZoom: layerConfig.zoomRange[0], + opacity: layerConfig.opacity, + sourceId: layerConfig.id, + outlineColor: layerConfig.style?.borderColor, + visibility: layerConfig.visibility, + }); } }; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index caeca28f..c1ef9530 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -18,12 +18,7 @@ import { Query, FILTERS, } from '../../../../src/plugins/data/common'; -import { - getDataLayers, - getMapBeforeLayerId, - getBaseLayers, - layersFunctionMap, -} from './layersFunctions'; +import { getDataLayers, getBaseLayers, layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; import { GeoBounds, getBounds } from './map/boundary'; @@ -106,7 +101,6 @@ export const handleDataLayerRender = ( mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, - beforeLayerId: string | undefined, timeRange?: TimeRange, filtersFromDashboard?: Filter[], query?: Query @@ -135,7 +129,7 @@ export const handleDataLayerRender = ( (result) => { const { layer, dataSource } = result; if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource); } } ); @@ -143,10 +137,9 @@ export const handleDataLayerRender = ( export const handleBaseLayerRender = ( layer: MapLayerSpecification, - maplibreRef: MaplibreRef, - beforeLayerId: string | undefined + maplibreRef: MaplibreRef ): void => { - layersFunctionMap[layer.type].render(maplibreRef, layer, beforeLayerId); + layersFunctionMap[layer.type].render(maplibreRef, layer); }; export const renderDataLayers = ( @@ -159,13 +152,11 @@ export const renderDataLayers = ( query?: Query ): void => { getDataLayers(layers).forEach((layer) => { - const beforeLayerId = getMapBeforeLayerId(layers, layer.id); handleDataLayerRender( layer, mapState, services, maplibreRef, - beforeLayerId, timeRange, filtersFromDashboard, query @@ -178,7 +169,19 @@ export const renderBaseLayers = ( maplibreRef: MaplibreRef ): void => { getBaseLayers(layers).forEach((layer) => { - const beforeLayerId = getMapBeforeLayerId(layers, layer.id); - handleBaseLayerRender(layer, maplibreRef, beforeLayerId); + handleBaseLayerRender(layer, maplibreRef); + }); +}; + +// Order maplibre layers based on the order of dashboard-maps layers +export const orderLayers = (mapLayers: MapLayerSpecification[], maplibre: Maplibre): void => { + const maplibreLayers = maplibre.getStyle().layers; + if (!maplibreLayers) return; + mapLayers.forEach((layer) => { + const layerId = layer.id; + const mbLayers = maplibreLayers.filter((mbLayer) => mbLayer.id.includes(layerId)); + mbLayers.forEach((mbLayer, index) => { + maplibre.moveLayer(mbLayer.id); + }); }); }; diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts index bb06bd09..4425cfce 100644 --- a/public/model/layersFunctions.ts +++ b/public/model/layersFunctions.ts @@ -67,9 +67,7 @@ export const baseLayerTypeLookup = { }; export const getDataLayers = (layers: MapLayerSpecification[]): DataLayerSpecification[] => { - return layers.filter( - (layer) => !baseLayerTypeLookup[layer.type] - ) as DataLayerSpecification[]; + return layers.filter((layer) => !baseLayerTypeLookup[layer.type]) as DataLayerSpecification[]; }; export const getBaseLayers = (layers: MapLayerSpecification[]): BaseLayerSpecification[] => { diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index 78e3a27c..ba85bbd7 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -71,21 +71,14 @@ export interface LineLayerSpecification extends Layer { width: number; } -export const addLineLayer = ( - map: Maplibre, - specification: LineLayerSpecification, - beforeId?: string -): string => { +export const addLineLayer = (map: Maplibre, specification: LineLayerSpecification): string => { const lineLayerId = specification.sourceId + '-line'; - map.addLayer( - { - id: lineLayerId, - type: 'line', - source: specification.sourceId, - filter: ['==', '$type', 'LineString'], - }, - beforeId - ); + map.addLayer({ + id: lineLayerId, + type: 'line', + source: specification.sourceId, + filter: ['==', '$type', 'LineString'], + }); return updateLineLayer(map, specification, lineLayerId); }; @@ -111,21 +104,14 @@ export interface CircleLayerSpecification extends Layer { width: number; } -export const addCircleLayer = ( - map: Maplibre, - specification: CircleLayerSpecification, - beforeId?: string -): string => { +export const addCircleLayer = (map: Maplibre, specification: CircleLayerSpecification): string => { const circleLayerId = specification.sourceId + '-circle'; - map.addLayer( - { - id: circleLayerId, - type: 'circle', - source: specification.sourceId, - filter: ['==', '$type', 'Point'], - }, - beforeId - ); + map.addLayer({ + id: circleLayerId, + type: 'circle', + source: specification.sourceId, + filter: ['==', '$type', 'Point'], + }); return updateCircleLayer(map, specification, circleLayerId); }; @@ -153,35 +139,25 @@ export interface PolygonLayerSpecification extends Layer { width: number; } -export const addPolygonLayer = ( - map: Maplibre, - specification: PolygonLayerSpecification, - beforeId?: string -) => { +export const addPolygonLayer = (map: Maplibre, specification: PolygonLayerSpecification) => { const fillLayerId = specification.sourceId + '-fill'; - map.addLayer( - { - id: fillLayerId, - type: 'fill', - source: specification.sourceId, - filter: ['==', '$type', 'Polygon'], - }, - beforeId - ); + map.addLayer({ + id: fillLayerId, + type: 'fill', + source: specification.sourceId, + filter: ['==', '$type', 'Polygon'], + }); updatePolygonFillLayer(map, specification, fillLayerId); // Due to limitations on WebGL, fill can't render outlines with width wider than 1, // so we have to create another style layer with type=line to apply width. const outlineId = fillLayerId + '-outline'; - map.addLayer( - { - id: outlineId, - type: 'line', - source: specification.sourceId, - filter: ['==', '$type', 'Polygon'], - }, - beforeId - ); + map.addLayer({ + id: outlineId, + type: 'line', + source: specification.sourceId, + filter: ['==', '$type', 'Polygon'], + }); updateLineLayer( map, { From a42c9155636b4d5a60dca45b61f613c07ded4795 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 22 Mar 2023 16:55:29 -0700 Subject: [PATCH 51/77] Add support to draw rectangle shape to filter documents (#348) * Add rectangle ui tool bar Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + common/index.ts | 4 + .../filter_by_rectangle.test.tsx.snap | 113 ++++++++++++++++++ .../filter_toolbar.test.tsx.snap | 8 ++ .../spatial_filter/draw_filter_shape.tsx | 24 +++- .../toolbar/spatial_filter/draw_tooltip.tsx | 4 + .../filter_by_rectangle.test.tsx | 23 ++++ .../spatial_filter/filter_by_rectangle.tsx | 90 ++++++++++++++ .../toolbar/spatial_filter/filter_toolbar.tsx | 5 + 9 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap create mode 100644 public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx create mode 100644 public/components/toolbar/spatial_filter/filter_by_rectangle.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 860ac652..1eaf3f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) * BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) * Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) +* Add support to draw rectangle shape to filter documents ([#348](https://github.com/opensearch-project/dashboards-maps/pull/348)) * Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) ### Bug Fixes diff --git a/common/index.ts b/common/index.ts index 7ecb3a21..c6160e8c 100644 --- a/common/index.ts +++ b/common/index.ts @@ -158,12 +158,14 @@ export enum TOOLTIP_STATE { export enum FILTER_DRAW_MODE { NONE = 'none', // draw filter is inactive POLYGON = 'polygon', // Filter is active and set to draw polygon + RECTANGLE = 'rectangle', // Filter is active and set to draw rectangle } export const MAPBOX_GL_DRAW_CREATE_LISTENER = 'draw.create'; export enum MAPBOX_GL_DRAW_MODES { DRAW_POLYGON = 'draw_polygon', + DRAW_RECTANGLE = 'draw_rectangle', SIMPLE_SELECT = 'simple_select', } @@ -175,5 +177,7 @@ export interface DrawFilterProperties { export const DRAW_FILTER_SHAPE_TITLE = 'DRAW SHAPE'; export const DRAW_FILTER_POLYGON_DEFAULT_LABEL = 'polygon'; +export const DRAW_FILTER_RECTANGLE_DEFAULT_LABEL = 'rectangle'; export const DRAW_FILTER_POLYGON = 'Draw Polygon'; +export const DRAW_FILTER_RECTANGLE = 'Draw Rectangle'; export const DRAW_FILTER_SPATIAL_RELATIONS = ['intersects', 'disjoint', 'within']; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap new file mode 100644 index 00000000..3dc50dad --- /dev/null +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders filter by rectangle button 1`] = ` + + + + } + closePopover={[Function]} + data-test-subj="drawRectanglePopOver" + display="inlineBlock" + hasArrow={true} + id="drawRectangleId" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "id": 0, + "title": "DRAW SHAPE", + }, + ] + } + size="m" + /> + +`; + +exports[`renders filter by rectangle in middle of drawing 1`] = ` + + + + } + closePopover={[Function]} + data-test-subj="drawRectanglePopOver" + display="inlineBlock" + hasArrow={true} + id="drawRectangleId" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "id": 0, + "title": "DRAW SHAPE", + }, + ] + } + size="m" + /> + +`; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap index fbafe8dd..edb9097b 100644 --- a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap @@ -12,6 +12,10 @@ exports[`renders spatial filter before drawing 1`] = ` isDrawActive={false} setDrawFilterProperties={[MockFunction]} /> + `; @@ -36,6 +40,10 @@ exports[`renders spatial filter while drawing 1`] = ` isDrawActive={true} setDrawFilterProperties={[MockFunction]} /> + `; diff --git a/public/components/toolbar/spatial_filter/draw_filter_shape.tsx b/public/components/toolbar/spatial_filter/draw_filter_shape.tsx index 567d049f..36f384c2 100644 --- a/public/components/toolbar/spatial_filter/draw_filter_shape.tsx +++ b/public/components/toolbar/spatial_filter/draw_filter_shape.tsx @@ -13,6 +13,7 @@ import { MAPBOX_GL_DRAW_MODES, MAPBOX_GL_DRAW_CREATE_LISTENER, } from '../../../../common'; +import { DrawRectangle } from '../../draw/modes/rectangle'; interface DrawFilterShapeProps { filterProperties: DrawFilterProperties; @@ -20,6 +21,17 @@ interface DrawFilterShapeProps { updateFilterProperties: (properties: DrawFilterProperties) => void; } +function getMapboxDrawMode(mode: FILTER_DRAW_MODE): string { + switch (mode) { + case FILTER_DRAW_MODE.POLYGON: + return MAPBOX_GL_DRAW_MODES.DRAW_POLYGON; + case FILTER_DRAW_MODE.RECTANGLE: + return MAPBOX_GL_DRAW_MODES.DRAW_RECTANGLE; + default: + return MAPBOX_GL_DRAW_MODES.SIMPLE_SELECT; + } +} + export const DrawFilterShape = ({ filterProperties, map, @@ -33,6 +45,10 @@ export const DrawFilterShape = ({ const mapboxDrawRef = useRef( new MapboxDraw({ displayControlsDefault: false, + modes: { + ...MapboxDraw.modes, + [MAPBOX_GL_DRAW_MODES.DRAW_RECTANGLE]: DrawRectangle, + }, }) ); @@ -51,12 +67,8 @@ export const DrawFilterShape = ({ }, []); useEffect(() => { - if (filterProperties.mode === FILTER_DRAW_MODE.POLYGON) { - mapboxDrawRef.current.changeMode(MAPBOX_GL_DRAW_MODES.DRAW_POLYGON); - } else { - // default mode - mapboxDrawRef.current.changeMode(MAPBOX_GL_DRAW_MODES.SIMPLE_SELECT); - } + const mapboxDrawMode: string = getMapboxDrawMode(filterProperties.mode); + mapboxDrawRef.current.changeMode(mapboxDrawMode); }, [filterProperties.mode]); return ; diff --git a/public/components/toolbar/spatial_filter/draw_tooltip.tsx b/public/components/toolbar/spatial_filter/draw_tooltip.tsx index bcb25a1f..ee42e317 100644 --- a/public/components/toolbar/spatial_filter/draw_tooltip.tsx +++ b/public/components/toolbar/spatial_filter/draw_tooltip.tsx @@ -22,6 +22,10 @@ const getTooltipContent = (mode: FILTER_DRAW_MODE): string => { return i18n.translate('maps.drawFilterPolygon.tooltipContent', { defaultMessage: 'Click to start shape. Click for vertex. Double click to finish.', }); + case FILTER_DRAW_MODE.RECTANGLE: + return i18n.translate('maps.drawFilterRectangle.tooltipContent', { + defaultMessage: 'Click and drag to draw rectangle.', + }); default: return i18n.translate('maps.drawFilterDefault.tooltipContent', { defaultMessage: 'Click to start shape. Double click to finish.', diff --git a/public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx b/public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx new file mode 100644 index 00000000..8b100b67 --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { shallow } from 'enzyme'; +import React from 'react'; +import { FilterByRectangle } from './filter_by_rectangle'; + +it('renders filter by rectangle button', () => { + const mockCallback = jest.fn(); + const component = shallow( + + ); + expect(component).toMatchSnapshot(); +}); + +it('renders filter by rectangle in middle of drawing', () => { + const mockCallback = jest.fn(); + const component = shallow( + + ); + expect(component).toMatchSnapshot(); +}); diff --git a/public/components/toolbar/spatial_filter/filter_by_rectangle.tsx b/public/components/toolbar/spatial_filter/filter_by_rectangle.tsx new file mode 100644 index 00000000..d018917e --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter_by_rectangle.tsx @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiContextMenu, EuiPanel, EuiButtonIcon } from '@elastic/eui'; +import { FilterInputPanel } from './filter_input_panel'; +// TODO: replace with rectangle image file once available +import rectangle from '../../../images/polygon.svg'; +import { + DrawFilterProperties, + DRAW_FILTER_SPATIAL_RELATIONS, + DRAW_FILTER_SHAPE_TITLE, + DRAW_FILTER_RECTANGLE, + DRAW_FILTER_RECTANGLE_DEFAULT_LABEL, +} from '../../../../common'; +import { FILTER_DRAW_MODE } from '../../../../common'; + +interface FilterByRectangleProps { + setDrawFilterProperties: (properties: DrawFilterProperties) => void; + isDrawActive: boolean; +} + +export const FilterByRectangle = ({ + setDrawFilterProperties, + isDrawActive, +}: FilterByRectangleProps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const onSubmit = (input: { relation: string; label: string; mode: FILTER_DRAW_MODE }) => { + setDrawFilterProperties({ + mode: input.mode, + relation: input.relation, + filterLabel: input.label, + }); + closePopover(); + }; + + const panels = [ + { + id: 0, + title: DRAW_FILTER_SHAPE_TITLE, + content: ( + + ), + }, + ]; + + const drawRectangleButton = ( + + + + ); + return ( + + + + ); +}; diff --git a/public/components/toolbar/spatial_filter/filter_toolbar.tsx b/public/components/toolbar/spatial_filter/filter_toolbar.tsx index 9fc6bbaa..747dd7bf 100644 --- a/public/components/toolbar/spatial_filter/filter_toolbar.tsx +++ b/public/components/toolbar/spatial_filter/filter_toolbar.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FilterByPolygon } from './filter_by_polygon'; import { FILTER_DRAW_MODE, DrawFilterProperties } from '../../../../common'; +import {FilterByRectangle} from "./filter_by_rectangle"; interface SpatialFilterToolBarProps { setFilterProperties: (properties: DrawFilterProperties) => void; @@ -25,6 +26,10 @@ export const SpatialFilterToolbar = ({ const filterIconGroups = ( + ); if (isDrawActive) { From 2259b6f9b25f48a513df8bd7a950a269957de226 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Tue, 28 Mar 2023 02:40:30 +0530 Subject: [PATCH 52/77] Remove cancel button on draw shape and use Escape to cancel draw (#359) * Add cancel draw on Escape key * Remove feature on escape key up event On Esc key press event, draw.create shouldn't be fired. Simply changing to simple_select doesn't prevent if state has all vertices. Hence, remode the feature on Escape to avoid draw.create event is triggerd. Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + common/util.ts | 4 +++ public/components/draw/modes/rectangle.ts | 6 +++- .../map_container/map_container.tsx | 8 +++-- .../filter_toolbar.test.tsx.snap | 18 +++-------- .../toolbar/spatial_filter/draw_tooltip.tsx | 12 ++++++- .../toolbar/spatial_filter/filter_toolbar.tsx | 31 ++++--------------- 7 files changed, 38 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eaf3f96..d7bba741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) * Add support to draw rectangle shape to filter documents ([#348](https://github.com/opensearch-project/dashboards-maps/pull/348)) * Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) +* Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/common/util.ts b/common/util.ts index 32529b0e..34f1d2fa 100644 --- a/common/util.ts +++ b/common/util.ts @@ -15,3 +15,7 @@ export const getMapLanguage = () => { const languageCode = parts.length > 1 ? parts[0] : OSDLanguage; return OSD_LANGUAGES.includes(languageCode) ? languageCode : FALLBACK_LANGUAGE; }; + +export function isEscapeKey(e: KeyboardEvent) { + return e.code === 'Escape'; +} diff --git a/public/components/draw/modes/rectangle.ts b/public/components/draw/modes/rectangle.ts index cb685e3f..7af205e3 100644 --- a/public/components/draw/modes/rectangle.ts +++ b/public/components/draw/modes/rectangle.ts @@ -5,6 +5,7 @@ import { Feature, GeoJSON, Position } from 'geojson'; import { DrawCustomMode, DrawFeature, DrawPolygon, MapMouseEvent } from '@mapbox/mapbox-gl-draw'; +import {isEscapeKey} from "../../../../common/util"; // converted to typescript from // https://github.com/mapbox/geojson.io/blob/main/src/ui/draw/rectangle.js @@ -96,7 +97,10 @@ export const DrawRectangle: DrawCustomMode = { } }, onKeyUp(state: DrawRectangleState, e: KeyboardEvent) { - if (e.code === 'Escape') { + if (isEscapeKey(e)) { + // delete feature on Escape, else, onStop will append feature and fires draw.create event + // @ts-ignore + this.deleteFeature([state.id], { silent: true }); // change mode to simple select if escape is pressed // @ts-ignore this.changeMode('simple_select'); diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 8f22cb3c..f85df4d6 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -240,7 +240,11 @@ export const MapContainer = ({ )} {mounted && Boolean(maplibreRef.current) && ( - + setFilterProperties({ mode: FILTER_DRAW_MODE.NONE })} + /> )} {mounted && maplibreRef.current && tooltipState === TOOLTIP_STATE.FILTER_DRAW_SHAPE && ( )}
diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap index edb9097b..2b4aac5a 100644 --- a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap @@ -9,11 +9,11 @@ exports[`renders spatial filter before drawing 1`] = ` > @@ -22,19 +22,11 @@ exports[`renders spatial filter before drawing 1`] = ` exports[`renders spatial filter while drawing 1`] = ` - - - Cancel - - void; } const X_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP = -12; @@ -33,7 +35,7 @@ const getTooltipContent = (mode: FILTER_DRAW_MODE): string => { } }; -export const DrawTooltip = ({ map, mode }: Props) => { +export const DrawTooltip = ({ map, mode, onCancel }: Props) => { const hoverPopupRef = useRef( new maplibregl.Popup({ closeButton: false, @@ -58,12 +60,19 @@ export const DrawTooltip = ({ map, mode }: Props) => { hoverPopupRef.current.remove(); } + function onKeyDown(e: KeyboardEvent) { + if (isEscapeKey(e)) { + onCancel(); + } + } + function resetAction() { map.getCanvas().style.cursor = ''; hoverPopupRef.current.remove(); // remove tooltip when users mouse move over a point map.off('mousemove', onMouseMoveMap); map.off('mouseout', onMouseMoveOut); + map.getContainer().removeEventListener('keydown', onKeyDown); } if (map && mode === FILTER_DRAW_MODE.NONE) { @@ -72,6 +81,7 @@ export const DrawTooltip = ({ map, mode }: Props) => { // add tooltip when users mouse move over a point map.on('mousemove', onMouseMoveMap); map.on('mouseout', onMouseMoveOut); + map.getContainer().addEventListener('keydown', onKeyDown); } return () => { // remove tooltip when users mouse move over a point diff --git a/public/components/toolbar/spatial_filter/filter_toolbar.tsx b/public/components/toolbar/spatial_filter/filter_toolbar.tsx index 747dd7bf..59b99422 100644 --- a/public/components/toolbar/spatial_filter/filter_toolbar.tsx +++ b/public/components/toolbar/spatial_filter/filter_toolbar.tsx @@ -4,25 +4,18 @@ */ import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FilterByPolygon } from './filter_by_polygon'; -import { FILTER_DRAW_MODE, DrawFilterProperties } from '../../../../common'; -import {FilterByRectangle} from "./filter_by_rectangle"; +import { DrawFilterProperties, FILTER_DRAW_MODE } from '../../../../common'; +import { FilterByRectangle } from './filter_by_rectangle'; interface SpatialFilterToolBarProps { setFilterProperties: (properties: DrawFilterProperties) => void; - isDrawActive: boolean; + mode: FILTER_DRAW_MODE; } -export const SpatialFilterToolbar = ({ - setFilterProperties, - isDrawActive, -}: SpatialFilterToolBarProps) => { - const onCancel = () => { - setFilterProperties({ - mode: FILTER_DRAW_MODE.NONE, - }); - }; +export const SpatialFilterToolbar = ({ setFilterProperties, mode }: SpatialFilterToolBarProps) => { + const isDrawActive: boolean = mode !== FILTER_DRAW_MODE.NONE; const filterIconGroups = ( @@ -32,18 +25,6 @@ export const SpatialFilterToolbar = ({ /> ); - if (isDrawActive) { - return ( - - - - {'Cancel'} - - - {filterIconGroups} - - ); - } return ( {filterIconGroups} From b7c1f9ae6f4caa56621ddf38aef10901c30f74c0 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 29 Mar 2023 18:27:42 +0530 Subject: [PATCH 53/77] Add support to build GeoShapeFilterMeta and GeoShapeFilter (#360) Add support to build GeoShapeFilterMeta and GeoShapeFilter Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + public/model/geo/filter.test.ts | 90 ++++++++++++++++++++++++++------- public/model/geo/filter.ts | 57 ++++++++++++--------- 3 files changed, 105 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7bba741..0ebbba23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) * Add support to draw rectangle shape to filter documents ([#348](https://github.com/opensearch-project/dashboards-maps/pull/348)) * Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) +* Add support to build GeoShapeFilterMeta and GeoShapeFilter ([#360](https://github.com/opensearch-project/dashboards-maps/pull/360)) * Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) ### Bug Fixes diff --git a/public/model/geo/filter.test.ts b/public/model/geo/filter.test.ts index 212b58c1..49d21c6d 100644 --- a/public/model/geo/filter.test.ts +++ b/public/model/geo/filter.test.ts @@ -5,9 +5,18 @@ import { LngLat } from 'maplibre-gl'; import { GeoBounds } from '../map/boundary'; -import { FilterMeta, FILTERS, GeoBoundingBoxFilter } from '../../../../../src/plugins/data/common'; -import { buildBBoxFilter, buildSpatialGeometryFilter, GeoShapeFilter } from './filter'; +import { + FilterMeta, + FILTERS, + GeoBoundingBoxFilter, + GeoShapeFilterMeta, + GeoShapeFilter, + ShapeFilter, + FilterStateStore, +} from '../../../../../src/plugins/data/common'; +import { buildBBoxFilter, buildGeoShapeFilter, buildGeoShapeFilterMeta } from './filter'; import { Polygon } from 'geojson'; +import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; describe('test bounding box filter', function () { it('should return valid bounding box', function () { @@ -57,25 +66,31 @@ describe('test geo shape filter', function () { }; const mockLabel: string = 'mypolygon'; const fieldName: string = 'location'; + const expectedParams: { + shape: ShapeFilter; + relation: GeoShapeRelation; + } = { + shape: mockPolygon, + relation: 'intersects', + }; + const mockFilterMeta: GeoShapeFilterMeta = { + alias: mockLabel, + disabled: false, + negate: false, + type: FILTERS.GEO_SHAPE, + params: expectedParams, + }; + const geoShapeFilter: GeoShapeFilter = buildGeoShapeFilter(fieldName, mockFilterMeta); - const geoShapeFilter: GeoShapeFilter = buildSpatialGeometryFilter( - fieldName, - mockPolygon, - mockLabel, - 'INTERSECTS' - ); - const expectedFilter: GeoShapeFilter = { - meta: { - alias: mockLabel, - disabled: false, - negate: false, - key: 'location', - type: FILTERS.SPATIAL_FILTER, - }, + const expectedFilterMeta: GeoShapeFilterMeta = { + ...mockFilterMeta, + key: fieldName, + }; + const expectedFilter = { geo_shape: { ignore_unmapped: true, location: { - relation: 'INTERSECTS', + relation: 'intersects', shape: { type: 'Polygon', coordinates: [ @@ -90,6 +105,45 @@ describe('test geo shape filter', function () { }, }, }; - expect(geoShapeFilter).toEqual(expectedFilter); + expect(geoShapeFilter.geo_shape).toEqual(expectedFilter.geo_shape); + expect(geoShapeFilter.meta).toEqual(expectedFilterMeta); + expect(geoShapeFilter.$state?.store).toEqual(FilterStateStore.APP_STATE); + }); +}); + +describe('build GeoShapeFilterMeta', function () { + it('should return valid filter meta', function () { + + const mockPolygon: Polygon = { + type: 'Polygon', + coordinates: [ + [ + [74.006, 40.7128], + [71.0589, 42.3601], + [73.7562, 42.6526], + [74.006, 40.7128], + ], + ], + }; + const actualFilter: GeoShapeFilterMeta = buildGeoShapeFilterMeta( + 'label', + mockPolygon, + 'intersects' + ); + const expectedParams: { + shape: ShapeFilter; + relation: GeoShapeRelation; + } = { + shape: mockPolygon, + relation: 'intersects', + }; + const expectedFilter: GeoShapeFilterMeta = { + disabled: false, + negate: false, + alias: 'label', + type: FILTERS.GEO_SHAPE, + params: expectedParams, + }; + expect(actualFilter).toEqual(expectedFilter); }); }); diff --git a/public/model/geo/filter.ts b/public/model/geo/filter.ts index ee6afdcf..340fbef6 100644 --- a/public/model/geo/filter.ts +++ b/public/model/geo/filter.ts @@ -3,23 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { LatLon } from '@opensearch-project/opensearch/api/types'; -import { Polygon } from 'geojson'; +import { GeoShapeRelation, LatLon } from '@opensearch-project/opensearch/api/types'; import { - Filter, FilterMeta, FILTERS, + FilterState, + FilterStateStore, GeoBoundingBoxFilter, + GeoShapeFilter, + GeoShapeFilterMeta, + ShapeFilter, } from '../../../../../src/plugins/data/common'; import { GeoBounds } from '../map/boundary'; -export type FilterRelations = 'INTERSECTS' | 'DISJOINT' | 'WITHIN'; - -export type GeoShapeFilter = Filter & { - meta: FilterMeta; - geo_shape: any; -}; - export const buildBBoxFilter = ( fieldName: string, mapBounds: GeoBounds, @@ -50,28 +46,39 @@ export const buildBBoxFilter = ( }; }; -export const buildSpatialGeometryFilter = ( - fieldName: string, - filterShape: Polygon, - filterLabel: string, - relation: FilterRelations -): GeoShapeFilter => { - const meta: FilterMeta = { - negate: false, - key: fieldName, +export const buildGeoShapeFilterMeta = ( + filterLabel: string | null, + filterShape: ShapeFilter, + relation: GeoShapeRelation +): GeoShapeFilterMeta => { + return { + type: FILTERS.GEO_SHAPE, alias: filterLabel, - type: FILTERS.SPATIAL_FILTER, disabled: false, + params: { + relation, + shape: filterShape, + }, + negate: false, }; +}; +export const buildGeoShapeFilter = ( + fieldName: string, + filterMeta: GeoShapeFilterMeta +): GeoShapeFilter => { + const $state: FilterState = { + store: FilterStateStore.APP_STATE, + }; return { - meta, + meta: { + ...filterMeta, + key: fieldName, + }, geo_shape: { ignore_unmapped: true, - [fieldName]: { - relation, - shape: filterShape, - }, + [fieldName]: filterMeta.params, }, + $state, }; }; From 4fbd8b6aec2de4a40a7de961adeae14a10704c75 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Wed, 29 Mar 2023 23:21:20 +0530 Subject: [PATCH 54/77] Update listener on KeyUp (#364) Update esc key press check on key up. Update popup information. Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + public/components/draw/modes/rectangle.ts | 12 +++++--- .../toolbar/spatial_filter/draw_tooltip.tsx | 30 +++++++++++-------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebbba23..96cddca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) * Add support to build GeoShapeFilterMeta and GeoShapeFilter ([#360](https://github.com/opensearch-project/dashboards-maps/pull/360)) * Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) +* Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/public/components/draw/modes/rectangle.ts b/public/components/draw/modes/rectangle.ts index 7af205e3..c99f5a0c 100644 --- a/public/components/draw/modes/rectangle.ts +++ b/public/components/draw/modes/rectangle.ts @@ -5,7 +5,7 @@ import { Feature, GeoJSON, Position } from 'geojson'; import { DrawCustomMode, DrawFeature, DrawPolygon, MapMouseEvent } from '@mapbox/mapbox-gl-draw'; -import {isEscapeKey} from "../../../../common/util"; +import { isEscapeKey } from '../../../../common/util'; // converted to typescript from // https://github.com/mapbox/geojson.io/blob/main/src/ui/draw/rectangle.js @@ -97,13 +97,17 @@ export const DrawRectangle: DrawCustomMode = { } }, onKeyUp(state: DrawRectangleState, e: KeyboardEvent) { - if (isEscapeKey(e)) { - // delete feature on Escape, else, onStop will append feature and fires draw.create event + if (!isEscapeKey(e)) { + return; + } + // delete feature on Escape, else, onStop will append feature and fires draw.create event + // @ts-ignore + if (state.id && this.getFeature(state.id)) { // @ts-ignore this.deleteFeature([state.id], { silent: true }); // change mode to simple select if escape is pressed // @ts-ignore - this.changeMode('simple_select'); + this.changeMode('simple_select', {}, { silent: true }); } }, onStop(state: DrawRectangleState) { diff --git a/public/components/toolbar/spatial_filter/draw_tooltip.tsx b/public/components/toolbar/spatial_filter/draw_tooltip.tsx index ab90107e..550af8d4 100644 --- a/public/components/toolbar/spatial_filter/draw_tooltip.tsx +++ b/public/components/toolbar/spatial_filter/draw_tooltip.tsx @@ -7,7 +7,7 @@ import maplibregl, { Map as Maplibre, MapEventType, Popup } from 'maplibre-gl'; import React, { Fragment, useEffect, useRef } from 'react'; import { i18n } from '@osd/i18n'; import { FILTER_DRAW_MODE } from '../../../../common'; -import {isEscapeKey} from "../../../../common/util"; +import { isEscapeKey } from '../../../../common/util'; interface Props { map: Maplibre; @@ -17,20 +17,24 @@ interface Props { const X_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP = -12; const Y_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP = 0; +const KEY_UP_EVENT_TYPE = 'keyup'; +const MOUSE_MOVE_EVENT_TYPE = 'mousemove'; +const MOUSE_OUT_EVENT_TYPE = 'mouseout'; const getTooltipContent = (mode: FILTER_DRAW_MODE): string => { switch (mode) { case FILTER_DRAW_MODE.POLYGON: return i18n.translate('maps.drawFilterPolygon.tooltipContent', { - defaultMessage: 'Click to start shape. Click for vertex. Double click to finish.', + defaultMessage: + 'Click to start shape. Click for vertex. Double click to finish, [esc] to cancel', }); case FILTER_DRAW_MODE.RECTANGLE: return i18n.translate('maps.drawFilterRectangle.tooltipContent', { - defaultMessage: 'Click and drag to draw rectangle.', + defaultMessage: 'Click and drag to draw rectangle, [esc] to cancel', }); default: return i18n.translate('maps.drawFilterDefault.tooltipContent', { - defaultMessage: 'Click to start shape. Double click to finish.', + defaultMessage: 'Click to start shape. Double click to finish, [esc] to cancel', }); } }; @@ -47,7 +51,7 @@ export const DrawTooltip = ({ map, mode, onCancel }: Props) => { useEffect(() => { // remove previous popup - function onMouseMoveMap(e: MapEventType['mousemove']) { + function onMouseMove(e: MapEventType['mousemove']) { map.getCanvas().style.cursor = 'crosshair'; // Changes cursor to '+' hoverPopupRef.current .setLngLat(e.lngLat) @@ -56,11 +60,11 @@ export const DrawTooltip = ({ map, mode, onCancel }: Props) => { .addTo(map); } - function onMouseMoveOut() { + function onMouseOut() { hoverPopupRef.current.remove(); } - function onKeyDown(e: KeyboardEvent) { + function onKeyUp(e: KeyboardEvent) { if (isEscapeKey(e)) { onCancel(); } @@ -70,18 +74,18 @@ export const DrawTooltip = ({ map, mode, onCancel }: Props) => { map.getCanvas().style.cursor = ''; hoverPopupRef.current.remove(); // remove tooltip when users mouse move over a point - map.off('mousemove', onMouseMoveMap); - map.off('mouseout', onMouseMoveOut); - map.getContainer().removeEventListener('keydown', onKeyDown); + map.off(MOUSE_MOVE_EVENT_TYPE, onMouseMove); + map.off(MOUSE_OUT_EVENT_TYPE, onMouseOut); + map.getContainer().removeEventListener(KEY_UP_EVENT_TYPE, onKeyUp); } if (map && mode === FILTER_DRAW_MODE.NONE) { resetAction(); } else { // add tooltip when users mouse move over a point - map.on('mousemove', onMouseMoveMap); - map.on('mouseout', onMouseMoveOut); - map.getContainer().addEventListener('keydown', onKeyDown); + map.on(MOUSE_MOVE_EVENT_TYPE, onMouseMove); + map.on(MOUSE_OUT_EVENT_TYPE, onMouseOut); + map.getContainer().addEventListener(KEY_UP_EVENT_TYPE, onKeyUp); } return () => { // remove tooltip when users mouse move over a point From b7aa8473b6f6db31f8ee07c2628dffb7414c769d Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Thu, 30 Mar 2023 05:37:09 +0530 Subject: [PATCH 55/77] Add geoshape filter while render data layers (#365) Add geoshape filter to Map state. Refresh data layers if filter is updated. Add filter based on field type. Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + common/index.ts | 1 + .../document_layer_source.tsx | 16 +++++++++++++ .../map_container/map_container.tsx | 5 ++++ public/model/layerRenderController.ts | 23 ++++++++++++++++++- public/model/mapLayerType.ts | 1 + public/model/mapState.ts | 3 ++- public/utils/getIntialConfig.ts | 2 ++ 8 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96cddca6..29c58d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) * Add support to build GeoShapeFilterMeta and GeoShapeFilter ([#360](https://github.com/opensearch-project/dashboards-maps/pull/360)) * Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) +* Add geoshape filter while render data layers ([#365](https://github.com/opensearch-project/dashboards-maps/pull/365) * Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) ### Bug Fixes diff --git a/common/index.ts b/common/index.ts index c6160e8c..921bf002 100644 --- a/common/index.ts +++ b/common/index.ts @@ -32,6 +32,7 @@ export const DOCUMENTS_MAX_MARKER_BORDER_THICKNESS = 100; export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; export const DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER: boolean = true; +export const DOCUMENTS_DEFAULT_APPLY_GLOBAL_FILTERS: boolean = true; export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; export const DOCUMENTS_DEFAULT_LABEL_ENABLES: boolean = false; export enum DOCUMENTS_LABEL_TEXT_TYPE { diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index c66ab09d..80b204a5 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -235,6 +235,11 @@ export const DocumentLayerSource = ({ setSelectedLayerConfig({ ...selectedLayerConfig, source }); }; + const onApplyGlobalFilters = (e: React.ChangeEvent) => { + const source = { ...selectedLayerConfig.source, applyGlobalFilters: e.target.checked }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + const shouldTooltipSectionOpen = () => { return ( selectedLayerConfig.source.showTooltips && @@ -357,6 +362,17 @@ export const DocumentLayerSource = ({ compressed />
+ + + diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index f85df4d6..822a7d22 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -161,6 +161,11 @@ export const MapContainer = ({ return () => clearInterval(intervalId); }, [refreshConfig]); + // Update data layers when global filter is updated + useEffect(() => { + renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + }, [mapState.spatialMetaFilters]); + useEffect(() => { if (!mounted || layers.length <= 0) { return; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index c1ef9530..3d4fdf7c 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -4,6 +4,7 @@ */ import { Map as Maplibre } from 'maplibre-gl'; +import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; import { MapLayerSpecification } from './mapLayerType'; import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; import { @@ -22,12 +23,23 @@ import { getDataLayers, getBaseLayers, layersFunctionMap } from './layersFunctio import { MapServices } from '../types'; import { MapState } from './mapState'; import { GeoBounds, getBounds } from './map/boundary'; -import { buildBBoxFilter } from './geo/filter'; +import { buildBBoxFilter, buildGeoShapeFilter } from './geo/filter'; interface MaplibreRef { current: Maplibre | null; } +const getSupportedOperations = (field: string): GeoShapeRelation[] => { + switch (field) { + case 'geo_point': + return ['intersects']; + case 'geo_shape': + return ['intersects', 'within', 'disjoint']; + default: + return []; + } +}; + export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, @@ -125,6 +137,15 @@ export const handleDataLayerRender = ( const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); filters.push(geoBoundingBoxFilter); + // build and add GeoShape filters from map state if applicable + if (mapLayer.source?.applyGlobalFilter ?? true) { + mapState?.spatialMetaFilters?.map((value) => { + if (getSupportedOperations(geoFieldType).includes(value.params.relation)) { + filters.push(buildGeoShapeFilter(geoField, value)); + } + }); + } + return prepareDataLayerSource(mapLayer, mapState, services, filters, timeRange, query).then( (result) => { const { layer, dataSource } = result; diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index 2ae4a0c3..e73b2564 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -47,6 +47,7 @@ export type DocumentLayerSpecification = AbstractLayerSpecification & { tooltipFields: string[]; useGeoBoundingBoxFilter: boolean; filters: Filter[]; + applyGlobalFilters?: boolean; }; style: { fillColor: string; diff --git a/public/model/mapState.ts b/public/model/mapState.ts index 4d4ceddc..f7648090 100644 --- a/public/model/mapState.ts +++ b/public/model/mapState.ts @@ -1,4 +1,4 @@ -import { Query, TimeRange } from '../../../../src/plugins/data/common'; +import { GeoShapeFilterMeta, Query, TimeRange } from '../../../../src/plugins/data/common'; export interface MapState { timeRange: TimeRange; @@ -7,4 +7,5 @@ export interface MapState { pause: boolean; value: number; }; + spatialMetaFilters?: GeoShapeFilterMeta[]; } diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts index 30585648..bbe696b3 100644 --- a/public/utils/getIntialConfig.ts +++ b/public/utils/getIntialConfig.ts @@ -25,6 +25,7 @@ import { DOCUMENTS_DEFAULT_LABEL_COLOR, DOCUMENTS_DEFAULT_LABEL_BORDER_COLOR, DOCUMENTS_NONE_LABEL_BORDER_WIDTH, + DOCUMENTS_DEFAULT_APPLY_GLOBAL_FILTERS, } from '../../common'; import { MapState } from '../model/mapState'; import { ConfigSchema } from '../../common/config'; @@ -61,6 +62,7 @@ export const getLayerConfigMap = (mapConfig: ConfigSchema) => ({ tooltipFields: DOCUMENTS_DEFAULT_TOOLTIPS, showTooltips: DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, displayTooltipsOnHover: DOCUMENTS_DEFAULT_DISPLAY_TOOLTIPS_ON_HOVER, + applyGlobalFilters: DOCUMENTS_DEFAULT_APPLY_GLOBAL_FILTERS, }, style: { ...getStyleColor(), From 5bc78f496ddc143269a69cad93c9dda2dc3614b7 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Tue, 4 Apr 2023 21:47:37 +0530 Subject: [PATCH 56/77] Update draw filter shape ui properties (#372) * Update draw filter shape 1. updat style after consulting with ux 2. Move draw shap helper from tooltip to fixed anchor. 3. Change icon 4. Cancel drawing either from helper or clicking shape toolbar again. 5. Create single component for polygon and rectangle. Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + common/index.ts | 1 + .../map_container/map_container.scss | 11 +- .../map_container/map_container.tsx | 22 +-- .../filter-by_shape.test.tsx.snap | 165 ++++++++++++++++++ .../filter_by_polygon.test.tsx.snap | 113 ------------ .../filter_by_rectangle.test.tsx.snap | 113 ------------ .../filter_toolbar.test.tsx.snap | 40 +++-- .../display_draw_helper.test.tsx | 123 +++++++++++++ .../spatial_filter/display_draw_helper.tsx | 62 +++++++ .../spatial_filter/draw_filter_shape.tsx | 2 + .../toolbar/spatial_filter/draw_style.ts | 57 ++++++ .../toolbar/spatial_filter/draw_tooltip.tsx | 98 ----------- .../spatial_filter/filter-by_shape.test.tsx | 86 +++++++++ .../spatial_filter/filter_by_polygon.test.tsx | 23 --- .../filter_by_rectangle.test.tsx | 23 --- .../spatial_filter/filter_by_rectangle.tsx | 90 ---------- ...ter_by_polygon.tsx => filter_by_shape.tsx} | 65 ++++--- .../toolbar/spatial_filter/filter_toolbar.tsx | 46 +++-- public/images/polygon.svg | 5 +- 20 files changed, 621 insertions(+), 525 deletions(-) create mode 100644 public/components/toolbar/spatial_filter/__snapshots__/filter-by_shape.test.tsx.snap delete mode 100644 public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap delete mode 100644 public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap create mode 100644 public/components/toolbar/spatial_filter/display_draw_helper.test.tsx create mode 100644 public/components/toolbar/spatial_filter/display_draw_helper.tsx create mode 100644 public/components/toolbar/spatial_filter/draw_style.ts delete mode 100644 public/components/toolbar/spatial_filter/draw_tooltip.tsx create mode 100644 public/components/toolbar/spatial_filter/filter-by_shape.test.tsx delete mode 100644 public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx delete mode 100644 public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx delete mode 100644 public/components/toolbar/spatial_filter/filter_by_rectangle.tsx rename public/components/toolbar/spatial_filter/{filter_by_polygon.tsx => filter_by_shape.tsx} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c58d23..91d3a502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) * Add geoshape filter while render data layers ([#365](https://github.com/opensearch-project/dashboards-maps/pull/365) * Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) +* Update draw filter shape ui properties ([#372](https://github.com/opensearch-project/dashboards-maps/pull/372)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/common/index.ts b/common/index.ts index 921bf002..6e750399 100644 --- a/common/index.ts +++ b/common/index.ts @@ -180,5 +180,6 @@ export const DRAW_FILTER_SHAPE_TITLE = 'DRAW SHAPE'; export const DRAW_FILTER_POLYGON_DEFAULT_LABEL = 'polygon'; export const DRAW_FILTER_RECTANGLE_DEFAULT_LABEL = 'rectangle'; export const DRAW_FILTER_POLYGON = 'Draw Polygon'; +export const DRAW_FILTER_CANCEL = 'Cancel'; export const DRAW_FILTER_RECTANGLE = 'Draw Rectangle'; export const DRAW_FILTER_SPATIAL_RELATIONS = ['intersects', 'disjoint', 'within']; diff --git a/public/components/map_container/map_container.scss b/public/components/map_container/map_container.scss index d659b5a9..8e3f8cfb 100644 --- a/public/components/map_container/map_container.scss +++ b/public/components/map_container/map_container.scss @@ -7,6 +7,7 @@ @import "../../variables"; .mapAppContainer, .map-page, .map-container, .map-main{ + position: relative; display: flex; flex-direction: column; flex: 1; @@ -34,6 +35,14 @@ .SpatialFilterToolbar-container { z-index: 1; position: absolute; - top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 5); + top: (2 * $euiSizeS) + ($euiSizeXL * 4); right: $euiSizeS; } + +.drawFilterShapeHelper { + z-index: 1; + position: absolute; + transform: translate(-50%, -50%); + bottom: 1px; + left: 50%; +} diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 822a7d22..820b848c 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -35,7 +35,7 @@ import { MapsFooter } from './maps_footer'; import { DisplayFeatures } from '../tooltip/display_features'; import { TOOLTIP_STATE } from '../../../common'; import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; -import { DrawTooltip } from '../toolbar/spatial_filter/draw_tooltip'; +import { DrawFilterShapeHelper } from '../toolbar/spatial_filter/display_draw_helper'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -224,7 +224,14 @@ export const MapContainer = ({ return (
- {mounted && } + {mounted && maplibreRef.current && } + {mounted && maplibreRef.current && ( + setFilterProperties({ mode: FILTER_DRAW_MODE.NONE })} + /> + )} {mounted && ( )} - {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && ( - - )} - {mounted && Boolean(maplibreRef.current) && ( - setFilterProperties({ mode: FILTER_DRAW_MODE.NONE })} - /> + {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && maplibreRef.current && ( + )} {mounted && maplibreRef.current && tooltipState === TOOLTIP_STATE.FILTER_DRAW_SHAPE && ( +
+
+ +
+
+
+`; + +exports[`render polygon renders filter by polygon option 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`render rectangle renders filter by rectangle in middle of drawing 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`render rectangle renders filter by rectangle option 1`] = ` +
+
+
+ +
+
+
+`; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap deleted file mode 100644 index 13cc521c..00000000 --- a/public/components/toolbar/spatial_filter/__snapshots__/filter_by_polygon.test.tsx.snap +++ /dev/null @@ -1,113 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders filter by polygon button 1`] = ` - - - - } - closePopover={[Function]} - data-test-subj="drawPolygonPopOver" - display="inlineBlock" - hasArrow={true} - id="drawPolygonId" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" -> - , - "id": 0, - "title": "DRAW SHAPE", - }, - ] - } - size="m" - /> - -`; - -exports[`renders filter by polygon in middle of drawing 1`] = ` - - - - } - closePopover={[Function]} - data-test-subj="drawPolygonPopOver" - display="inlineBlock" - hasArrow={true} - id="drawPolygonId" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" -> - , - "id": 0, - "title": "DRAW SHAPE", - }, - ] - } - size="m" - /> - -`; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap deleted file mode 100644 index 3dc50dad..00000000 --- a/public/components/toolbar/spatial_filter/__snapshots__/filter_by_rectangle.test.tsx.snap +++ /dev/null @@ -1,113 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders filter by rectangle button 1`] = ` - - - - } - closePopover={[Function]} - data-test-subj="drawRectanglePopOver" - display="inlineBlock" - hasArrow={true} - id="drawRectangleId" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" -> - , - "id": 0, - "title": "DRAW SHAPE", - }, - ] - } - size="m" - /> - -`; - -exports[`renders filter by rectangle in middle of drawing 1`] = ` - - - - } - closePopover={[Function]} - data-test-subj="drawRectanglePopOver" - display="inlineBlock" - hasArrow={true} - id="drawRectangleId" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" -> - , - "id": 0, - "title": "DRAW SHAPE", - }, - ] - } - size="m" - /> - -`; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap index 2b4aac5a..09b84779 100644 --- a/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap @@ -2,19 +2,27 @@ exports[`renders spatial filter before drawing 1`] = ` - - + + @@ -22,19 +30,27 @@ exports[`renders spatial filter before drawing 1`] = ` exports[`renders spatial filter while drawing 1`] = ` - - + + diff --git a/public/components/toolbar/spatial_filter/display_draw_helper.test.tsx b/public/components/toolbar/spatial_filter/display_draw_helper.test.tsx new file mode 100644 index 00000000..ec2bbebc --- /dev/null +++ b/public/components/toolbar/spatial_filter/display_draw_helper.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as MapLibre } from 'maplibre-gl'; + +jest.mock('maplibre-gl', () => ({ + Map: jest.fn(() => ({ + getCanvas: jest.fn(() => { + return { + style: { + cursor: '', + }, + }; + }), + })), +})); + +import { DrawFilterShapeHelper } from './display_draw_helper'; +import React from 'react'; +import { FILTER_DRAW_MODE } from '../../../../common'; +import { act } from 'react-dom/test-utils'; +import { render, unmountComponentAtNode } from 'react-dom'; +import TestRenderer from 'react-test-renderer'; +import { EuiButton } from '@elastic/eui'; + +let container: Element | null; + +beforeEach(() => { + // setup a DOM element as a render target + container = document.createElement('div'); + document.body.appendChild(container); +}); + +afterEach(() => { + // cleanup on exiting + unmountComponentAtNode(container!); + container?.remove(); + container = null; +}); + +describe('test draw filter helper displays valid content', function () { + it('renders filter by polygon helper content', () => { + const mockCallback = jest.fn(); + const map = new MapLibre({ + container: document.createElement('div'), + style: { + layers: [], + version: 8 as 8, + sources: {}, + }, + }); + act(() => { + render( + , + container + ); + }); + expect(container?.textContent).toBe( + 'Click to start shape. Click for vertex. Double click to finish.Cancel' + ); + }); + it('renders filter by rectangle helper content', () => { + const mockCallback = jest.fn(); + const map = new MapLibre({ + container: document.createElement('div'), + style: { + layers: [], + version: 8 as 8, + sources: {}, + }, + }); + act(() => { + render( + , + container + ); + }); + expect(container?.textContent).toBe( + 'Click to start rectangle. Move mouse to adjust size. Click to finish.Cancel' + ); + }); + it('dont render helper content', () => { + const mockCallback = jest.fn(); + const map = new MapLibre({ + container: document.createElement('div'), + style: { + layers: [], + version: 8 as 8, + sources: {}, + }, + }); + act(() => { + render( + , + container + ); + }); + expect(container?.textContent).toBe(''); + }); + it('check cancel is called', () => { + const mockCallback = jest.fn(); + const map = new MapLibre({ + container: document.createElement('div'), + style: { + layers: [], + version: 8 as 8, + sources: {}, + }, + }); + const helper = TestRenderer.create( + + ); + const button = helper.root.findByType(EuiButton); + button.props.onClick(); + expect(mockCallback).toBeCalledTimes(1); + }); +}); diff --git a/public/components/toolbar/spatial_filter/display_draw_helper.tsx b/public/components/toolbar/spatial_filter/display_draw_helper.tsx new file mode 100644 index 00000000..fbcd15d2 --- /dev/null +++ b/public/components/toolbar/spatial_filter/display_draw_helper.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import React, { memo, useEffect } from 'react'; +import { i18n } from '@osd/i18n'; +import { Map as Maplibre } from 'maplibre-gl'; +import { FILTER_DRAW_MODE } from '../../../../common'; + +interface DrawFilterShapeHelper { + map: Maplibre; + mode: FILTER_DRAW_MODE; + onCancel: () => void; +} + +const getHelpText = (mode: FILTER_DRAW_MODE): string => { + switch (mode) { + case FILTER_DRAW_MODE.POLYGON: + return i18n.translate('maps.drawFilterPolygon.tooltipContent', { + defaultMessage: 'Click to start shape. Click for vertex. Double click to finish.', + }); + case FILTER_DRAW_MODE.RECTANGLE: + return i18n.translate('maps.drawFilterRectangle.tooltipContent', { + defaultMessage: 'Click to start rectangle. Move mouse to adjust size. Click to finish.', + }); + default: + return i18n.translate('maps.drawFilterDefault.tooltipContent', { + defaultMessage: 'Click to start shape. Double click to finish.', + }); + } +}; +export const DrawFilterShapeHelper = memo(({ map, mode, onCancel }: DrawFilterShapeHelper) => { + useEffect(() => { + if (mode !== FILTER_DRAW_MODE.NONE) { + map.getCanvas().style.cursor = 'crosshair'; // Changes cursor to '+' + } else { + map.getCanvas().style.cursor = ''; + } + }, [mode]); + + return mode === FILTER_DRAW_MODE.NONE ? null : ( + + + + {getHelpText(mode)} + + + + {'Cancel'} + + + + + ); +}); diff --git a/public/components/toolbar/spatial_filter/draw_filter_shape.tsx b/public/components/toolbar/spatial_filter/draw_filter_shape.tsx index 36f384c2..83a150d1 100644 --- a/public/components/toolbar/spatial_filter/draw_filter_shape.tsx +++ b/public/components/toolbar/spatial_filter/draw_filter_shape.tsx @@ -14,6 +14,7 @@ import { MAPBOX_GL_DRAW_CREATE_LISTENER, } from '../../../../common'; import { DrawRectangle } from '../../draw/modes/rectangle'; +import {DRAW_SHAPE_STYLE} from "./draw_style"; interface DrawFilterShapeProps { filterProperties: DrawFilterProperties; @@ -49,6 +50,7 @@ export const DrawFilterShape = ({ ...MapboxDraw.modes, [MAPBOX_GL_DRAW_MODES.DRAW_RECTANGLE]: DrawRectangle, }, + styles: DRAW_SHAPE_STYLE, }) ); diff --git a/public/components/toolbar/spatial_filter/draw_style.ts b/public/components/toolbar/spatial_filter/draw_style.ts new file mode 100644 index 00000000..ea1217c9 --- /dev/null +++ b/public/components/toolbar/spatial_filter/draw_style.ts @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export const DRAW_SHAPE_STYLE = [ + { + id: 'gl-draw-line', + type: 'line', + filter: ['all', ['==', '$type', 'LineString'], ['!=', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#845D10', + 'line-dasharray': [0.2, 2], + 'line-width': 2, + }, + }, + // polygon fill + { + id: 'gl-draw-polygon-fill', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], + paint: { + 'fill-color': '#845D10', + 'fill-outline-color': '#845D10', + 'fill-opacity': 0.2, + }, + }, + // polygon outline stroke + // This doesn't style the first edge of the polygon, which uses the line stroke styling instead + { + id: 'gl-draw-polygon-stroke-active', + type: 'line', + filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#845D10', + 'line-dasharray': [0.2, 2], + 'line-width': 2, + }, + }, + // vertex points + { + id: 'gl-draw-polygon-and-line-vertex-active', + type: 'circle', + filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + paint: { + 'circle-radius': 3, + 'circle-color': '#845D10', + }, + }, +]; diff --git a/public/components/toolbar/spatial_filter/draw_tooltip.tsx b/public/components/toolbar/spatial_filter/draw_tooltip.tsx deleted file mode 100644 index 550af8d4..00000000 --- a/public/components/toolbar/spatial_filter/draw_tooltip.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import maplibregl, { Map as Maplibre, MapEventType, Popup } from 'maplibre-gl'; -import React, { Fragment, useEffect, useRef } from 'react'; -import { i18n } from '@osd/i18n'; -import { FILTER_DRAW_MODE } from '../../../../common'; -import { isEscapeKey } from '../../../../common/util'; - -interface Props { - map: Maplibre; - mode: FILTER_DRAW_MODE; - onCancel: () => void; -} - -const X_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP = -12; -const Y_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP = 0; -const KEY_UP_EVENT_TYPE = 'keyup'; -const MOUSE_MOVE_EVENT_TYPE = 'mousemove'; -const MOUSE_OUT_EVENT_TYPE = 'mouseout'; - -const getTooltipContent = (mode: FILTER_DRAW_MODE): string => { - switch (mode) { - case FILTER_DRAW_MODE.POLYGON: - return i18n.translate('maps.drawFilterPolygon.tooltipContent', { - defaultMessage: - 'Click to start shape. Click for vertex. Double click to finish, [esc] to cancel', - }); - case FILTER_DRAW_MODE.RECTANGLE: - return i18n.translate('maps.drawFilterRectangle.tooltipContent', { - defaultMessage: 'Click and drag to draw rectangle, [esc] to cancel', - }); - default: - return i18n.translate('maps.drawFilterDefault.tooltipContent', { - defaultMessage: 'Click to start shape. Double click to finish, [esc] to cancel', - }); - } -}; - -export const DrawTooltip = ({ map, mode, onCancel }: Props) => { - const hoverPopupRef = useRef( - new maplibregl.Popup({ - closeButton: false, - closeOnClick: false, - anchor: 'right', - maxWidth: 'max-content', - }) - ); - - useEffect(() => { - // remove previous popup - function onMouseMove(e: MapEventType['mousemove']) { - map.getCanvas().style.cursor = 'crosshair'; // Changes cursor to '+' - hoverPopupRef.current - .setLngLat(e.lngLat) - .setOffset([X_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP, Y_AXIS_GAP_BETWEEN_CURSOR_AND_POPUP]) // add some gap between cursor and pop up - .setText(getTooltipContent(mode)) - .addTo(map); - } - - function onMouseOut() { - hoverPopupRef.current.remove(); - } - - function onKeyUp(e: KeyboardEvent) { - if (isEscapeKey(e)) { - onCancel(); - } - } - - function resetAction() { - map.getCanvas().style.cursor = ''; - hoverPopupRef.current.remove(); - // remove tooltip when users mouse move over a point - map.off(MOUSE_MOVE_EVENT_TYPE, onMouseMove); - map.off(MOUSE_OUT_EVENT_TYPE, onMouseOut); - map.getContainer().removeEventListener(KEY_UP_EVENT_TYPE, onKeyUp); - } - - if (map && mode === FILTER_DRAW_MODE.NONE) { - resetAction(); - } else { - // add tooltip when users mouse move over a point - map.on(MOUSE_MOVE_EVENT_TYPE, onMouseMove); - map.on(MOUSE_OUT_EVENT_TYPE, onMouseOut); - map.getContainer().addEventListener(KEY_UP_EVENT_TYPE, onKeyUp); - } - return () => { - // remove tooltip when users mouse move over a point - // when component is unmounted - resetAction(); - }; - }, [mode]); - - return ; -}; diff --git a/public/components/toolbar/spatial_filter/filter-by_shape.test.tsx b/public/components/toolbar/spatial_filter/filter-by_shape.test.tsx new file mode 100644 index 00000000..53906e79 --- /dev/null +++ b/public/components/toolbar/spatial_filter/filter-by_shape.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import renderer from 'react-test-renderer'; +import React from 'react'; +import { FilterByShape } from './filter_by_shape'; +import { + DRAW_FILTER_POLYGON, + DRAW_FILTER_POLYGON_DEFAULT_LABEL, + DRAW_FILTER_RECTANGLE, + DRAW_FILTER_RECTANGLE_DEFAULT_LABEL, + FILTER_DRAW_MODE, +} from '../../../../common'; + +describe('render polygon', function () { + it('renders filter by polygon option', () => { + const mockCallback = jest.fn(); + const polygonComponent = renderer + .create( + + ) + .toJSON(); + expect(polygonComponent).toMatchSnapshot(); + }); + + it('renders filter by polygon in middle of drawing', () => { + const mockCallback = jest.fn(); + const polygonComponent = renderer + .create( + + ) + .toJSON(); + expect(polygonComponent).toMatchSnapshot(); + }); +}); + +describe('render rectangle', function () { + it('renders filter by rectangle option', () => { + const mockCallback = jest.fn(); + const rectangle = renderer + .create( + + ) + .toJSON(); + expect(rectangle).toMatchSnapshot(); + }); + + it('renders filter by rectangle in middle of drawing', () => { + const mockCallback = jest.fn(); + const rectangle = renderer + .create( + + ) + .toJSON(); + expect(rectangle).toMatchSnapshot(); + }); +}); diff --git a/public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx b/public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx deleted file mode 100644 index 4d00f984..00000000 --- a/public/components/toolbar/spatial_filter/filter_by_polygon.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { shallow } from 'enzyme'; -import React from 'react'; -import { FilterByPolygon } from './filter_by_polygon'; - -it('renders filter by polygon button', () => { - const mockCallback = jest.fn(); - const polygonComponent = shallow( - - ); - expect(polygonComponent).toMatchSnapshot(); -}); - -it('renders filter by polygon in middle of drawing', () => { - const mockCallback = jest.fn(); - const polygonComponent = shallow( - - ); - expect(polygonComponent).toMatchSnapshot(); -}); diff --git a/public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx b/public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx deleted file mode 100644 index 8b100b67..00000000 --- a/public/components/toolbar/spatial_filter/filter_by_rectangle.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { shallow } from 'enzyme'; -import React from 'react'; -import { FilterByRectangle } from './filter_by_rectangle'; - -it('renders filter by rectangle button', () => { - const mockCallback = jest.fn(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); -}); - -it('renders filter by rectangle in middle of drawing', () => { - const mockCallback = jest.fn(); - const component = shallow( - - ); - expect(component).toMatchSnapshot(); -}); diff --git a/public/components/toolbar/spatial_filter/filter_by_rectangle.tsx b/public/components/toolbar/spatial_filter/filter_by_rectangle.tsx deleted file mode 100644 index d018917e..00000000 --- a/public/components/toolbar/spatial_filter/filter_by_rectangle.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from 'react'; -import { EuiPopover, EuiContextMenu, EuiPanel, EuiButtonIcon } from '@elastic/eui'; -import { FilterInputPanel } from './filter_input_panel'; -// TODO: replace with rectangle image file once available -import rectangle from '../../../images/polygon.svg'; -import { - DrawFilterProperties, - DRAW_FILTER_SPATIAL_RELATIONS, - DRAW_FILTER_SHAPE_TITLE, - DRAW_FILTER_RECTANGLE, - DRAW_FILTER_RECTANGLE_DEFAULT_LABEL, -} from '../../../../common'; -import { FILTER_DRAW_MODE } from '../../../../common'; - -interface FilterByRectangleProps { - setDrawFilterProperties: (properties: DrawFilterProperties) => void; - isDrawActive: boolean; -} - -export const FilterByRectangle = ({ - setDrawFilterProperties, - isDrawActive, -}: FilterByRectangleProps) => { - const [isPopoverOpen, setPopover] = useState(false); - - const onClick = () => { - setPopover(!isPopoverOpen); - }; - - const closePopover = () => { - setPopover(false); - }; - - const onSubmit = (input: { relation: string; label: string; mode: FILTER_DRAW_MODE }) => { - setDrawFilterProperties({ - mode: input.mode, - relation: input.relation, - filterLabel: input.label, - }); - closePopover(); - }; - - const panels = [ - { - id: 0, - title: DRAW_FILTER_SHAPE_TITLE, - content: ( - - ), - }, - ]; - - const drawRectangleButton = ( - - - - ); - return ( - - - - ); -}; diff --git a/public/components/toolbar/spatial_filter/filter_by_polygon.tsx b/public/components/toolbar/spatial_filter/filter_by_shape.tsx similarity index 58% rename from public/components/toolbar/spatial_filter/filter_by_polygon.tsx rename to public/components/toolbar/spatial_filter/filter_by_shape.tsx index cf32c18a..caa2f2ab 100644 --- a/public/components/toolbar/spatial_filter/filter_by_polygon.tsx +++ b/public/components/toolbar/spatial_filter/filter_by_shape.tsx @@ -3,31 +3,44 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui'; import React, { useState } from 'react'; -import { EuiPopover, EuiContextMenu, EuiPanel, EuiButtonIcon } from '@elastic/eui'; -import { FilterInputPanel } from './filter_input_panel'; -import polygon from '../../../images/polygon.svg'; import { - DrawFilterProperties, - DRAW_FILTER_POLYGON, - DRAW_FILTER_POLYGON_DEFAULT_LABEL, - DRAW_FILTER_SPATIAL_RELATIONS, + DRAW_FILTER_CANCEL, DRAW_FILTER_SHAPE_TITLE, + DRAW_FILTER_SPATIAL_RELATIONS, + DrawFilterProperties, + FILTER_DRAW_MODE, } from '../../../../common'; -import { FILTER_DRAW_MODE } from '../../../../common'; +import { FilterInputPanel } from './filter_input_panel'; -interface FilterByPolygonProps { +interface FilterByShapeProps { setDrawFilterProperties: (properties: DrawFilterProperties) => void; - isDrawActive: boolean; + mode: FILTER_DRAW_MODE; + shapeMode: FILTER_DRAW_MODE; + shapeLabel: string; + defaultLabel: string; + iconType: any; } -export const FilterByPolygon = ({ +export const FilterByShape = ({ + shapeMode, setDrawFilterProperties, - isDrawActive, -}: FilterByPolygonProps) => { + mode, + defaultLabel, + iconType, + shapeLabel, +}: FilterByShapeProps) => { const [isPopoverOpen, setPopover] = useState(false); + const isFilterActive: boolean = mode === shapeMode; const onClick = () => { + if (isFilterActive) { + setDrawFilterProperties({ + mode: FILTER_DRAW_MODE.NONE, + }); + return; + } setPopover(!isPopoverOpen); }; @@ -50,38 +63,40 @@ export const FilterByPolygon = ({ title: DRAW_FILTER_SHAPE_TITLE, content: ( ), }, ]; - const drawPolygonButton = ( + const drawShapeButton = ( ); return ( diff --git a/public/components/toolbar/spatial_filter/filter_toolbar.tsx b/public/components/toolbar/spatial_filter/filter_toolbar.tsx index 59b99422..6ba7d225 100644 --- a/public/components/toolbar/spatial_filter/filter_toolbar.tsx +++ b/public/components/toolbar/spatial_filter/filter_toolbar.tsx @@ -5,9 +5,16 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FilterByPolygon } from './filter_by_polygon'; -import { DrawFilterProperties, FILTER_DRAW_MODE } from '../../../../common'; -import { FilterByRectangle } from './filter_by_rectangle'; +import { + DRAW_FILTER_POLYGON, + DRAW_FILTER_POLYGON_DEFAULT_LABEL, + DRAW_FILTER_RECTANGLE, + DRAW_FILTER_RECTANGLE_DEFAULT_LABEL, + DrawFilterProperties, + FILTER_DRAW_MODE, +} from '../../../../common'; +import { FilterByShape } from './filter_by_shape'; +import polygon from '../../../images/polygon.svg'; interface SpatialFilterToolBarProps { setFilterProperties: (properties: DrawFilterProperties) => void; @@ -15,19 +22,28 @@ interface SpatialFilterToolBarProps { } export const SpatialFilterToolbar = ({ setFilterProperties, mode }: SpatialFilterToolBarProps) => { - const isDrawActive: boolean = mode !== FILTER_DRAW_MODE.NONE; - const filterIconGroups = ( - - - - - ); return ( - - {filterIconGroups} + + + + + + + ); }; diff --git a/public/images/polygon.svg b/public/images/polygon.svg index 281b7b05..e8236186 100644 --- a/public/images/polygon.svg +++ b/public/images/polygon.svg @@ -1 +1,4 @@ - image/svg+xml \ No newline at end of file + + + + From 853aad8b2e449092e7dafbe9d100276395cebf11 Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Tue, 4 Apr 2023 22:34:42 +0530 Subject: [PATCH 57/77] Add filter bar to view spatial filter (#371) Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 1 + .../__snapshots__/filter_bar.test.tsx.snap | 80 ++++++++ .../__snapshots__/filter_editor.test.tsx.snap | 93 +++++++++ .../__snapshots__/filter_view.test.tsx.snap | 41 ++++ .../filter_bar/filter_actions.test.ts | 37 ++++ .../components/filter_bar/filter_actions.ts | 21 ++ .../components/filter_bar/filter_bar.test.tsx | 48 +++++ public/components/filter_bar/filter_bar.tsx | 122 ++++++++++++ .../filter_bar/filter_editor.test.tsx | 22 +++ .../components/filter_bar/filter_editor.tsx | 107 +++++++++++ public/components/filter_bar/filter_item.tsx | 179 ++++++++++++++++++ .../components/filter_bar/filter_options.tsx | 126 ++++++++++++ .../filter_bar/filter_view.test.tsx | 37 ++++ public/components/filter_bar/filter_view.tsx | 59 ++++++ .../map_container/map_container.tsx | 7 +- public/components/map_page/map_page.tsx | 43 ++++- .../spatial_filter/draw_filter_shape.tsx | 31 +++ .../spatial_filter/spatial_filter.scss | 3 +- public/model/layerRenderController.ts | 2 +- 19 files changed, 1054 insertions(+), 5 deletions(-) create mode 100644 public/components/filter_bar/__snapshots__/filter_bar.test.tsx.snap create mode 100644 public/components/filter_bar/__snapshots__/filter_editor.test.tsx.snap create mode 100644 public/components/filter_bar/__snapshots__/filter_view.test.tsx.snap create mode 100644 public/components/filter_bar/filter_actions.test.ts create mode 100644 public/components/filter_bar/filter_actions.ts create mode 100644 public/components/filter_bar/filter_bar.test.tsx create mode 100644 public/components/filter_bar/filter_bar.tsx create mode 100644 public/components/filter_bar/filter_editor.test.tsx create mode 100644 public/components/filter_bar/filter_editor.tsx create mode 100644 public/components/filter_bar/filter_item.tsx create mode 100644 public/components/filter_bar/filter_options.tsx create mode 100644 public/components/filter_bar/filter_view.test.tsx create mode 100644 public/components/filter_bar/filter_view.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d3a502..2056cacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Add geoshape filter while render data layers ([#365](https://github.com/opensearch-project/dashboards-maps/pull/365) * Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) * Update draw filter shape ui properties ([#372](https://github.com/opensearch-project/dashboards-maps/pull/372)) +* Add filter bar to display global geospatial filters ([#371](https://github.com/opensearch-project/dashboards-maps/pull/371)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/public/components/filter_bar/__snapshots__/filter_bar.test.tsx.snap b/public/components/filter_bar/__snapshots__/filter_bar.test.tsx.snap new file mode 100644 index 00000000..f4965916 --- /dev/null +++ b/public/components/filter_bar/__snapshots__/filter_bar.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders one filter inside filter bar 1`] = ` + + + + + + + + + + + + +`; diff --git a/public/components/filter_bar/__snapshots__/filter_editor.test.tsx.snap b/public/components/filter_bar/__snapshots__/filter_editor.test.tsx.snap new file mode 100644 index 00000000..9695c399 --- /dev/null +++ b/public/components/filter_bar/__snapshots__/filter_editor.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders one filter inside filter bar 1`] = ` +
+ + + + Edit Filter + + + +
+ + + + +
+ + + + +
+ + + + + Save + + + + + Cancel + + + + +
+
+
+`; diff --git a/public/components/filter_bar/__snapshots__/filter_view.test.tsx.snap b/public/components/filter_bar/__snapshots__/filter_view.test.tsx.snap new file mode 100644 index 00000000..20c66ac8 --- /dev/null +++ b/public/components/filter_bar/__snapshots__/filter_view.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test filter display renders filter display on disabled 1`] = ` + + mylabel + +`; + +exports[`test filter display renders filter display on enabled 1`] = ` + + mylabel + +`; diff --git a/public/components/filter_bar/filter_actions.test.ts b/public/components/filter_bar/filter_actions.test.ts new file mode 100644 index 00000000..fdbbaafe --- /dev/null +++ b/public/components/filter_bar/filter_actions.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import {disableGeoShapeFilterMeta, enableGeoShapeFilterMeta, toggleGeoShapeFilterMetaNegated} from './filter_actions'; +import { GeoShapeFilterMeta } from '../../../../../src/plugins/data/common'; + +describe('test filter actions', function () { + it('should enable GeoShapeFilterMeta', function () { + const updatedFilterMeta: GeoShapeFilterMeta = enableGeoShapeFilterMeta({ + alias: null, + negate: false, + params: {}, + disabled: true, + }); + expect(updatedFilterMeta.disabled).toEqual(false); + }); + it('should disable GeoShapeFilterMeta', function () { + const updatedFilterMeta: GeoShapeFilterMeta = disableGeoShapeFilterMeta({ + alias: null, + negate: false, + params: {}, + disabled: false, + }); + expect(updatedFilterMeta.disabled).toEqual(true); + }); + it('should toggle GeoShapeFilterMeta negation', function () { + const updatedFilterMeta: GeoShapeFilterMeta = toggleGeoShapeFilterMetaNegated({ + alias: null, + negate: false, + params: {}, + disabled: false, + }); + expect(updatedFilterMeta.negate).toEqual(true); + }); +}); diff --git a/public/components/filter_bar/filter_actions.ts b/public/components/filter_bar/filter_actions.ts new file mode 100644 index 00000000..2e0199f0 --- /dev/null +++ b/public/components/filter_bar/filter_actions.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GeoShapeFilterMeta } from '../../../../../src/plugins/data/common'; + +export const enableGeoShapeFilterMeta = (meta: GeoShapeFilterMeta) => + !meta.disabled ? meta : toggleGeoShapeFilterMetaDisabled(meta); + +export const disableGeoShapeFilterMeta = (meta: GeoShapeFilterMeta) => + meta.disabled ? meta : toggleGeoShapeFilterMetaDisabled(meta); + +export const toggleGeoShapeFilterMetaNegated = (meta: GeoShapeFilterMeta) => { + const negate = !meta.negate; + return { ...meta, negate }; +}; +export const toggleGeoShapeFilterMetaDisabled = (meta: GeoShapeFilterMeta) => { + const status: boolean = !meta.disabled; + return { ...meta, disabled: status }; +}; diff --git a/public/components/filter_bar/filter_bar.test.tsx b/public/components/filter_bar/filter_bar.test.tsx new file mode 100644 index 00000000..46b07090 --- /dev/null +++ b/public/components/filter_bar/filter_bar.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { FilterBar } from './filter_bar'; +import { FILTERS, GeoShapeFilterMeta, ShapeFilter } from '../../../../../src/plugins/data/common'; +import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; +import { Polygon } from 'geojson'; + +it('renders one filter inside filter bar', () => { + const mockCallback = jest.fn(); + const mockPolygon: Polygon = { + type: 'Polygon', + coordinates: [ + [ + [74.006, 40.7128], + [71.0589, 42.3601], + [73.7562, 42.6526], + [74.006, 40.7128], + ], + ], + }; + const expectedParams: { + shape: ShapeFilter; + relation: GeoShapeRelation; + } = { + shape: mockPolygon, + relation: 'intersects', + }; + const mockFilterMeta: GeoShapeFilterMeta = { + alias: 'mylabel', + disabled: false, + negate: false, + type: FILTERS.GEO_SHAPE, + params: expectedParams, + }; + const filterBar = shallow( + + ); + expect(filterBar).toMatchSnapshot(); +}); diff --git a/public/components/filter_bar/filter_bar.tsx b/public/components/filter_bar/filter_bar.tsx new file mode 100644 index 00000000..ec2a72fd --- /dev/null +++ b/public/components/filter_bar/filter_bar.tsx @@ -0,0 +1,122 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; + +import { FilterItem } from './filter_item'; +import { GeoShapeFilterMeta } from '../../../../../src/plugins/data/common'; +import { + disableGeoShapeFilterMeta, + enableGeoShapeFilterMeta, + toggleGeoShapeFilterMetaDisabled, + toggleGeoShapeFilterMetaNegated, +} from './filter_actions'; +import { FilterOptions } from './filter_options'; + +interface Props { + filters: GeoShapeFilterMeta[]; + onFiltersUpdated?: (filters: GeoShapeFilterMeta[]) => void; + className: string; +} +export const FilterBar = ({ filters, onFiltersUpdated, className }: Props) => { + function updateFilters(items: GeoShapeFilterMeta[]) { + if (onFiltersUpdated) { + onFiltersUpdated(items); + } + } + + function renderItems(filterMeta: GeoShapeFilterMeta[]) { + return filterMeta.map((meta, i) => ( + + onUpdate(i, newFilter)} + onRemove={() => onRemove(i)} + /> + + )); + } + + function onEnableAll() { + const updatedFilters: GeoShapeFilterMeta[] = filters.map(enableGeoShapeFilterMeta); + if (onFiltersUpdated) { + onFiltersUpdated(updatedFilters); + } + } + + function onDisableAll() { + const updatedFilters: GeoShapeFilterMeta[] = filters.map(disableGeoShapeFilterMeta); + if (onFiltersUpdated) { + onFiltersUpdated(updatedFilters); + } + } + + function onToggleAllNegated() { + const updatedFilters: GeoShapeFilterMeta[] = filters.map(toggleGeoShapeFilterMetaNegated); + if (onFiltersUpdated) { + onFiltersUpdated(updatedFilters); + } + } + + function onToggleAllDisabled() { + const updatedFilters: GeoShapeFilterMeta[] = filters.map(toggleGeoShapeFilterMetaDisabled); + if (onFiltersUpdated) { + onFiltersUpdated(updatedFilters); + } + } + + function onRemoveAll() { + if (onFiltersUpdated) { + onFiltersUpdated([]); + } + } + + function onRemove(i: number) { + const updatedFilters = [...filters]; + updatedFilters.splice(i, 1); + updateFilters(updatedFilters); + } + + function onUpdate(i: number, filter: GeoShapeFilterMeta) { + const current = [...filters]; + current[i] = filter; + updateFilters(current); + } + + const classes = classNames('globalFilterBar', className); + + return ( + + + + + + + {renderItems(filters)} + + + + ); +}; diff --git a/public/components/filter_bar/filter_editor.test.tsx b/public/components/filter_bar/filter_editor.test.tsx new file mode 100644 index 00000000..284cd3be --- /dev/null +++ b/public/components/filter_bar/filter_editor.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { FilterEditor } from './filter_editor'; + +it('renders one filter inside filter bar', () => { + const mockOnSubmitCallback = jest.fn(); + const mockOnCancelCallback = jest.fn(); + const filterEditor = shallow( + + ); + expect(filterEditor).toMatchSnapshot(); +}); diff --git a/public/components/filter_bar/filter_editor.tsx b/public/components/filter_bar/filter_editor.tsx new file mode 100644 index 00000000..35b6d854 --- /dev/null +++ b/public/components/filter_bar/filter_editor.tsx @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCodeEditor, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPopoverTitle, + EuiSpacer, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { i18n } from '@osd/i18n'; + +interface Props { + content: string; + label: string; + onSubmit: (content: string, label: string) => void; + onCancel: () => void; +} + +const isFilterValid = (content: string) => { + try { + return Boolean(JSON.parse(content)); + } catch (e) { + return false; + } +}; + +export const FilterEditor = ({ content, label, onSubmit, onCancel }: Props) => { + const [filterLabel, setFilterLabel] = useState(label); + const [filterContent, setFilterContent] = useState(content); + + const renderEditor = () => { + return ( + + + + ); + }; + return ( +
+ + + {'Edit Filter'} + + + +
+ + {renderEditor()} +
+ + + setFilterLabel(event.target.value)} + /> + +
+ + + + onSubmit(filterContent, filterLabel)} + isDisabled={!isFilterValid(filterContent)} + data-test-subj="saveFilter" + > + {'Save'} + + + + + {'Cancel'} + + + + +
+
+
+ ); +}; diff --git a/public/components/filter_bar/filter_item.tsx b/public/components/filter_bar/filter_item.tsx new file mode 100644 index 00000000..caf0863c --- /dev/null +++ b/public/components/filter_bar/filter_item.tsx @@ -0,0 +1,179 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiContextMenu, EuiPopover } from '@elastic/eui'; +import classNames from 'classnames'; +import React, { MouseEvent, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { GeoShapeFilterMeta } from '../../../../../src/plugins/data/common'; +import { FilterView } from './filter_view'; +import { + toggleGeoShapeFilterMetaDisabled, + toggleGeoShapeFilterMetaNegated, +} from './filter_actions'; +import { FilterEditor } from './filter_editor'; + +interface Props { + id: string; + filterMeta: GeoShapeFilterMeta; + className?: string; + onUpdate: (filter: GeoShapeFilterMeta) => void; + onRemove: () => void; +} + +export function FilterItem({ filterMeta, onUpdate, onRemove, id, className }: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + function onSubmit(content: string, label: string) { + setIsPopoverOpen(false); + const updatedFilter: GeoShapeFilterMeta = { + ...filterMeta, + params: JSON.parse(content), + alias: label, + }; + onUpdate(updatedFilter); + } + + function handleClick(e: MouseEvent) { + if (e.shiftKey) { + onToggleDisabled(); + } else { + setIsPopoverOpen(!isPopoverOpen); + } + } + + function onToggleNegated() { + const f = toggleGeoShapeFilterMetaNegated(filterMeta); + onUpdate(f); + } + + function onToggleDisabled() { + const f = toggleGeoShapeFilterMetaDisabled(filterMeta); + onUpdate(f); + } + + function getClasses(negate: boolean, disabled: boolean) { + return classNames( + 'globalFilterItem', + { + 'globalFilterItem-isDisabled': disabled, + 'globalFilterItem-isExcluded': negate, + }, + className + ); + } + + function getDataTestSubj() { + const dataTestSubjKey = filterMeta.key ? `filter-key-${filterMeta.key}` : ''; + const dataTestSubjValue = filterMeta.value; + const dataTestSubjNegated = filterMeta.negate ? 'filter-negated' : ''; + const dataTestSubjDisabled = `filter-${filterMeta.disabled ? 'disabled' : 'enabled'}`; + return `filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjNegated}`; + } + + function getPanels() { + const { negate, disabled } = filterMeta; + return [ + { + id: 0, + items: [ + { + name: i18n.translate('maps.filter.filterBar.editFilterButtonLabel', { + defaultMessage: 'Edit filter', + }), + icon: 'pencil', + panel: 1, + 'data-test-subj': 'editFilter', + }, + { + name: negate + ? i18n.translate('maps.filter.filterBar.includeFilterButtonLabel', { + defaultMessage: 'Include results', + }) + : i18n.translate('data.filter.filterBar.excludeFilterButtonLabel', { + defaultMessage: 'Exclude results', + }), + icon: negate ? 'plusInCircle' : 'minusInCircle', + onClick: () => { + setIsPopoverOpen(false); + onToggleNegated(); + }, + 'data-test-subj': 'negateFilter', + }, + { + name: disabled + ? i18n.translate('data.filter.filterBar.enableFilterButtonLabel', { + defaultMessage: 'Re-enable', + }) + : i18n.translate('data.filter.filterBar.disableFilterButtonLabel', { + defaultMessage: 'Temporarily disable', + }), + icon: `${disabled ? 'eye' : 'eyeClosed'}`, + onClick: () => { + setIsPopoverOpen(false); + onToggleDisabled(); + }, + 'data-test-subj': 'disableFilter', + }, + { + name: i18n.translate('data.filter.filterBar.deleteFilterButtonLabel', { + defaultMessage: 'Delete', + }), + icon: 'trash', + onClick: () => { + setIsPopoverOpen(false); + onRemove(); + }, + 'data-test-subj': 'deleteFilter', + }, + ], + }, + { + id: 1, + width: 600, + content: ( +
+ { + setIsPopoverOpen(false); + }} + /> +
+ ), + }, + ]; + } + + const badge = ( + + ); + + return ( + { + setIsPopoverOpen(false); + }} + button={badge} + anchorPosition="downLeft" + panelPaddingSize="none" + > + + + ); +} diff --git a/public/components/filter_bar/filter_options.tsx b/public/components/filter_bar/filter_options.tsx new file mode 100644 index 00000000..ce6239f3 --- /dev/null +++ b/public/components/filter_bar/filter_options.tsx @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import React, { useState } from 'react'; +import { i18n } from '@osd/i18n'; + +interface Props { + onEnableAll: () => void; + onDisableAll: () => void; + onToggleAllNegated: () => void; + onToggleAllDisabled: () => void; + onRemoveAll: () => void; +} + +export const FilterOptions = ({ + onEnableAll, + onDisableAll, + onToggleAllNegated, + onToggleAllDisabled, + onRemoveAll, +}: Props) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const panelTree = { + id: 0, + items: [ + { + name: i18n.translate('maps.filter.options.enableAllFiltersButtonLabel', { + defaultMessage: 'Enable all', + }), + icon: 'eye', + onClick: () => { + closePopover(); + onEnableAll(); + }, + 'data-test-subj': 'enableAllFilters', + }, + { + name: i18n.translate('maps.filter.options.disableAllFiltersButtonLabel', { + defaultMessage: 'Disable all', + }), + icon: 'eyeClosed', + onClick: () => { + closePopover(); + onDisableAll(); + }, + 'data-test-subj': 'disableAllFilters', + }, + { + name: i18n.translate('maps.filter.options.invertNegatedFiltersButtonLabel', { + defaultMessage: 'Invert inclusion', + }), + icon: 'invert', + onClick: () => { + closePopover(); + onToggleAllNegated(); + }, + 'data-test-subj': 'invertInclusionAllFilters', + }, + { + name: i18n.translate('maps.filter.options.invertDisabledFiltersButtonLabel', { + defaultMessage: 'Invert enabled/disabled', + }), + icon: 'eye', + onClick: () => { + closePopover(); + onToggleAllDisabled(); + }, + 'data-test-subj': 'invertEnableDisableAllFilters', + }, + { + name: i18n.translate('maps.filter.options.deleteAllFiltersButtonLabel', { + defaultMessage: 'Remove all', + }), + icon: 'trash', + onClick: () => { + closePopover(); + onRemoveAll(); + }, + 'data-test-subj': 'removeAllFilters', + }, + ], + }; + + return ( + + } + anchorPosition="rightUp" + panelPaddingSize="none" + repositionOnScroll + > + + {i18n.translate('maps.filter.changeAllFiltersTitle', { + defaultMessage: 'Change all filters', + })} + + + + ); +}; diff --git a/public/components/filter_bar/filter_view.test.tsx b/public/components/filter_bar/filter_view.test.tsx new file mode 100644 index 00000000..fb5bb462 --- /dev/null +++ b/public/components/filter_bar/filter_view.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { FilterView } from './filter_view'; + +describe('test filter display', function () { + it('renders filter display on disabled', () => { + const mockOnClick = jest.fn(); + const mockOnRemove = jest.fn(); + const filterBar = shallow( + + ); + expect(filterBar).toMatchSnapshot(); + }); + it('renders filter display on enabled', () => { + const mockOnClick = jest.fn(); + const mockOnRemove = jest.fn(); + const filterView = shallow( + + ); + expect(filterView).toMatchSnapshot(); + }); +}); diff --git a/public/components/filter_bar/filter_view.tsx b/public/components/filter_bar/filter_view.tsx new file mode 100644 index 00000000..800717bb --- /dev/null +++ b/public/components/filter_bar/filter_view.tsx @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiBadge } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React, { FC } from 'react'; + +interface Props { + isDisabled: boolean; + valueLabel: string; + onRemove: () => void; + [propName: string]: any; +} + +export const FilterView: FC = ({ + isDisabled, + onRemove, + onClick, + valueLabel, + ...rest +}: Props) => { + let title = i18n.translate('maps.filter.filterBar.moreFilterActionsMessage', { + defaultMessage: 'Filter: {valueLabel}. Select for more filter actions.', + values: { valueLabel }, + }); + + if (isDisabled) { + title = `${i18n.translate('maps.filter.filterBar.disabledFilterPrefix', { + defaultMessage: 'Disabled', + })} ${title}`; + } + + return ( + + {valueLabel} + + ); +}; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 820b848c..0d3c397d 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -6,6 +6,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Map as Maplibre, NavigationControl } from 'maplibre-gl'; import { debounce, throttle } from 'lodash'; +import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; @@ -36,6 +37,7 @@ import { DisplayFeatures } from '../tooltip/display_features'; import { TOOLTIP_STATE } from '../../../common'; import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; import { DrawFilterShapeHelper } from '../toolbar/spatial_filter/display_draw_helper'; +import { ShapeFilter } from '../../../../../src/plugins/data/common'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -52,6 +54,7 @@ interface MapContainerProps { query?: Query; isUpdatingLayerRender: boolean; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; + addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; } export const MapContainer = ({ @@ -69,6 +72,7 @@ export const MapContainer = ({ query, isUpdatingLayerRender, setIsUpdatingLayerRender, + addSpatialFilter, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); const mapContainer = useRef(null); @@ -256,10 +260,11 @@ export const MapContainer = ({ map={maplibreRef.current} filterProperties={filterProperties} updateFilterProperties={setFilterProperties} + addSpatialFilter={addSpatialFilter} /> )}
- {mounted && ( + {!isReadOnlyMode && mounted && ( { + const filterMeta: GeoShapeFilterMeta = buildGeoShapeFilterMeta(label, shape, relation); + const geoShapeFilterMeta: GeoShapeFilterMeta[] = mapState.spatialMetaFilters || []; + setMapState({ + ...mapState, + spatialMetaFilters: [...geoShapeFilterMeta, filterMeta], + }); + }; + + const onFiltersUpdated = (newFilters: GeoShapeFilterMeta[]) => { + setMapState({ + ...mapState, + spatialMetaFilters: [...newFilters], + }); + }; + + const filterGroupClasses = classNames('globalFilterGroup__wrapper', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'globalFilterGroup__wrapper-isVisible': !!mapState.spatialMetaFilters?.length, + }); + return (
{isReadOnlyMode ? null : ( @@ -106,7 +136,17 @@ export const MapComponent = ({ setIsUpdatingLayerRender={setIsUpdatingLayerRender} /> )} - + {!isReadOnlyMode && !!mapState.spatialMetaFilters?.length && ( +
+
+ +
+
+ )}
); diff --git a/public/components/toolbar/spatial_filter/draw_filter_shape.tsx b/public/components/toolbar/spatial_filter/draw_filter_shape.tsx index 83a150d1..32df0a57 100644 --- a/public/components/toolbar/spatial_filter/draw_filter_shape.tsx +++ b/public/components/toolbar/spatial_filter/draw_filter_shape.tsx @@ -7,6 +7,7 @@ import React, { Fragment, useEffect, useRef } from 'react'; import { IControl, Map as Maplibre } from 'maplibre-gl'; import MapboxDraw from '@mapbox/mapbox-gl-draw'; import { Feature } from 'geojson'; +import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; import { DrawFilterProperties, FILTER_DRAW_MODE, @@ -15,11 +16,13 @@ import { } from '../../../../common'; import { DrawRectangle } from '../../draw/modes/rectangle'; import {DRAW_SHAPE_STYLE} from "./draw_style"; +import { GeoShapeFilter, ShapeFilter } from '../../../../../../src/plugins/data/common'; interface DrawFilterShapeProps { filterProperties: DrawFilterProperties; map: Maplibre; updateFilterProperties: (properties: DrawFilterProperties) => void; + addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; } function getMapboxDrawMode(mode: FILTER_DRAW_MODE): string { @@ -32,13 +35,41 @@ function getMapboxDrawMode(mode: FILTER_DRAW_MODE): string { return MAPBOX_GL_DRAW_MODES.SIMPLE_SELECT; } } +export const isGeoShapeFilter = (filter: any): filter is GeoShapeFilter => filter?.geo_shape; + +const isShapeFilter = (geometry: any): geometry is ShapeFilter => { + return geometry && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon'); +}; + +const toGeoShapeRelation = (relation?: string): GeoShapeRelation => { + switch (relation) { + case 'intersects': + return relation; + case 'within': + return relation; + case 'disjoint': + return relation; + default: + return 'intersects'; + } +}; export const DrawFilterShape = ({ filterProperties, map, updateFilterProperties, + addSpatialFilter, }: DrawFilterShapeProps) => { const onDraw = (event: { features: Feature[] }) => { + event.features.map((feature) => { + if (isShapeFilter(feature.geometry)) { + addSpatialFilter( + feature.geometry, + filterProperties.filterLabel || null, + toGeoShapeRelation(filterProperties.relation) + ); + } + }); updateFilterProperties({ mode: FILTER_DRAW_MODE.NONE, }); diff --git a/public/components/toolbar/spatial_filter/spatial_filter.scss b/public/components/toolbar/spatial_filter/spatial_filter.scss index 0025f923..fe0febae 100644 --- a/public/components/toolbar/spatial_filter/spatial_filter.scss +++ b/public/components/toolbar/spatial_filter/spatial_filter.scss @@ -7,10 +7,9 @@ position: relative; .euiButtonIcon { - border-color:rgba(0,0,0,0.9); - color:rgba(255,255,255,0.5); width:30px; height:30px; + border: transparent; } } .spatialFilterGroup__popoverPanel{ diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 3d4fdf7c..06753541 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -138,7 +138,7 @@ export const handleDataLayerRender = ( filters.push(geoBoundingBoxFilter); // build and add GeoShape filters from map state if applicable - if (mapLayer.source?.applyGlobalFilter ?? true) { + if (mapLayer.source?.applyGlobalFilters ?? true) { mapState?.spatialMetaFilters?.map((value) => { if (getSupportedOperations(geoFieldType).includes(value.params.relation)) { filters.push(buildGeoShapeFilter(geoField, value)); From 704bb13129ff5338eae2cb4b1898a6c3015c44cb Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 4 Apr 2023 13:10:05 -0500 Subject: [PATCH 58/77] Change font opacity along with OpenSearch base map layer (#375) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + public/model/OSMLayerFunctions.ts | 57 +++++-------------- public/model/map/layer_operations.test.ts | 69 ++++++++++++++++++++++- public/model/map/layer_operations.ts | 55 +++++++++++++++++- 4 files changed, 138 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2056cacf..ba9a944d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) * Update draw filter shape ui properties ([#372](https://github.com/opensearch-project/dashboards-maps/pull/372)) * Add filter bar to display global geospatial filters ([#371](https://github.com/opensearch-project/dashboards-maps/pull/371)) +* Change font opacity along with OpenSearch base map layer ([#373](https://github.com/opensearch-project/dashboards-maps/pull/373)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index 8f382144..057da37f 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -1,6 +1,13 @@ import { Map as Maplibre, LayerSpecification, SymbolLayerSpecification } from 'maplibre-gl'; import { OSMLayerSpecification } from './mapLayerType'; -import { getLayers, hasLayer } from './map/layer_operations'; +import { + addOSMLayerSource, + addOSMStyleLayer, + getLayers, + hasLayer, + getOSMStyleLayerWithMapLayerId, + updateOSMStyleLayer, +} from './map/layer_operations'; import { getMapLanguage } from '../../common/util'; interface MaplibreRef { @@ -20,20 +27,7 @@ const fetchStyleLayers = (url: string) => { const handleStyleLayers = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { getLayers(maplibreRef.current!, layerConfig.id).forEach((mbLayer) => { - maplibreRef.current?.setLayerZoomRange( - mbLayer.id, - layerConfig.zoomRange[0], - layerConfig.zoomRange[1] - ); - // TODO: figure out error reason - if (mbLayer.type === 'symbol') { - return; - } - maplibreRef.current?.setPaintProperty( - mbLayer.id, - `${mbLayer.type}-opacity`, - layerConfig.opacity / 100 - ); + updateOSMStyleLayer(maplibreRef.current!, layerConfig, mbLayer); }); }; @@ -54,35 +48,14 @@ const setLanguage = (maplibreRef: MaplibreRef, styleLayer: LayerSpecification) = const addNewLayer = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { if (maplibreRef.current) { - const { source, style } = layerConfig; - maplibreRef.current.addSource(layerConfig.id, { - type: 'vector', - url: source?.dataURL, - }); + const maplibre = maplibreRef.current; + const { id, source, style } = layerConfig; + addOSMLayerSource(maplibre, id, source.dataURL); fetchStyleLayers(style?.styleURL).then((styleLayers: LayerSpecification[]) => { - styleLayers.forEach((styleLayer) => { - styleLayer.id = styleLayer.id + '_' + layerConfig.id; - // TODO: Add comments on why we skip background type - if (styleLayer.type !== 'background') { - styleLayer.source = layerConfig.id; - } - maplibreRef.current?.addLayer(styleLayer); + styleLayers.forEach((layer) => { + const styleLayer = getOSMStyleLayerWithMapLayerId(id, layer); + addOSMStyleLayer(maplibre, layerConfig, styleLayer); setLanguage(maplibreRef, styleLayer); - maplibreRef.current?.setLayoutProperty(styleLayer.id, 'visibility', layerConfig.visibility); - maplibreRef.current?.setLayerZoomRange( - styleLayer.id, - layerConfig.zoomRange[0], - layerConfig.zoomRange[1] - ); - // TODO: figure out error reason - if (styleLayer.type === 'symbol') { - return; - } - maplibreRef.current?.setPaintProperty( - styleLayer.id, - `${styleLayer.type}-opacity`, - layerConfig.opacity / 100 - ); }); }); } diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 7569a3bf..056803f3 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -16,10 +16,14 @@ import { updateLayerVisibilityHandler, addSymbolLayer, updateSymbolLayer, + addOSMLayerSource, + addOSMStyleLayer, + getOSMStyleLayerWithMapLayerId, } from './layer_operations'; -import { Map as Maplibre } from 'maplibre-gl'; +import { LayerSpecification, Map as Maplibre } from 'maplibre-gl'; import { MockMaplibreMap } from './__mocks__/map'; import { MockLayer } from './__mocks__/layer'; +import { OSMLayerSpecification } from '../mapLayerType'; describe('Circle layer', () => { it('add new circle layer', () => { @@ -509,3 +513,66 @@ describe('update visibility', function () { ); }); }); + +describe('OpenSearch base map', function () { + it('should add OpenSearch base map source', function () { + const mockMap: MockMaplibreMap = new MockMaplibreMap([]); + addOSMLayerSource(mockMap as unknown as Maplibre, 'source-1', 'foo.com'); + expect(mockMap.getSource('source-1')).toBeDefined(); + }); + + it('should add OpenSearch base map style layer', function () { + const mockMap: MockMaplibreMap = new MockMaplibreMap([]); + const mockMapLayer: OSMLayerSpecification = { + name: 'mock-layer-1', + type: 'opensearch_vector_tile_map', + id: 'layer-1-id', + description: 'layer-1-description', + zoomRange: [0, 10], + opacity: 80, + visibility: 'visible', + source: { + dataURL: 'foo.data.com', + }, + style: { + styleURL: 'foo.style.com', + }, + }; + const mockStyleLayer = { + id: 'style-layer-1', + type: 'fill', + source: 'source-1', + } as unknown as LayerSpecification; + + const mockSymbolStyleLayer = { + id: 'style-layer-2', + type: 'symbol', + source: 'source-1', + } as unknown as LayerSpecification; + + addOSMStyleLayer(mockMap as unknown as Maplibre, mockMapLayer, mockStyleLayer); + expect(mockMap.getLayers().length).toBe(1); + expect(mockMap.getLayers()[0].getProperty('id')).toBe('style-layer-1'); + expect(mockMap.getLayers()[0].getProperty('type')).toBe('fill'); + expect(mockMap.getLayers()[0].getProperty('source')).toBe('source-1'); + expect(mockMap.getLayers()[0].getProperty('visibility')).toBe('visible'); + expect(mockMap.getLayers()[0].getProperty('minZoom')).toBe(0); + expect(mockMap.getLayers()[0].getProperty('maxZoom')).toBe(10); + expect(mockMap.getLayers()[0].getProperty('fill-opacity')).toBe(0.8); + + addOSMStyleLayer(mockMap as unknown as Maplibre, mockMapLayer, mockSymbolStyleLayer); + expect(mockMap.getLayers().length).toBe(2); + expect(mockMap.getLayers()[1].getProperty('id')).toBe('style-layer-2'); + expect(mockMap.getLayers()[1].getProperty('type')).toBe('symbol'); + expect(mockMap.getLayers()[1].getProperty('text-opacity')).toBe(0.8); + }); + + it('should set OSM style layer source ID', function () { + const mockMapLayerId = 'layer-1-id'; + const mockStyleLayer = { + id: 'style-layer-1', + type: 'fill', + } as LayerSpecification; + getOSMStyleLayerWithMapLayerId(mockMapLayerId, mockStyleLayer); + }); +}); diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index ba85bbd7..a9f4e1ad 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { LayerSpecification, Map as Maplibre } from 'maplibre-gl'; -import { DocumentLayerSpecification } from '../mapLayerType'; +import { DocumentLayerSpecification, OSMLayerSpecification } from '../mapLayerType'; export const getLayers = (map: Maplibre, dashboardMapsLayerId?: string): LayerSpecification[] => { const layers: LayerSpecification[] = map.getStyle().layers; @@ -277,3 +277,56 @@ export const updateSymbolLayer = ( map.setPaintProperty(symbolLayerId, 'text-halo-color', specification.symbolBorderColor); return symbolLayerId; }; + +// The function to add a new OSM layer to the map +export const addOSMLayerSource = (map: Maplibre, sourceId: string, dataURL: string): void => { + map.addSource(sourceId, { + type: 'vector', + url: dataURL, + }); +}; + +export const addOSMStyleLayer = ( + map: Maplibre, + mapLayer: OSMLayerSpecification, + styleLayer: LayerSpecification +) => { + map.addLayer(styleLayer); + return updateOSMStyleLayer(map, mapLayer, styleLayer); +}; + +export const updateOSMStyleLayer = ( + map: Maplibre, + mapLayer: OSMLayerSpecification, + styleLayer: LayerSpecification +) => { + const { zoomRange, visibility, opacity } = mapLayer; + const { id: styleLayerId, type: styleLayerType } = styleLayer; + map.setLayoutProperty(styleLayerId, 'visibility', visibility); + map.setLayerZoomRange(styleLayerId, zoomRange[0], zoomRange[1]); + if (styleLayerType === 'symbol') { + map.setPaintProperty(styleLayerId, 'text-opacity', opacity / 100); + } else { + map.setPaintProperty(styleLayerId, `${styleLayerType}-opacity`, opacity / 100); + } +}; + +export const getOSMStyleLayerWithMapLayerId = ( + mapLayerId: string, + styleLayer: LayerSpecification +): LayerSpecification => { + let updatedStyleLayerId = mapLayerId; + // non-background layer requires source + if (styleLayer.type !== 'background') { + updatedStyleLayerId = `${styleLayer.id}_${mapLayerId}`; + return { + ...styleLayer, + id: updatedStyleLayerId, + source: mapLayerId, + }; + } + return { + ...styleLayer, + id: updatedStyleLayerId, + }; +}; From ef5be8ed7a3195409caf979da928e9d9dcceca23 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 5 Apr 2023 12:07:53 -0500 Subject: [PATCH 59/77] Add stats API for maps and layers in maps plugin (#362) * Add stats API for maps and layers in maps plugin Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + common/index.ts | 12 +- public/model/mapLayerType.ts | 13 +- server/common/stats/stats_helper.test.ts | 164 +++++++++++++++++++++++ server/common/stats/stats_helper.ts | 116 ++++++++++++++++ server/plugin.ts | 6 +- server/routes/index.ts | 3 +- server/routes/stats_router.ts | 45 +++++++ 8 files changed, 350 insertions(+), 10 deletions(-) create mode 100644 server/common/stats/stats_helper.test.ts create mode 100644 server/common/stats/stats_helper.ts create mode 100644 server/routes/stats_router.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9a944d..f37470cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Infrastructure * Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) +* Add stats API for maps and layers in maps plugin ([#362](https://github.com/opensearch-project/dashboards-maps/pull/362)) ### Documentation diff --git a/common/index.ts b/common/index.ts index 6e750399..9f7b5cc6 100644 --- a/common/index.ts +++ b/common/index.ts @@ -82,8 +82,11 @@ export const APP_PATH = { LANDING_PAGE_PATH: '/', CREATE_MAP: '/create', EDIT_MAP: '/:id', + STATS: '/stats', }; +export const APP_API = '/api/maps-dashboards'; + export enum DASHBOARDS_MAPS_LAYER_NAME { OPENSEARCH_MAP = 'OpenSearch map', DOCUMENTS = 'Documents', @@ -96,6 +99,11 @@ export enum DASHBOARDS_MAPS_LAYER_TYPE { CUSTOM_MAP = 'custom_map', } +export enum DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE { + WMS = 'wms', + TMS = 'tms', +} + export enum DASHBOARDS_MAPS_LAYER_ICON { OPENSEARCH_MAP = 'globe', DOCUMENTS = 'document', @@ -147,7 +155,7 @@ export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe', }; -//refer https://github.com/opensearch-project/i18n-plugin/blob/main/DEVELOPER_GUIDE.md#new-locale for OSD supported languages +// refer https://github.com/opensearch-project/i18n-plugin/blob/main/DEVELOPER_GUIDE.md#new-locale for OSD supported languages export const OSD_LANGUAGES = ['en', 'es', 'fr', 'de', 'ja', 'ko', 'zh']; // all these codes are also supported in vector tiles map export const FALLBACK_LANGUAGE = 'en'; @@ -183,3 +191,5 @@ export const DRAW_FILTER_POLYGON = 'Draw Polygon'; export const DRAW_FILTER_CANCEL = 'Cancel'; export const DRAW_FILTER_RECTANGLE = 'Draw Rectangle'; export const DRAW_FILTER_SPATIAL_RELATIONS = ['intersects', 'disjoint', 'within']; + +export const PER_PAGE_REQUEST_NUMBER = 50; diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts index e73b2564..8a76a84f 100644 --- a/public/model/mapLayerType.ts +++ b/public/model/mapLayerType.ts @@ -4,6 +4,7 @@ */ import { Filter } from '../../../../src/plugins/data/public'; +import { DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE, DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; /* eslint @typescript-eslint/consistent-type-definitions: ["error", "type"] */ export type MapLayerSpecification = @@ -25,7 +26,7 @@ export type AbstractLayerSpecification = { }; export type OSMLayerSpecification = AbstractLayerSpecification & { - type: 'opensearch_vector_tile_map'; + type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP; source: { dataURL: string; }; @@ -35,7 +36,7 @@ export type OSMLayerSpecification = AbstractLayerSpecification & { }; export type DocumentLayerSpecification = AbstractLayerSpecification & { - type: 'documents'; + type: DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS; source: { indexPatternRefName: string; indexPatternId: string; @@ -70,19 +71,19 @@ export type DocumentLayerSpecification = AbstractLayerSpecification & { export type CustomLayerSpecification = CustomTMSLayerSpecification | CustomWMSLayerSpecification; export type CustomTMSLayerSpecification = AbstractLayerSpecification & { - type: 'custom_map'; + type: DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP; source: { url: string; - customType: 'tms'; + customType: DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE.TMS; attribution: string; }; }; export type CustomWMSLayerSpecification = AbstractLayerSpecification & { - type: 'custom_map'; + type: DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP; source: { url: string; - customType: 'wms'; + customType: DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE.WMS; attribution: string; layers: string; styles: string; diff --git a/server/common/stats/stats_helper.test.ts b/server/common/stats/stats_helper.test.ts new file mode 100644 index 00000000..1a09a1da --- /dev/null +++ b/server/common/stats/stats_helper.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsFindResponse } from '../../../../../src/core/server'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { getMapSavedObjects, getStats } from './stats_helper'; + +describe('getStats', () => { + const mockSavedObjects: SavedObjectsFindResponse = { + page: 1, + per_page: 1000, + total: 3, + saved_objects: [ + { + type: 'map', + id: 'cfa702d0-cf47-11ed-9728-3b2a82d0d675', + attributes: { + title: 'test1', + description: '', + layerList: + '[{"name":"Default map","description":"","type":"opensearch_vector_tile_map","id":"2b7a9a72-e29e-45f4-9e47-93c12c6e07cb","zoomRange":[0,22],"opacity":100,"visibility":"visible","source":{"dataURL":"https://tiles.maps.opensearch.org/data/v1.json"},"style":{"styleURL":"https://tiles.maps.opensearch.org/styles/basic.json"}},{"name":"New layer 2","description":"","type":"documents","id":"6ed74651-533c-4a4b-b453-c70ed63bbc8a","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_logs","geoFieldType":"geo_point","geoFieldName":"geo.coordinates","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","filters":[{"meta":{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","params":{},"alias":null,"negate":false,"disabled":false},"range":{"bytes":{"gte":11,"lt":233}},"$state":{"store":"appState"}}]},"style":{"fillColor":"#f32a8a","borderColor":"#f32a8a","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}}]', + mapState: + '{"timeRange":{"from":"now-15m","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}', + version: 1, + }, + references: [], + updated_at: '2023-03-30T22:12:55.966Z', + version: 'WzIxNSwxXQ==', + score: 0, + }, + { + type: 'map', + id: 'b1483670-cf4b-11ed-9fa4-bbade202e9e0', + attributes: { + title: 'test2', + description: '', + layerList: + '[{"name":"Default map","description":"","type":"opensearch_vector_tile_map","id":"ea10f34e-f927-420b-8467-ee7950143dd8","zoomRange":[0,22],"opacity":100,"visibility":"visible","source":{"dataURL":"https://tiles.maps.opensearch.org/data/v1.json"},"style":{"styleURL":"https://tiles.maps.opensearch.org/styles/basic.json"}},{"name":"New layer 2","description":"","type":"documents","id":"0b231a61-e44a-4c2c-b821-82be5441925f","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_logs","geoFieldType":"geo_point","geoFieldName":"geo.coordinates","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","filters":[]},"style":{"fillColor":"#622d7f","borderColor":"#622d7f","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}},{"name":"New layer 3","description":"","type":"documents","id":"ab1a5116-ad57-40a4-832d-be10edca4976","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_flights","geoFieldType":"geo_point","geoFieldName":"DestLocation","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filters":[]},"style":{"fillColor":"#6d11fa","borderColor":"#6d11fa","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}}]', + mapState: + '{"timeRange":{"from":"now-7d","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000},"spatialMetaFilters":[{"type":"geo_shape","alias":"rectangle","disabled":false,"params":{"relation":"intersects","shape":{"coordinates":[[[-106.03593830559568,46.582217440485095],[-80.61568219066639,46.582217440485095],[-80.61568219066639,28.78448193045257],[-106.03593830559568,28.78448193045257],[-106.03593830559568,46.582217440485095]]],"type":"Polygon"}},"negate":false}]}', + version: 1, + }, + references: [], + updated_at: '2023-03-30T22:56:11.273Z', + version: 'WzIxOSwxXQ==', + score: 0, + }, + { + type: 'map', + id: '48b5ddb0-cfd7-11ed-96c2-f323ef4d8d0b', + attributes: { + title: 'test3', + description: '', + layerList: + '[{"name":"Default map","description":"","type":"opensearch_vector_tile_map","id":"bce4c650-434c-4785-8429-b9f9f2042054","zoomRange":[0,22],"opacity":100,"visibility":"visible","source":{"dataURL":"https://tiles.maps.opensearch.org/data/v1.json"},"style":{"styleURL":"https://tiles.maps.opensearch.org/styles/basic.json"}},{"name":"New layer 2","description":"","type":"documents","id":"48104a9b-72aa-4aa0-8dbf-e19ad4462dd0","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_logs","geoFieldType":"geo_point","geoFieldName":"geo.coordinates","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","filters":[]},"style":{"fillColor":"#da23f2","borderColor":"#da23f2","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}}]', + mapState: + '{"timeRange":{"from":"now-24h","to":"now"},"query":{"query":"response:404","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}', + version: 1, + }, + references: [], + updated_at: '2023-03-31T15:18:26.313Z', + version: 'WzIyMCwxXQ==', + score: 0, + }, + ], + }; + + const mockEmptySavedObjects: SavedObjectsFindResponse = { + page: 1, + per_page: 1000, + total: 0, + saved_objects: [], + }; + + it('returns expected stats', () => { + const stats = getStats(mockSavedObjects); + expect(stats.maps_total).toEqual(3); + expect(stats.layers_filters_total).toEqual(1); + expect(stats.layers_total.opensearch_vector_tile_map).toEqual(3); + expect(stats.layers_total.documents).toEqual(4); + expect(stats.layers_total.wms).toEqual(0); + expect(stats.layers_total.tms).toEqual(0); + expect(stats.maps_list.length).toEqual(3); + expect(stats.maps_list[0].id).toEqual('cfa702d0-cf47-11ed-9728-3b2a82d0d675'); + expect(stats.maps_list[0].layers_filters_total).toEqual(1); + expect(stats.maps_list[0].layers_total.documents).toEqual(1); + expect(stats.maps_list[0].layers_total.opensearch_vector_tile_map).toEqual(1); + }); + + it('returns expected stats with empty saved objects', () => { + const stats = getStats(mockEmptySavedObjects); + expect(stats.maps_total).toEqual(0); + expect(stats.layers_filters_total).toEqual(0); + expect(stats.layers_total.opensearch_vector_tile_map).toEqual(0); + expect(stats.layers_total.documents).toEqual(0); + expect(stats.layers_total.wms).toEqual(0); + expect(stats.layers_total.tms).toEqual(0); + expect(stats.maps_list.length).toEqual(0); + }); +}); + +describe('getMapSavedObjects', () => { + const mockSavedObjectsClient = { + find: jest.fn(), + }; + const perPage = 2; + + it('should fetch and return all saved objects of type MAP_SAVED_OBJECT_TYPE', async () => { + // create mock SavedObjectsClientContract + mockSavedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { id: '1', attributes: { title: 'Map 1' } }, + { id: '2', attributes: { title: 'Map 2' } }, + ], + }); + + // @ts-ignore + const result = await getMapSavedObjects(mockSavedObjectsClient, perPage); + + expect(result.total).toBe(2); // total should match what was returned by mockSavedObjectsClient.find() + expect(result.saved_objects).toHaveLength(2); // all saved objects should be returned + expect(result.saved_objects[0].attributes.title).toBe('Map 1'); // first saved object should have correct title + expect(result.saved_objects[1].attributes.title).toBe('Map 2'); // second saved object should have correct title + }); + + it('should make additional requests if there are more than perPage maps', async () => { + mockSavedObjectsClient.find + .mockResolvedValueOnce({ + total: 3, + saved_objects: [ + { id: '1', attributes: { title: 'Map 1' } }, + { id: '2', attributes: { title: 'Map 2' } }, + ], + }) + .mockResolvedValueOnce({ + total: 3, + saved_objects: [{ id: '3', attributes: { title: 'Map 3' } }], + }); + + // @ts-ignore + const result = await getMapSavedObjects(mockSavedObjectsClient, perPage); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledTimes(2); // should have made two calls to find() + expect(result.saved_objects).toHaveLength(3); // all saved objects should be returned + expect(result.saved_objects[0].attributes.title).toBe('Map 1'); // first saved object should have correct title + expect(result.saved_objects[1].attributes.title).toBe('Map 2'); // second saved object should have correct title + expect(result.saved_objects[2].attributes.title).toBe('Map 3'); // second saved object should have correct title + }); + + it('should return empty array if no maps are found', async () => { + mockSavedObjectsClient.find.mockResolvedValueOnce({ + total: 0, + saved_objects: [], + }); + + // @ts-ignore + const result = await getMapSavedObjects(mockSavedObjectsClient, perPage); + + expect(result.saved_objects).toHaveLength(0); // should return empty array + }); +}); diff --git a/server/common/stats/stats_helper.ts b/server/common/stats/stats_helper.ts new file mode 100644 index 00000000..a38a2a79 --- /dev/null +++ b/server/common/stats/stats_helper.ts @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsFindResponse } from '../../../../../src/core/server'; +import { MapLayerSpecification } from '../../../public/model/mapLayerType'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { + DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE, + DASHBOARDS_MAPS_LAYER_TYPE, + MAP_SAVED_OBJECT_TYPE, +} from '../../../common'; +import { SavedObjectsClientContract } from '../../../../../src/core/server'; + +interface MapAppStats { + maps_total: number; + layers_filters_total: number; + layers_total: { [key: string]: number }; + maps_list: Array<{ + id: string; + layers_filters_total: number; + layers_total: { [key: string]: number }; + }>; +} + +export const getStats = ( + mapsSavedObjects: SavedObjectsFindResponse +): MapAppStats => { + const totalLayersCountByType = buildLayerTypesCountObject(); + let totalLayersFiltersCount = 0; + const mapsList: Array<{ + id: string; + layers_filters_total: number; + layers_total: { [key: string]: number }; + }> = []; + mapsSavedObjects.saved_objects.forEach((mapRes) => { + const layersCountByType = buildLayerTypesCountObject(); + let layersFiltersCount = 0; + const layerList: MapLayerSpecification[] = mapRes?.attributes?.layerList + ? JSON.parse(mapRes?.attributes?.layerList) + : []; + layerList.forEach((layer) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP) { + layersCountByType[layer.source.customType]++; + totalLayersCountByType[layer.source.customType]++; + } else { + layersCountByType[layer.type]++; + totalLayersCountByType[layer.type]++; + } + // @ts-ignore + const layerFiltersCount = layer.source?.filters?.length ?? 0; + layersFiltersCount += layerFiltersCount; + totalLayersFiltersCount += layerFiltersCount; + }); + + mapsList.push({ + id: mapRes?.id, + layers_filters_total: layersFiltersCount, + layers_total: { + ...layersCountByType, + }, + }); + }); + + return { + maps_total: mapsSavedObjects.total, + layers_filters_total: totalLayersFiltersCount, + layers_total: { + ...totalLayersCountByType, + }, + maps_list: mapsList, + }; +}; + +const buildLayerTypesCountObject = (): { [key: string]: number } => { + const layersCountByType: { [key: string]: number } = {}; + Object.values(DASHBOARDS_MAPS_LAYER_TYPE).forEach((layerType) => { + if (layerType === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP) { + Object.values(DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE).forEach((customLayerType) => { + layersCountByType[customLayerType] = 0; + }); + } else { + layersCountByType[layerType] = 0; + } + }); + return layersCountByType; +}; + +export const getMapSavedObjects = async ( + savedObjectsClient: SavedObjectsClientContract, + perPage: number +): Promise> => { + const mapsSavedObjects: SavedObjectsFindResponse = + await savedObjectsClient?.find({ + type: MAP_SAVED_OBJECT_TYPE, + perPage, + }); + // If there are more than perPage of maps, we need to make additional requests to get all maps. + if (mapsSavedObjects.total > perPage) { + const iterations = Math.ceil(mapsSavedObjects.total / perPage); + for (let i = 1; i < iterations; i++) { + const mapsSavedObjectsPage: SavedObjectsFindResponse = + await savedObjectsClient?.find({ + type: MAP_SAVED_OBJECT_TYPE, + perPage, + page: i + 1, + }); + mapsSavedObjects.saved_objects = [ + ...mapsSavedObjects.saved_objects, + ...mapsSavedObjectsPage.saved_objects, + ]; + } + } + return mapsSavedObjects; +}; diff --git a/server/plugin.ts b/server/plugin.ts index cae4e8ce..67243500 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -22,13 +22,14 @@ import { } from './types'; import { createGeospatialCluster } from './clusters'; import { GeospatialService, OpensearchService } from './services'; -import { geospatial, opensearch } from '../server/routes'; +import { geospatial, opensearch, statsRoute } from '../server/routes'; import { mapSavedObjectsType } from './saved_objects'; import { capabilitiesProvider } from './saved_objects/capabilities_provider'; import { ConfigSchema } from '../common/config'; export class CustomImportMapPlugin - implements Plugin { + implements Plugin +{ private readonly logger: Logger; private readonly globalConfig$; private readonly config$; @@ -62,6 +63,7 @@ export class CustomImportMapPlugin // Register server side APIs geospatial(geospatialService, router); opensearch(opensearchService, router); + statsRoute(router, this.logger); // Register saved object types core.savedObjects.registerType(mapSavedObjectsType); diff --git a/server/routes/index.ts b/server/routes/index.ts index f3ed5073..9a8032ba 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -5,5 +5,6 @@ import geospatial from './geospatial'; import opensearch from './opensearch'; +import { statsRoute } from './stats_router'; -export { geospatial, opensearch }; +export { geospatial, opensearch, statsRoute }; diff --git a/server/routes/stats_router.ts b/server/routes/stats_router.ts new file mode 100644 index 00000000..9c52fe85 --- /dev/null +++ b/server/routes/stats_router.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; +import { Logger } from '@osd/logging'; +import { + IOpenSearchDashboardsResponse, + IRouter, + SavedObjectsFindResponse, +} from '../../../../src/core/server'; +import { APP_API, APP_PATH, PER_PAGE_REQUEST_NUMBER } from '../../common'; +import { getMapSavedObjects, getStats } from '../common/stats/stats_helper'; +import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; + +export function statsRoute(router: IRouter, logger: Logger) { + router.get( + { + path: `${APP_API}${APP_PATH.STATS}`, + validate: {}, + }, + async ( + context, + request, + response + ): Promise> => { + try { + const savedObjectsClient = context.core.savedObjects.client; + const mapsSavedObjects: SavedObjectsFindResponse = + await getMapSavedObjects(savedObjectsClient, PER_PAGE_REQUEST_NUMBER); + const stats = getStats(mapsSavedObjects); + return response.ok({ + body: stats, + }); + } catch (error) { + logger.error(error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); +} From e6c00048d631863aa8e0b31dce90dbc1338469da Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 5 Apr 2023 12:21:54 -0500 Subject: [PATCH 60/77] Add search query ability on map (#370) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + .../map_container/map_container.tsx | 54 ++++--- public/components/map_page/map_page.tsx | 43 +++-- .../components/map_top_nav/top_nav_menu.tsx | 32 ++-- public/embeddable/map_component.tsx | 14 +- public/model/layerRenderController.ts | 152 +++++++++++------- 6 files changed, 167 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f37470cc..79ef7893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Add geo shape query filter ([#319](https://github.com/opensearch-project/dashboards-maps/pull/319)) * Add shape filter UI button ([#329](https://github.com/opensearch-project/dashboards-maps/pull/329)) * Add tooltip to draw shape filter ([#330](https://github.com/opensearch-project/dashboards-maps/pull/330)) +* Add search query ability on map([#370](https://github.com/opensearch-project/dashboards-maps/pull/370)) ### Enhancements * Enhance layer visibility status display ([#299](https://github.com/opensearch-project/dashboards-maps/pull/299)) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 0d3c397d..6f3fcca3 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -10,15 +10,9 @@ import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { DrawFilterProperties, FILTER_DRAW_MODE, MAP_INITIAL_STATE } from '../../../common'; -import { MapLayerSpecification } from '../../model/mapLayerType'; +import { DataLayerSpecification, MapLayerSpecification } from '../../model/mapLayerType'; import { DrawFilterShape } from '../toolbar/spatial_filter/draw_filter_shape'; -import { - Filter, - IndexPattern, - Query, - RefreshInterval, - TimeRange, -} from '../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { renderDataLayers, @@ -38,6 +32,7 @@ import { TOOLTIP_STATE } from '../../../common'; import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; import { DrawFilterShapeHelper } from '../toolbar/spatial_filter/display_draw_helper'; import { ShapeFilter } from '../../../../../src/plugins/data/common'; +import { DashboardProps } from '../map_page/map_page'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -48,10 +43,7 @@ interface MapContainerProps { mapState: MapState; mapConfig: ConfigSchema; isReadOnlyMode: boolean; - timeRange?: TimeRange; - refreshConfig?: RefreshInterval; - filters?: Filter[]; - query?: Query; + dashboardProps?: DashboardProps; isUpdatingLayerRender: boolean; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; @@ -66,10 +58,7 @@ export const MapContainer = ({ mapState, mapConfig, isReadOnlyMode, - timeRange, - refreshConfig, - filters, - query, + dashboardProps, isUpdatingLayerRender, setIsUpdatingLayerRender, addSpatialFilter, @@ -140,7 +129,7 @@ export const MapContainer = ({ // Rerender layers with 200ms debounce to avoid calling the search API too frequently, especially when // resizing the window, the "moveend" event could be fired constantly const debouncedRenderLayers = debounce(() => { - renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); }, 200); if (maplibreRef.current) { @@ -157,17 +146,21 @@ export const MapContainer = ({ // Update data layers when state bar enable auto refresh useEffect(() => { let intervalId: NodeJS.Timeout | undefined; - if (refreshConfig && !refreshConfig.pause) { + if (dashboardProps && dashboardProps.refreshConfig && !dashboardProps.refreshConfig.pause) { + const { refreshConfig } = dashboardProps; intervalId = setInterval(() => { - renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); }, refreshConfig.value); } return () => clearInterval(intervalId); - }, [refreshConfig]); + }, [dashboardProps?.refreshConfig]); // Update data layers when global filter is updated useEffect(() => { - renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + if (!mapState?.spatialMetaFilters) { + return; + } + renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); }, [mapState.spatialMetaFilters]); useEffect(() => { @@ -183,10 +176,15 @@ export const MapContainer = ({ handleBaseLayerRender(selectedLayerConfig, maplibreRef); } else { updateIndexPatterns(); - handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef); + handleDataLayerRender( + selectedLayerConfig as DataLayerSpecification, + mapState, + services, + maplibreRef + ); } } else { - renderDataLayers(layers, mapState, services, maplibreRef, timeRange, filters, query); + renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); renderBaseLayers(layers, maplibreRef); // Because of async layer rendering, layers order is not guaranteed, so we need to order layers // after all layers are rendered. @@ -197,7 +195,15 @@ export const MapContainer = ({ return () => { maplibreRef.current!.off('idle', orderLayersAfterRenderLoaded); }; - }, [layers, mounted, timeRange, filters, query, mapState, isReadOnlyMode]); + }, [ + layers, + mounted, + dashboardProps?.query, + dashboardProps?.timeRange, + dashboardProps?.filters, + mapState, + isReadOnlyMode, + ]); useEffect(() => { const currentTooltipState: TOOLTIP_STATE = diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index d9ae1850..dafb0e21 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -6,8 +6,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Map as Maplibre } from 'maplibre-gl'; -import { SimpleSavedObject } from '../../../../../src/core/public'; import classNames from 'classnames'; +import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; +import { SimpleSavedObject } from '../../../../../src/core/public'; import { MapContainer } from '../map_container'; import { MapTopNavMenu } from '../map_top_nav'; import { MapServices } from '../../types'; @@ -29,32 +30,31 @@ import { } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; -import {GeoShapeFilterMeta, ShapeFilter} from "../../../../../src/plugins/data/common"; -import {GeoShapeRelation} from "@opensearch-project/opensearch/api/types"; -import {buildGeoShapeFilterMeta} from "../../model/geo/filter"; -import {FilterBar} from "../filter_bar/filter_bar"; +import { GeoShapeFilterMeta, ShapeFilter } from '../../../../../src/plugins/data/common'; +import { buildGeoShapeFilterMeta } from '../../model/geo/filter'; +import { FilterBar } from '../filter_bar/filter_bar'; interface MapPageProps { mapConfig: ConfigSchema; } -interface MapComponentProps { - mapConfig: ConfigSchema; - mapIdFromSavedObject: string; +export interface DashboardProps { timeRange?: TimeRange; - isReadOnlyMode: boolean; refreshConfig?: RefreshInterval; filters?: Filter[]; query?: Query; } + +interface MapComponentProps { + mapConfig: ConfigSchema; + mapIdFromSavedObject: string; + dashboardProps?: DashboardProps; +} + export const MapComponent = ({ mapIdFromSavedObject, mapConfig, - timeRange, - isReadOnlyMode, - refreshConfig, - filters, - query, + dashboardProps, }: MapComponentProps) => { const { services } = useOpenSearchDashboards(); const { @@ -67,6 +67,7 @@ export const MapComponent = ({ const maplibreRef = useRef(null); const [mapState, setMapState] = useState(getInitialMapState()); const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(true); + const isReadOnlyMode = !!dashboardProps; useEffect(() => { if (mapIdFromSavedObject) { @@ -87,8 +88,9 @@ export const MapComponent = ({ setLayersIndexPatterns(savedIndexPatterns); }); } else { - const initialDefaultLayer: MapLayerSpecification = - getLayerConfigMap(mapConfig)[OPENSEARCH_MAP_LAYER.type]; + const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap(mapConfig)[ + OPENSEARCH_MAP_LAYER.type + ] as MapLayerSpecification; initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; setLayers([initialDefaultLayer]); } @@ -125,8 +127,6 @@ export const MapComponent = ({ {isReadOnlyMode ? null : ( { const { id: mapId } = useParams<{ id: string }>(); - return ; + return ; }; diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index 2f3cb9dc..d6519d94 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -24,8 +24,6 @@ interface MapTopNavMenuProps { maplibreRef: any; mapState: MapState; setMapState: (mapState: MapState) => void; - isReadOnlyMode: boolean; - timeRange?: TimeRange; originatingApp?: string; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; } @@ -33,8 +31,6 @@ interface MapTopNavMenuProps { export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, - isReadOnlyMode, - timeRange, layers, layersIndexPatterns, maplibreRef, @@ -99,26 +95,21 @@ export const MapTopNavMenu = ({ const handleQuerySubmit = ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => { setIsUpdatingLayerRender(true); - if (query) { - setMapState({ ...mapState, query }); - } - if (dateRange) { - setMapState({ ...mapState, timeRange: dateRange }); - } + const updatedMapState = { + ...mapState, + ...(query && { query }), + ...(dateRange && { timeRange: dateRange }), + }; + setMapState(updatedMapState); }; useEffect(() => { - if (!isReadOnlyMode) { - setDateFrom(mapState.timeRange.from); - setDateTo(mapState.timeRange.to); - } else { - setDateFrom(timeRange!.from); - setDateTo(timeRange!.to); - } + setDateFrom(mapState.timeRange.from); + setDateTo(mapState.timeRange.to); setQueryConfig(mapState.query); setIsRefreshPaused(mapState.refreshInterval.pause); setRefreshIntervalValue(mapState.refreshInterval.value); - }, [mapState, timeRange]); + }, [mapState.timeRange, mapState.query, mapState.refreshInterval]); const onRefreshChange = useCallback( ({ isPaused, refreshInterval }: { isPaused: boolean; refreshInterval: number }) => { @@ -142,14 +133,15 @@ export const MapTopNavMenu = ({ }, [services, mapIdFromUrl, layers, title, description, mapState, originatingApp]); return ( + // @ts-ignore ); diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 06753541..27057501 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -5,30 +5,36 @@ import { Map as Maplibre } from 'maplibre-gl'; import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; -import { MapLayerSpecification } from './mapLayerType'; +import { DataLayerSpecification, MapLayerSpecification } from './mapLayerType'; import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; import { buildOpenSearchQuery, Filter, FilterMeta, + FILTERS, GeoBoundingBoxFilter, getTime, IOpenSearchDashboardsSearchResponse, isCompleteResponse, - TimeRange, Query, - FILTERS, + TimeRange, } from '../../../../src/plugins/data/common'; -import { getDataLayers, getBaseLayers, layersFunctionMap } from './layersFunctions'; +import { getBaseLayers, getDataLayers, layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; import { GeoBounds, getBounds } from './map/boundary'; import { buildBBoxFilter, buildGeoShapeFilter } from './geo/filter'; +import { DashboardProps } from '../components/map_page/map_page'; interface MaplibreRef { current: Maplibre | null; } +interface MapGlobalStates { + timeRange: TimeRange; + query: Query; +} + const getSupportedOperations = (field: string): GeoShapeRelation[] => { switch (field) { case 'geo_point': @@ -44,9 +50,8 @@ export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, { data, toastNotifications }: MapServices, - filters: Filter[] = [], - timeRange?: TimeRange, - query?: Query + maplibreRef: MaplibreRef, + dashboardProps?: DashboardProps ): Promise => { return new Promise(async (resolve, reject) => { if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { @@ -62,18 +67,16 @@ export const prepareDataLayerSource = ( if (label && label.enabled && label.textType === 'by_field' && label.textByField.length > 0) { sourceFields.push(label.textByField); } - let buildQuery; - let selectedTimeRange; + let mergedQuery; if (indexPattern) { - if (timeRange) { - selectedTimeRange = timeRange; - } else { - selectedTimeRange = mapState.timeRange; - } + const { timeRange: selectedTimeRange, query: selectedSearchQuery } = getGlobalStates( + mapState, + dashboardProps + ); const timeFilters = getTime(indexPattern, selectedTimeRange); - buildQuery = buildOpenSearchQuery(indexPattern, query ? [query] : [], [ - ...filters, - ...(layer.source.filters ? layer.source.filters : []), + const mergedFilters = getMergedFilters(layer, mapState, maplibreRef, dashboardProps); + mergedQuery = buildOpenSearchQuery(indexPattern, selectedSearchQuery, [ + ...mergedFilters, ...(timeFilters ? [timeFilters] : []), ]); } @@ -83,7 +86,7 @@ export const prepareDataLayerSource = ( size: layer.source.documentRequestNumber, body: { _source: sourceFields, - query: buildQuery, + query: mergedQuery, }, }, }; @@ -109,20 +112,64 @@ export const prepareDataLayerSource = ( }; export const handleDataLayerRender = ( - mapLayer: MapLayerSpecification, + mapLayer: DataLayerSpecification, mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, - timeRange?: TimeRange, - filtersFromDashboard?: Filter[], - query?: Query + dashboardProps?: DashboardProps ) => { - if (mapLayer.type !== DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - return; + return prepareDataLayerSource(mapLayer, mapState, services, maplibreRef, dashboardProps).then( + (result) => { + const { layer, dataSource } = result; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource); + } + } + ); +}; + +const getMergedFilters = ( + mapLayer: DataLayerSpecification, + mapState: MapState, + maplibre: MaplibreRef, + dashboardProps?: DashboardProps +): Filter[] => { + const mergedFilters: Filter[] = []; + + // add layer local filters if applicable + if (mapLayer.source.filters) { + mergedFilters.push(...mapLayer.source.filters); + } + + // add dashboard filters if applicable + if (dashboardProps?.filters) { + mergedFilters.push(...dashboardProps.filters); } - // filters are passed from dashboard filters and geo bounding box filters - const filters: Filter[] = []; - filters.push(...(filtersFromDashboard ? filtersFromDashboard : [])); + + // add global filters from map state if applicable + if (mapLayer.source?.applyGlobalFilters ?? true) { + // add spatial filters from map state if applicable + if (mapState?.spatialMetaFilters) { + const geoField = mapLayer.source.geoFieldName; + const geoFieldType = mapLayer.source.geoFieldType; + mapState?.spatialMetaFilters?.map((value) => { + if (getSupportedOperations(geoFieldType).includes(value.params.relation)) { + mergedFilters.push(buildGeoShapeFilter(geoField, value)); + } + }); + } + } + + // build and add GeoBoundingBox filter from map state if applicable + const geoBoundingBoxFilter = getGeoBoundingBoxFilter(mapLayer, maplibre); + mergedFilters.push(geoBoundingBoxFilter); + return mergedFilters; +}; + +const getGeoBoundingBoxFilter = ( + mapLayer: DataLayerSpecification, + maplibreRef: MaplibreRef +): GeoBoundingBoxFilter => { const geoField = mapLayer.source.geoFieldName; const geoFieldType = mapLayer.source.geoFieldType; @@ -134,26 +181,27 @@ export const handleDataLayerRender = ( negate: false, type: FILTERS.GEO_BOUNDING_BOX, }; - const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); - filters.push(geoBoundingBoxFilter); - - // build and add GeoShape filters from map state if applicable - if (mapLayer.source?.applyGlobalFilters ?? true) { - mapState?.spatialMetaFilters?.map((value) => { - if (getSupportedOperations(geoFieldType).includes(value.params.relation)) { - filters.push(buildGeoShapeFilter(geoField, value)); - } - }); - } + return buildBBoxFilter(geoField, mapBounds, meta); +}; - return prepareDataLayerSource(mapLayer, mapState, services, filters, timeRange, query).then( - (result) => { - const { layer, dataSource } = result; - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource); - } +const getGlobalStates = (mapState: MapState, dashboardProps?: DashboardProps): MapGlobalStates => { + if (!!dashboardProps) { + if (!dashboardProps.timeRange) { + throw new Error('timeRange is not defined in dashboard mode'); } - ); + if (!dashboardProps.query) { + throw new Error('query is not defined in dashboard mode'); + } + return { + timeRange: dashboardProps.timeRange, + query: dashboardProps.query, + }; + } else { + return { + timeRange: mapState.timeRange, + query: mapState.query, + }; + } }; export const handleBaseLayerRender = ( @@ -168,20 +216,10 @@ export const renderDataLayers = ( mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, - timeRange?: TimeRange, - filtersFromDashboard?: Filter[], - query?: Query + dashboardProps?: DashboardProps ): void => { getDataLayers(layers).forEach((layer) => { - handleDataLayerRender( - layer, - mapState, - services, - maplibreRef, - timeRange, - filtersFromDashboard, - query - ); + handleDataLayerRender(layer, mapState, services, maplibreRef, dashboardProps); }); }; From e21cd6c9187ea41101a20cff5a1c95f8aa5d52e6 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 10 Apr 2023 13:12:53 -0500 Subject: [PATCH 61/77] Add before layer id when adding documents label (#387) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + common/map_saved_object_attributes.ts | 2 +- .../layer_config/layer_config_panel.tsx | 1 - .../layer_control_panel/hide_layer_button.tsx | 5 +- .../layer_control_panel.tsx | 6 +- .../map_container/map_container.tsx | 1 + public/model/OSMLayerFunctions.ts | 7 +- public/model/customLayerFunctions.ts | 7 +- public/model/documentLayerFunctions.ts | 8 +- public/model/layerRenderController.ts | 6 +- public/model/layersFunction.test.ts | 105 ++++++++++++++++++ public/model/layersFunctions.ts | 44 +++----- public/model/map/layer_operations.test.ts | 102 +++++++++-------- public/model/map/layer_operations.ts | 19 +++- 14 files changed, 203 insertions(+), 111 deletions(-) create mode 100644 public/model/layersFunction.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ef7893..dfa7eb7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Update draw filter shape ui properties ([#372](https://github.com/opensearch-project/dashboards-maps/pull/372)) * Add filter bar to display global geospatial filters ([#371](https://github.com/opensearch-project/dashboards-maps/pull/371)) * Change font opacity along with OpenSearch base map layer ([#373](https://github.com/opensearch-project/dashboards-maps/pull/373)) +* Add before layer id when adding documents label ([#387](https://github.com/opensearch-project/dashboards-maps/pull/387)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/common/map_saved_object_attributes.ts b/common/map_saved_object_attributes.ts index 3d071ebb..70d0dcde 100644 --- a/common/map_saved_object_attributes.ts +++ b/common/map_saved_object_attributes.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectAttributes } from 'opensearch-dashboards/server'; +import { SavedObjectAttributes } from '../../../src/core/types'; export interface MapSavedObjectAttributes extends SavedObjectAttributes { /** Title of the map */ diff --git a/public/components/layer_config/layer_config_panel.tsx b/public/components/layer_config/layer_config_panel.tsx index 52c8c146..75161450 100644 --- a/public/components/layer_config/layer_config_panel.tsx +++ b/public/components/layer_config/layer_config_panel.tsx @@ -90,7 +90,6 @@ export const LayerConfigPanel = ({ updateLayer(); closeLayerConfigPanel(false); setOriginLayerConfig(null); - setSelectedLayerConfig(undefined); if (isNewLayer) { setIsNewLayer(false); } diff --git a/public/components/layer_control_panel/hide_layer_button.tsx b/public/components/layer_control_panel/hide_layer_button.tsx index 913f8b8f..73652c95 100644 --- a/public/components/layer_control_panel/hide_layer_button.tsx +++ b/public/components/layer_control_panel/hide_layer_button.tsx @@ -5,7 +5,6 @@ import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; import React, { useState } from 'react'; -import { Map as Maplibre } from 'maplibre-gl'; import { LAYER_PANEL_HIDE_LAYER_ICON, LAYER_PANEL_SHOW_LAYER_ICON, @@ -13,10 +12,8 @@ import { } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { updateLayerVisibilityHandler } from '../../model/map/layer_operations'; +import { MaplibreRef } from '../../model/layersFunctions'; -interface MaplibreRef { - current: Maplibre | null; -} interface HideLayerProps { layer: MapLayerSpecification; maplibreRef: MaplibreRef; diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index e759522e..e4f0a65d 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -21,10 +21,10 @@ import { EuiToolTip, } from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; -import { Map as Maplibre } from 'maplibre-gl'; import './layer_control_panel.scss'; import { isEqual } from 'lodash'; import { i18n } from '@osd/i18n'; +import { MaplibreRef } from 'public/model/layersFunctions'; import { IndexPattern } from '../../../../../src/plugins/data/public'; import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; @@ -38,10 +38,6 @@ import { moveLayers, removeLayers } from '../../model/map/layer_operations'; import { DeleteLayerModal } from './delete_layer_modal'; import { HideLayer } from './hide_layer_button'; -interface MaplibreRef { - current: Maplibre | null; -} - interface Props { maplibreRef: MaplibreRef; setLayers: (layers: MapLayerSpecification[]) => void; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 6f3fcca3..f87898fd 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -183,6 +183,7 @@ export const MapContainer = ({ maplibreRef ); } + setSelectedLayerConfig(undefined); } else { renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); renderBaseLayers(layers, maplibreRef); diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index 057da37f..972e6218 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -1,4 +1,4 @@ -import { Map as Maplibre, LayerSpecification, SymbolLayerSpecification } from 'maplibre-gl'; +import { LayerSpecification, SymbolLayerSpecification } from 'maplibre-gl'; import { OSMLayerSpecification } from './mapLayerType'; import { addOSMLayerSource, @@ -9,10 +9,7 @@ import { updateOSMStyleLayer, } from './map/layer_operations'; import { getMapLanguage } from '../../common/util'; - -interface MaplibreRef { - current: Maplibre | null; -} +import { MaplibreRef } from './layersFunctions'; // Fetch style layers from OpenSearch vector tile service const fetchStyleLayers = (url: string) => { diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index 5de803c6..51fc2a2d 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -1,10 +1,7 @@ -import { Map as Maplibre, AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; +import { AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; import { hasLayer, removeLayers } from './map/layer_operations'; - -interface MaplibreRef { - current: Maplibre | null; -} +import { MaplibreRef } from './layersFunctions'; const updateLayerConfig = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { const maplibreInstance = maplibreRef.current; diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts index 6ed22ef6..eff50bda 100644 --- a/public/model/documentLayerFunctions.ts +++ b/public/model/documentLayerFunctions.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Map as Maplibre } from 'maplibre-gl'; import { parse } from 'wellknown'; import { DocumentLayerSpecification } from './mapLayerType'; import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; @@ -21,10 +20,8 @@ import { removeSymbolLayer, createSymbolLayerSpecification, } from './map/layer_operations'; +import { getMaplibreAboveLayerId, MaplibreRef } from './layersFunctions'; -interface MaplibreRef { - current: Maplibre | null; -} // https://opensearch.org/docs/1.3/opensearch/supported-field-types/geo-shape const openSearchGeoJSONMap = new Map([ ['point', 'Point'], @@ -227,7 +224,8 @@ const renderLabelLayer = (layerConfig: DocumentLayerSpecification, maplibreRef: if (hasLabelLayer) { updateSymbolLayer(maplibreRef.current!, symbolLayerSpec); } else { - addSymbolLayer(maplibreRef.current!, symbolLayerSpec); + const beforeLayerId = getMaplibreAboveLayerId(layerConfig.id, maplibreRef.current!); + addSymbolLayer(maplibreRef.current!, symbolLayerSpec, beforeLayerId); } } else { // If the label set to disabled, remove the label layer if it exists diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 27057501..b7527f8c 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -19,17 +19,13 @@ import { Query, TimeRange, } from '../../../../src/plugins/data/common'; -import { getBaseLayers, getDataLayers, layersFunctionMap } from './layersFunctions'; +import { getBaseLayers, getDataLayers, layersFunctionMap, MaplibreRef } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; import { GeoBounds, getBounds } from './map/boundary'; import { buildBBoxFilter, buildGeoShapeFilter } from './geo/filter'; import { DashboardProps } from '../components/map_page/map_page'; -interface MaplibreRef { - current: Maplibre | null; -} - interface MapGlobalStates { timeRange: TimeRange; query: Query; diff --git a/public/model/layersFunction.test.ts b/public/model/layersFunction.test.ts new file mode 100644 index 00000000..9367fefa --- /dev/null +++ b/public/model/layersFunction.test.ts @@ -0,0 +1,105 @@ +import { + baseLayerTypeLookup, + getBaseLayers, + getDataLayers, + getMaplibreAboveLayerId, + layersFunctionMap, + layersTypeIconMap, + layersTypeNameMap, +} from './layersFunctions'; +import { MapLayerSpecification } from './mapLayerType'; +import { OSMLayerFunctions } from './OSMLayerFunctions'; +import { DocumentLayerFunctions } from './documentLayerFunctions'; +import { CustomLayerFunctions } from './customLayerFunctions'; +import { MockMaplibreMap } from './map/__mocks__/map'; +import { MockLayer } from './map/__mocks__/layer'; +import { Map as Maplibre } from 'maplibre-gl'; + +describe('getDataLayers', () => { + it('should return an array of DataLayerSpecification objects', () => { + const layers = [ + { type: 'opensearch_vector_tile_map', name: 'layer1' }, + { type: 'custom_map', name: 'layer2' }, + { type: 'documents', name: 'layer3' }, + { type: 'opensearch_vector_tile_map', name: 'layer4' }, + { type: 'custom_map', name: 'layer5' }, + ] as unknown as MapLayerSpecification[]; + const dataLayers = getDataLayers(layers); + expect(dataLayers).toHaveLength(1); + expect(dataLayers[0].name).toBe('layer3'); + }); +}); + +describe('getBaseLayers', () => { + it('should return an array of BaseLayerSpecification objects', () => { + const layers = [ + { type: 'opensearch_vector_tile_map', name: 'layer1' }, + { type: 'custom_map', name: 'layer2' }, + { type: 'documents', name: 'layer3' }, + { type: 'opensearch_vector_tile_map', name: 'layer4' }, + { type: 'custom_map', name: 'layer5' }, + ] as unknown as MapLayerSpecification[]; + const baseLayers = getBaseLayers(layers); + expect(baseLayers).toHaveLength(4); + expect(baseLayers[0].name).toBe('layer1'); + expect(baseLayers[1].name).toBe('layer2'); + expect(baseLayers[2].name).toBe('layer4'); + expect(baseLayers[3].name).toBe('layer5'); + }); +}); + +describe('Exported objects', () => { + it('should have the correct values assigned to their keys', () => { + expect(layersFunctionMap).toEqual({ + opensearch_vector_tile_map: OSMLayerFunctions, + documents: DocumentLayerFunctions, + custom_map: CustomLayerFunctions, + }); + + expect(layersTypeNameMap).toEqual({ + opensearch_vector_tile_map: 'OpenSearch map', + documents: 'Documents', + custom_map: 'Custom map', + }); + + expect(layersTypeIconMap).toEqual({ + opensearch_vector_tile_map: 'globe', + documents: 'document', + custom_map: 'globe', + }); + + expect(baseLayerTypeLookup).toEqual({ + opensearch_vector_tile_map: true, + custom_map: true, + documents: false, + }); + }); +}); + +describe('getMaplibreAboveLayerId', () => { + const mockMapLayer1Id = 'layer-1'; + const mockMapLayer2Id = 'layer-2'; + const mockMbLayer1: MockLayer = new MockLayer(`${mockMapLayer1Id}-1`); + const mockMbLayer2: MockLayer = new MockLayer(`${mockMapLayer1Id}-2`); + const mockMbLayer3: MockLayer = new MockLayer(`${mockMapLayer2Id}-1`); + const mockMap = new MockMaplibreMap([ + mockMbLayer1, + mockMbLayer2, + mockMbLayer3, + ]) as unknown as Maplibre; + + it('should return the id of the layer above the given mapLayerId', () => { + const aboveLayerId = getMaplibreAboveLayerId(mockMapLayer1Id, mockMap); + expect(aboveLayerId).toBe(`${mockMapLayer2Id}-1`); + }); + + it('should return undefined if there is no layer above the given mapLayerId', () => { + const aboveLayerId = getMaplibreAboveLayerId(mockMapLayer2Id, mockMap); + expect(aboveLayerId).toBeUndefined(); + }); + + it('should return undefined if the given mapLayerId is not found', () => { + const aboveLayerId = getMaplibreAboveLayerId('undefined-layer', mockMap); + expect(aboveLayerId).toBeUndefined(); + }); +}); diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts index 4425cfce..797518db 100644 --- a/public/model/layersFunctions.ts +++ b/public/model/layersFunctions.ts @@ -19,11 +19,7 @@ import { import { CustomLayerFunctions } from './customLayerFunctions'; import { getLayers } from './map/layer_operations'; -interface MaplibreRef { - current: Maplibre | null; -} - -interface MaplibreRef { +export interface MaplibreRef { current: Maplibre | null; } @@ -39,21 +35,6 @@ export const layersTypeNameMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, }; -export const getMaplibreBeforeLayerId = ( - selectedLayer: MapLayerSpecification, - maplibreRef: MaplibreRef, - beforeLayerId: string | undefined -): string | undefined => { - const currentLoadedMbLayers = getLayers(maplibreRef.current!); - if (beforeLayerId) { - const beforeMbLayer = currentLoadedMbLayers.find((mbLayer) => - mbLayer.id.includes(beforeLayerId) - ); - return beforeMbLayer?.id; - } - return undefined; -}; - export const layersTypeIconMap: { [key: string]: string } = { [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, @@ -74,15 +55,20 @@ export const getBaseLayers = (layers: MapLayerSpecification[]): BaseLayerSpecifi return layers.filter((layer) => baseLayerTypeLookup[layer.type]) as BaseLayerSpecification[]; }; -// Get layer id from layers that is above the selected layer -export const getMapBeforeLayerId = ( - layers: MapLayerSpecification[], - selectedLayerId: string +export const getMaplibreAboveLayerId = ( + mapLayerId: string, + maplibre: Maplibre ): string | undefined => { - const selectedLayerIndex = layers.findIndex((layer) => layer.id === selectedLayerId); - const beforeLayers = layers.slice(selectedLayerIndex + 1); - if (beforeLayers.length === 0) { - return undefined; + const currentLoadedMbLayers = getLayers(maplibre); + const matchingMbLayers = currentLoadedMbLayers.filter((mbLayer) => + mbLayer.id.includes(mapLayerId) + ); + if (matchingMbLayers.length > 0) { + const highestMbLayerIndex = currentLoadedMbLayers.indexOf( + matchingMbLayers[matchingMbLayers.length - 1] + ); + const aboveMbLayer = currentLoadedMbLayers[highestMbLayerIndex + 1]; + return aboveMbLayer?.id; } - return beforeLayers[0]?.id; + return undefined; }; diff --git a/public/model/map/layer_operations.test.ts b/public/model/map/layer_operations.test.ts index 056803f3..d886c72c 100644 --- a/public/model/map/layer_operations.test.ts +++ b/public/model/map/layer_operations.test.ts @@ -295,21 +295,25 @@ describe('Symbol layer', () => { const mockMap = new MockMaplibreMap([]); const sourceId: string = 'symbol-layer-source'; const expectedLayerId = sourceId + '-symbol'; - addSymbolLayer(mockMap as unknown as Maplibre, { - sourceId, - visibility: 'visible', - textFont: ['Noto Sans Regular'], - textByFixed: 'test text', - textColor: '#af938a', - textSize: 12, - minZoom: 2, - maxZoom: 10, - opacity: 60, - symbolBorderWidth: 2, - symbolBorderColor: '#D6BF57', - textType: 'fixed', - textByField: '', - }); + addSymbolLayer( + mockMap as unknown as Maplibre, + { + sourceId, + visibility: 'visible', + textFont: ['Noto Sans Regular'], + textByFixed: 'test text', + textColor: '#af938a', + textSize: 12, + minZoom: 2, + maxZoom: 10, + opacity: 60, + symbolBorderWidth: 2, + symbolBorderColor: '#D6BF57', + textType: 'fixed', + textByField: '', + }, + undefined + ); const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; expect(layer.getProperty('visibility')).toBe('visible'); @@ -330,21 +334,25 @@ describe('Symbol layer', () => { const mockMap = new MockMaplibreMap([]); const sourceId: string = 'symbol-layer-source'; const expectedLayerId = sourceId + '-symbol'; - addSymbolLayer(mockMap as unknown as Maplibre, { - sourceId, - visibility: 'visible', - textFont: ['Noto Sans Regular'], - textByFixed: '', - textColor: '#af938a', - textSize: 12, - minZoom: 2, - maxZoom: 10, - opacity: 60, - symbolBorderWidth: 2, - symbolBorderColor: '#D6BF57', - textType: 'by_field', - textByField: 'name_by_field', - }); + addSymbolLayer( + mockMap as unknown as Maplibre, + { + sourceId, + visibility: 'visible', + textFont: ['Noto Sans Regular'], + textByFixed: '', + textColor: '#af938a', + textSize: 12, + minZoom: 2, + maxZoom: 10, + opacity: 60, + symbolBorderWidth: 2, + symbolBorderColor: '#D6BF57', + textType: 'by_field', + textByField: 'name_by_field', + }, + undefined + ); const layer = mockMap.getLayers().filter((l) => l.getProperty('id') === expectedLayerId)[0]; expect(layer.getProperty('visibility')).toBe('visible'); @@ -366,21 +374,25 @@ describe('Symbol layer', () => { const sourceId: string = 'symbol-layer-source'; const expectedLayerId = sourceId + '-symbol'; // add layer first - addSymbolLayer(mockMap as unknown as Maplibre, { - sourceId, - visibility: 'visible', - textFont: ['Noto Sans Regular'], - textSize: 12, - textColor: '#251914', - textByFixed: 'test text by fixed', - minZoom: 2, - maxZoom: 10, - opacity: 60, - symbolBorderWidth: 2, - symbolBorderColor: '#D6BF57', - textType: 'fixed', - textByField: '', - }); + addSymbolLayer( + mockMap as unknown as Maplibre, + { + sourceId, + visibility: 'visible', + textFont: ['Noto Sans Regular'], + textSize: 12, + textColor: '#251914', + textByFixed: 'test text by fixed', + minZoom: 2, + maxZoom: 10, + opacity: 60, + symbolBorderWidth: 2, + symbolBorderColor: '#D6BF57', + textType: 'fixed', + textByField: '', + }, + undefined + ); expect(mockMap.getLayer(expectedLayerId).length).toBe(1); // update symbol for test diff --git a/public/model/map/layer_operations.ts b/public/model/map/layer_operations.ts index a9f4e1ad..43580792 100644 --- a/public/model/map/layer_operations.ts +++ b/public/model/map/layer_operations.ts @@ -247,13 +247,20 @@ export const removeSymbolLayer = (map: Maplibre, layerId: string) => { map.removeLayer(layerId + '-symbol'); }; -export const addSymbolLayer = (map: Maplibre, specification: SymbolLayerSpecification): string => { +export const addSymbolLayer = ( + map: Maplibre, + specification: SymbolLayerSpecification, + beforeMbLayerId: string | undefined +): string => { const symbolLayerId = specification.sourceId + '-symbol'; - map.addLayer({ - id: symbolLayerId, - type: 'symbol', - source: specification.sourceId, - }); + map.addLayer( + { + id: symbolLayerId, + type: 'symbol', + source: specification.sourceId, + }, + beforeMbLayerId + ); return updateSymbolLayer(map, specification); }; From 63d7232370544449c2c7ebef1e54845e1d8710b1 Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Tue, 11 Apr 2023 18:37:52 -0500 Subject: [PATCH 62/77] Toast Warning Message if OpenSearch base map is used in conflicting regions (#382) * Toast Warning Message if OpenSearch base map is used in conflicting regions Signed-off-by: Naveen Tatikonda * Add Custom map link to Toast Notification Message Signed-off-by: Naveen Tatikonda * Address Review Comments Signed-off-by: Naveen Tatikonda --------- Signed-off-by: Naveen Tatikonda --- CHANGELOG.md | 1 + .../map_container/map_container.tsx | 19 ++++++- .../map_container/maps_messages.tsx | 25 ++++++++++ public/model/OSMLayerFunctions.ts | 50 +++++++++++++++---- public/model/layerRenderController.ts | 10 ++-- 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 public/components/map_container/maps_messages.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa7eb7f..fd6b1b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Add filter bar to display global geospatial filters ([#371](https://github.com/opensearch-project/dashboards-maps/pull/371)) * Change font opacity along with OpenSearch base map layer ([#373](https://github.com/opensearch-project/dashboards-maps/pull/373)) * Add before layer id when adding documents label ([#387](https://github.com/opensearch-project/dashboards-maps/pull/387)) +* Toast Warning Message if OpenSearch base map is used in conflicting regions ([#382](https://github.com/opensearch-project/dashboards-maps/pull/382)) ### Bug Fixes * Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index f87898fd..98c7bd6d 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -33,6 +33,7 @@ import { SpatialFilterToolbar } from '../toolbar/spatial_filter/filter_toolbar'; import { DrawFilterShapeHelper } from '../toolbar/spatial_filter/display_draw_helper'; import { ShapeFilter } from '../../../../../src/plugins/data/common'; import { DashboardProps } from '../map_page/map_page'; +import { MapsServiceErrorMsg } from './maps_messages'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -49,6 +50,13 @@ interface MapContainerProps { addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; } +export class MapsServiceError extends Error { + constructor(message?: string) { + super(message); + this.name = 'MapsServiceError'; + } +} + export const MapContainer = ({ setLayers, layers, @@ -64,6 +72,13 @@ export const MapContainer = ({ addSpatialFilter, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); + + function onError(e: unknown) { + if (e instanceof MapsServiceError) { + services.toastNotifications.addWarning(MapsServiceErrorMsg); + } + } + const mapContainer = useRef(null); const [mounted, setMounted] = useState(false); const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); @@ -173,7 +188,7 @@ export const MapContainer = ({ if (isUpdatingLayerRender || isReadOnlyMode) { if (selectedLayerConfig) { if (baseLayerTypeLookup[selectedLayerConfig.type]) { - handleBaseLayerRender(selectedLayerConfig, maplibreRef); + handleBaseLayerRender(selectedLayerConfig, maplibreRef, onError); } else { updateIndexPatterns(); handleDataLayerRender( @@ -186,7 +201,7 @@ export const MapContainer = ({ setSelectedLayerConfig(undefined); } else { renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); - renderBaseLayers(layers, maplibreRef); + renderBaseLayers(layers, maplibreRef, onError); // Because of async layer rendering, layers order is not guaranteed, so we need to order layers // after all layers are rendered. maplibreRef.current!.once('idle', orderLayersAfterRenderLoaded); diff --git a/public/components/map_container/maps_messages.tsx b/public/components/map_container/maps_messages.tsx new file mode 100644 index 00000000..02d0d058 --- /dev/null +++ b/public/components/map_container/maps_messages.tsx @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { ToastInputFields } from '../../../../../src/core/public'; +import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; + +export const MapsServiceErrorMsg: ToastInputFields = { + title: 'The OpenSearch Maps Service is currently not available in your region', + text: toMountPoint( +

+ You can configure OpenSearch Dashboards to use a{' '} + + custom map + {' '} + server for dashboards maps. +

+ ), +}; diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts index 972e6218..989d2983 100644 --- a/public/model/OSMLayerFunctions.ts +++ b/public/model/OSMLayerFunctions.ts @@ -10,6 +10,17 @@ import { } from './map/layer_operations'; import { getMapLanguage } from '../../common/util'; import { MaplibreRef } from './layersFunctions'; +import { MapsServiceError } from '../components/map_container/map_container'; + +const fetchDataLayer = (dataUrl: string) => { + return fetch(dataUrl) + .then(() => true) + .catch((error) => { + // eslint-disable-next-line no-console + console.log('error', error); + throw new MapsServiceError(error.message); + }); +}; // Fetch style layers from OpenSearch vector tile service const fetchStyleLayers = (url: string) => { @@ -19,6 +30,7 @@ const fetchStyleLayers = (url: string) => { .catch((error) => { // eslint-disable-next-line no-console console.log('error', error); + throw new MapsServiceError(error.message); }); }; @@ -43,28 +55,46 @@ const setLanguage = (maplibreRef: MaplibreRef, styleLayer: LayerSpecification) = } }; -const addNewLayer = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { +const addNewLayer = ( + layerConfig: OSMLayerSpecification, + maplibreRef: MaplibreRef, + onError: Function +) => { if (maplibreRef.current) { const maplibre = maplibreRef.current; const { id, source, style } = layerConfig; - addOSMLayerSource(maplibre, id, source.dataURL); - fetchStyleLayers(style?.styleURL).then((styleLayers: LayerSpecification[]) => { - styleLayers.forEach((layer) => { - const styleLayer = getOSMStyleLayerWithMapLayerId(id, layer); - addOSMStyleLayer(maplibre, layerConfig, styleLayer); - setLanguage(maplibreRef, styleLayer); + fetchDataLayer(source.dataURL) + .then(() => { + addOSMLayerSource(maplibre, id, source.dataURL); + fetchStyleLayers(style?.styleURL) + .then((styleLayers: LayerSpecification[]) => { + styleLayers.forEach((layer) => { + const styleLayer = getOSMStyleLayerWithMapLayerId(id, layer); + addOSMStyleLayer(maplibre, layerConfig, styleLayer); + setLanguage(maplibreRef, styleLayer); + }); + }) + .catch((e) => { + if (onError) { + onError(e); + } + }); + }) + .catch((e) => { + if (onError) { + onError(e); + } }); - }); } }; // Functions for OpenSearch maps vector tile layer export const OSMLayerFunctions = { - render: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + render: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification, onError: Function) => { // If layer already exist in maplibre source, update layer config // else add new layer. return hasLayer(maplibreRef.current!, layerConfig.id) ? updateLayerConfig(layerConfig, maplibreRef) - : addNewLayer(layerConfig, maplibreRef); + : addNewLayer(layerConfig, maplibreRef, onError); }, }; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index b7527f8c..e4e8a2eb 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -202,9 +202,10 @@ const getGlobalStates = (mapState: MapState, dashboardProps?: DashboardProps): M export const handleBaseLayerRender = ( layer: MapLayerSpecification, - maplibreRef: MaplibreRef + maplibreRef: MaplibreRef, + onError: Function ): void => { - layersFunctionMap[layer.type].render(maplibreRef, layer); + layersFunctionMap[layer.type].render(maplibreRef, layer, onError); }; export const renderDataLayers = ( @@ -221,10 +222,11 @@ export const renderDataLayers = ( export const renderBaseLayers = ( layers: MapLayerSpecification[], - maplibreRef: MaplibreRef + maplibreRef: MaplibreRef, + onError: Function ): void => { getBaseLayers(layers).forEach((layer) => { - handleBaseLayerRender(layer, maplibreRef); + handleBaseLayerRender(layer, maplibreRef, onError); }); }; From 1d7e1e993d6057e26b89050feb76a793dbafafb6 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 17 Apr 2023 10:18:24 -0500 Subject: [PATCH 63/77] Fix cypress test failed due to OSD dashboard create button updated (#393) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + cypress/integration/add_map_to_dashboard.spec.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6b1b9e..e6703222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Infrastructure * Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) * Add stats API for maps and layers in maps plugin ([#362](https://github.com/opensearch-project/dashboards-maps/pull/362)) +* Fix cypress test failed due to OSD dashboard create button updated ([#393](https://github.com/opensearch-project/dashboards-maps/pull/393)) ### Documentation diff --git a/cypress/integration/add_map_to_dashboard.spec.js b/cypress/integration/add_map_to_dashboard.spec.js index fbb3da8f..02f327c5 100644 --- a/cypress/integration/add_map_to_dashboard.spec.js +++ b/cypress/integration/add_map_to_dashboard.spec.js @@ -20,7 +20,7 @@ describe('Add map to dashboard', () => { it('Add new map to dashboard', () => { const testMapName = 'saved-map-' + Date.now().toString(); cy.visit(`${BASE_PATH}/app/dashboards`); - cy.get('button[data-test-subj="newItemButton"]').click(); + cy.get('[data-test-subj="newItemButton"]').click(); cy.get('button[data-test-subj="dashboardAddNewPanelButton"]').click(); cy.get('button[data-test-subj="visType-customImportMap"]').click(); cy.wait(5000).get('button[data-test-subj="mapSaveButton"]').click(); From 3507cf47ac4a4e55a3256a676da8f5360a9e8d1b Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Mon, 17 Apr 2023 12:19:59 -0500 Subject: [PATCH 64/77] Add 2.7.0 Release Notes (#392) Signed-off-by: Naveen Tatikonda --- CHANGELOG.md | 39 +------------- ...h-dashboards-maps.release-notes-2.7.0.0.md | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 release-notes/opensearch-dashboards-maps.release-notes-2.7.0.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e6703222..2e252c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,54 +12,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Maintenance ### Refactoring -## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.6...2.x) +## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.7...2.x) ### Features -* Support adding static label to document layer ([#322](https://github.com/opensearch-project/dashboards-maps/pull/322)) -* Add geo shape query filter ([#319](https://github.com/opensearch-project/dashboards-maps/pull/319)) -* Add shape filter UI button ([#329](https://github.com/opensearch-project/dashboards-maps/pull/329)) -* Add tooltip to draw shape filter ([#330](https://github.com/opensearch-project/dashboards-maps/pull/330)) -* Add search query ability on map([#370](https://github.com/opensearch-project/dashboards-maps/pull/370)) ### Enhancements -* Enhance layer visibility status display ([#299](https://github.com/opensearch-project/dashboards-maps/pull/299)) -* Introduce disable tooltip on hover property ([#313](https://github.com/opensearch-project/dashboards-maps/pull/313)) -* Update tooltip behavior change ([#317](https://github.com/opensearch-project/dashboards-maps/pull/317)) -* Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) -* BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) -* Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) -* Add support to draw rectangle shape to filter documents ([#348](https://github.com/opensearch-project/dashboards-maps/pull/348)) -* Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) -* Add support to build GeoShapeFilterMeta and GeoShapeFilter ([#360](https://github.com/opensearch-project/dashboards-maps/pull/360)) -* Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) -* Add geoshape filter while render data layers ([#365](https://github.com/opensearch-project/dashboards-maps/pull/365) -* Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) -* Update draw filter shape ui properties ([#372](https://github.com/opensearch-project/dashboards-maps/pull/372)) -* Add filter bar to display global geospatial filters ([#371](https://github.com/opensearch-project/dashboards-maps/pull/371)) -* Change font opacity along with OpenSearch base map layer ([#373](https://github.com/opensearch-project/dashboards-maps/pull/373)) -* Add before layer id when adding documents label ([#387](https://github.com/opensearch-project/dashboards-maps/pull/387)) -* Toast Warning Message if OpenSearch base map is used in conflicting regions ([#382](https://github.com/opensearch-project/dashboards-maps/pull/382)) ### Bug Fixes -* Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) -* Show scroll bar when panel height reaches container bottom ([#295](https://github.com/opensearch-project/dashboards-maps/pull/295)) -* fix: fixed filters not reset when index pattern changed ([#234](https://github.com/opensearch-project/dashboards-maps/pull/234)) -* Add custom layer visibility config to render ([#297](https://github.com/opensearch-project/dashboards-maps/pull/297)) -* Fix color picker component issue ([#305](https://github.com/opensearch-project/dashboards-maps/pull/305)) -* fix: layer filter setting been reset unexpectedly ([#327](https://github.com/opensearch-project/dashboards-maps/pull/327)) -* Fix data query in dashboard mode when enable around map filter ([#339](https://github.com/opensearch-project/dashboards-maps/pull/339)) -* Sync maplibre layer order after layers rendered ([#353](https://github.com/opensearch-project/dashboards-maps/pull/353)) ### Infrastructure -* Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) -* Add stats API for maps and layers in maps plugin ([#362](https://github.com/opensearch-project/dashboards-maps/pull/362)) -* Fix cypress test failed due to OSD dashboard create button updated ([#393](https://github.com/opensearch-project/dashboards-maps/pull/393)) ### Documentation ### Maintenance ### Refactoring -* Move zoom and coordinates as separate component ([#309](https://github.com/opensearch-project/dashboards-maps/pull/309)) -* Move coordinates to footer ([#315](https://github.com/opensearch-project/dashboards-maps/pull/315)) -* Refactor tooltip setup as component ([#320](https://github.com/opensearch-project/dashboards-maps/pull/320)) -* Refactor get field options and add field label option on UI ([#328](https://github.com/opensearch-project/dashboards-maps/pull/328)) diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.7.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.7.0.0.md new file mode 100644 index 00000000..d08fee02 --- /dev/null +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.7.0.0.md @@ -0,0 +1,54 @@ +## Version 2.7.0.0 Release Notes +Compatible with OpenSearch and OpenSearch Dashboards Version 2.7.0 + +### Features +* Add localization for opensearch base map ([#312](https://github.com/opensearch-project/dashboards-maps/pull/312)) +* Add geo shape query filter ([#319](https://github.com/opensearch-project/dashboards-maps/pull/319)) +* Support adding static label to document layer ([#322](https://github.com/opensearch-project/dashboards-maps/pull/322)) +* Add shape filter UI button ([#329](https://github.com/opensearch-project/dashboards-maps/pull/329)) +* Add tooltip to draw shape filter ([#330](https://github.com/opensearch-project/dashboards-maps/pull/330)) +* Support adding field label to document layer ([#336](https://github.com/opensearch-project/dashboards-maps/pull/336)) +* Add search query ability on map([#370](https://github.com/opensearch-project/dashboards-maps/pull/370)) + +### Infrastructure +* Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342)) +* Add stats API for maps and layers in maps plugin ([#362](https://github.com/opensearch-project/dashboards-maps/pull/362)) +* Fix cypress test failed due to OSD dashboard create button updated ([#393](https://github.com/opensearch-project/dashboards-maps/pull/393)) + +### Refactoring +* Move zoom and coordinates as separate component ([#309](https://github.com/opensearch-project/dashboards-maps/pull/309)) +* Move coordinates to footer ([#315](https://github.com/opensearch-project/dashboards-maps/pull/315)) +* Refactor tooltip setup as component ([#320](https://github.com/opensearch-project/dashboards-maps/pull/320)) +* Refactor get field options and add field label option on UI ([#328](https://github.com/opensearch-project/dashboards-maps/pull/328)) + +### Enhancements +* Enhance layer visibility status display ([#299](https://github.com/opensearch-project/dashboards-maps/pull/299)) +* Introduce disable tooltip on hover property ([#313](https://github.com/opensearch-project/dashboards-maps/pull/313)) +* Update tooltip behavior change ([#317](https://github.com/opensearch-project/dashboards-maps/pull/317)) +* Update max supported layer count ([#332](https://github.com/opensearch-project/dashboards-maps/pull/332)) +* BWC for document layer label textType ([#340](https://github.com/opensearch-project/dashboards-maps/pull/340)) +* Add mapbox-gl draw mode ([#347](https://github.com/opensearch-project/dashboards-maps/pull/347)) +* Add support to draw rectangle shape to filter documents ([#348](https://github.com/opensearch-project/dashboards-maps/pull/348)) +* Avoid trigger tooltip from label ([#350](https://github.com/opensearch-project/dashboards-maps/pull/350)) +* Remove cancel button on draw shape and use Escape to cancel draw ([#359](https://github.com/opensearch-project/dashboards-maps/pull/359)) +* Add support to build GeoShapeFilterMeta and GeoShapeFilter ([#360](https://github.com/opensearch-project/dashboards-maps/pull/360)) +* Update listener on KeyUp ([#364](https://github.com/opensearch-project/dashboards-maps/pull/364)) +* Add geoshape filter while render data layers ([#365](https://github.com/opensearch-project/dashboards-maps/pull/365)) +* Add filter bar to display global geospatial filters ([#371](https://github.com/opensearch-project/dashboards-maps/pull/371)) +* Update draw filter shape ui properties ([#372](https://github.com/opensearch-project/dashboards-maps/pull/372)) +* Change font opacity along with OpenSearch base map layer ([#373](https://github.com/opensearch-project/dashboards-maps/pull/373)) +* Toast Warning Message if OpenSearch base map is used in conflicting regions ([#382](https://github.com/opensearch-project/dashboards-maps/pull/382)) +* Add before layer id when adding documents label ([#387](https://github.com/opensearch-project/dashboards-maps/pull/387)) + +### Bug Fixes +* Fix: fixed filters not reset when index pattern changed ([#234](https://github.com/opensearch-project/dashboards-maps/pull/234)) +* Fix property value undefined check ([#276](https://github.com/opensearch-project/dashboards-maps/pull/276)) +* Show scroll bar when panel height reaches container bottom ([#295](https://github.com/opensearch-project/dashboards-maps/pull/295)) +* Add custom layer visibility config to render ([#297](https://github.com/opensearch-project/dashboards-maps/pull/297)) +* Fix color picker component issue ([#305](https://github.com/opensearch-project/dashboards-maps/pull/305)) +* Fix: layer filter setting been reset unexpectedly ([#327](https://github.com/opensearch-project/dashboards-maps/pull/327)) +* Fix data query in dashboard mode when enable around map filter ([#339](https://github.com/opensearch-project/dashboards-maps/pull/339)) +* Sync maplibre layer order after layers rendered ([#353](https://github.com/opensearch-project/dashboards-maps/pull/353)) + +### Maintenance +* Bump to 2.7.0 version ([#356](https://github.com/opensearch-project/dashboards-maps/pull/356)) From b2b4cd3c562c8a255a84726cb63446cb45ce1717 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Wed, 26 Apr 2023 16:58:44 -0500 Subject: [PATCH 65/77] Remove package-lock.json (#400) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + package-lock.json | 249 ---------------------------------------------- 2 files changed, 1 insertion(+), 249 deletions(-) delete mode 100644 package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e252c6a..88c745ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,5 +24,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Documentation ### Maintenance +Remove package-lock.json ([#400](https://github.com/opensearch-project/dashboards-maps/pull/400)) ### Refactoring diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b786a608..00000000 --- a/package-lock.json +++ /dev/null @@ -1,249 +0,0 @@ -{ - "name": "mapsDashboards", - "version": "3.0.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@mapbox/geojson-rewind": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", - "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", - "requires": { - "get-stream": "^6.0.1", - "minimist": "^1.2.6" - } - }, - "@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==" - }, - "@mapbox/mapbox-gl-supported": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", - "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" - }, - "@mapbox/point-geometry": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", - "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" - }, - "@mapbox/tiny-sdf": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz", - "integrity": "sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw==" - }, - "@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" - }, - "@mapbox/vector-tile": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", - "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", - "requires": { - "@mapbox/point-geometry": "~0.1.0" - } - }, - "@mapbox/whoots-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" - }, - "@types/geojson": { - "version": "7946.0.10", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" - }, - "@types/mapbox__point-geometry": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", - "integrity": "sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA==" - }, - "@types/mapbox__vector-tile": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz", - "integrity": "sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g==", - "requires": { - "@types/geojson": "*", - "@types/mapbox__point-geometry": "*", - "@types/pbf": "*" - } - }, - "@types/pbf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz", - "integrity": "sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ==" - }, - "csscolorparser": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" - }, - "earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" - }, - "geojson-vt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" - }, - "gl-matrix": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", - "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "kdbush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "maplibre-gl": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-2.4.0.tgz", - "integrity": "sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==", - "requires": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^2.0.1", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^2.0.5", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "@types/geojson": "^7946.0.10", - "@types/mapbox__point-geometry": "^0.1.2", - "@types/mapbox__vector-tile": "^1.3.0", - "@types/pbf": "^3.0.2", - "csscolorparser": "~1.0.3", - "earcut": "^2.2.4", - "geojson-vt": "^3.2.1", - "gl-matrix": "^3.4.3", - "global-prefix": "^3.0.0", - "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", - "potpack": "^1.0.2", - "quickselect": "^2.0.0", - "supercluster": "^7.1.5", - "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.3" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "murmurhash-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", - "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" - }, - "pbf": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", - "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", - "requires": { - "ieee754": "^1.1.12", - "resolve-protobuf-schema": "^2.1.0" - } - }, - "potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" - }, - "protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" - }, - "quickselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" - }, - "resolve-protobuf-schema": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", - "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", - "requires": { - "protocol-buffers-schema": "^3.3.1" - } - }, - "supercluster": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", - "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", - "requires": { - "kdbush": "^3.0.0" - } - }, - "tinyqueue": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, - "vt-pbf": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", - "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", - "requires": { - "@mapbox/point-geometry": "0.1.0", - "@mapbox/vector-tile": "^1.3.1", - "pbf": "^3.2.1" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - } - } -} From ce8d8295b2aa1307bbcd05be0201c8bfa1d86962 Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Wed, 26 Apr 2023 17:03:57 -0500 Subject: [PATCH 66/77] Fix failing cypress test (#401) Signed-off-by: Naveen Tatikonda --- cypress/integration/add_saved_object.spec.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cypress/integration/add_saved_object.spec.js b/cypress/integration/add_saved_object.spec.js index 81571bf2..34a61d8d 100644 --- a/cypress/integration/add_saved_object.spec.js +++ b/cypress/integration/add_saved_object.spec.js @@ -7,13 +7,10 @@ import { BASE_PATH } from '../utils/constants'; describe('Add flights dataset saved object', () => { before(() => { - cy.visit(`${BASE_PATH}/app/maps-dashboards`, { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { retryOnStatusCodeFailure: true, timeout: 60000, }); - cy.get('div[data-test-subj="indexPatternEmptyState"]', { timeout: 60000 }) - .contains(/Add sample data/) - .click(); cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) .contains(/Add data/) .click(); From dff9030cf20151ccc35c7565ff12fb5c0151cea0 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Wed, 10 May 2023 18:39:27 -0400 Subject: [PATCH 67/77] Fix: CODEOWNERS. (#410) Signed-off-by: dblock --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e63c800..589ec140 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams -* @opensearch-project/dashboards-maps \ No newline at end of file +# This should match the list in MAINTAINERS.md. +* @VijayanB @vamshin @junqiu-lei \ No newline at end of file From f807fd27f64cfe746d19f497a389b5f1a8099198 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 22 May 2023 19:05:31 -0500 Subject: [PATCH 68/77] Update repo owners and maintainers (#414) Signed-off-by: Junqiu Lei --- .github/CODEOWNERS | 2 +- MAINTAINERS.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 589ec140..10d43097 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # This should match the list in MAINTAINERS.md. -* @VijayanB @vamshin @junqiu-lei \ No newline at end of file +* @heemin32 @navneet1v @VijayanB @vamshin @jmazanec15 @naveentatikonda @junqiu-lei @martin-gaievski diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 927e4248..645933e3 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -3,12 +3,16 @@ This document contains a list of maintainers in this repo. See [opensearch-project/.github/RESPONSIBILITIES.md](https://github.com/opensearch-project/.github/blob/main/RESPONSIBILITIES.md#maintainer-responsibilities) that explains what the role of maintainer means, what maintainers do in this and other repos, and how they should be doing it. If you're interested in contributing, and becoming a maintainer, see [CONTRIBUTING](CONTRIBUTING.md). ## Current Maintainers - -| Maintainer | GitHub ID | Affiliation | -|-------------------------|---------------------------------------------| ----------- | -| Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | -| Vamshi Vijay Nakkirtha | [vamshin](https://github.com/vamshin) | Amazon | -| Junqiu Lei | [junqiu-lei](https://github.com/junqiu-lei) | Amazon | +| Maintainer | GitHub ID | Affiliation | +|-------------------------|-------------------------------------------------------|-------------| +| Heemin Kim | [heemin32](https://github.com/heemin32) | Amazon | +| Jack Mazanec | [jmazanec15](https://github.com/jmazanec15) | Amazon | +| Junqiu Lei | [junqiu-lei](https://github.com/junqiu-lei) | Amazon | +| Martin Gaievski | [martin-gaievski](https://github.com/martin-gaievski) | Amazon | +| Naveen Tatikonda | [naveentatikonda](https://github.com/naveentatikonda) | Amazon | +| Navneet Verma | [navneet1v](https://github.com/navneet1v) | Amazon | +| Vamshi Vijay Nakkirtha | [vamshin](https://github.com/vamshin) | Amazon | +| Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | ## Emeritus From ddb27edba5b693a2249226a473ab470c1ce56fd9 Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Wed, 24 May 2023 15:50:46 -0500 Subject: [PATCH 69/77] Add Auto Release Workflow (#409) Signed-off-by: Naveen Tatikonda --- .github/workflows/auto-release.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/auto-release.yml diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 00000000..44985274 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,28 @@ +name: Releases + +on: + push: + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: GitHub App token + id: github_app_token + uses: tibdex/github-app-token@v1.5.0 + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + installation_id: 22958780 + - name: Get tag + id: tag + uses: dawidd6/action-get-tag@v1 + - uses: actions/checkout@v2 + - uses: ncipollo/release-action@v1 + with: + github_token: ${{ steps.github_app_token.outputs.token }} + bodyFile: release-notes/opensearch-dashboards-maps.release-notes-${{steps.tag.outputs.tag}}.md From d32f2c4f93b1f09864167fea233c2e30662e3b72 Mon Sep 17 00:00:00 2001 From: Martin Gaievski Date: Fri, 26 May 2023 09:14:05 -0700 Subject: [PATCH 70/77] Adding release notes for 2.8 (#417) Signed-off-by: Martin Gaievski --- CHANGELOG.md | 3 +-- .../opensearch-dashboards-maps.release-notes-2.8.0.0.md | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 release-notes/opensearch-dashboards-maps.release-notes-2.8.0.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c745ca..694195c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Maintenance ### Refactoring -## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.7...2.x) +## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.8...2.x) ### Features ### Enhancements @@ -24,6 +24,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Documentation ### Maintenance -Remove package-lock.json ([#400](https://github.com/opensearch-project/dashboards-maps/pull/400)) ### Refactoring diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.8.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.8.0.0.md new file mode 100644 index 00000000..a0f3e2cc --- /dev/null +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.8.0.0.md @@ -0,0 +1,5 @@ +## Version 2.8.0.0 Release Notes +Compatible with OpenSearch and OpenSearch Dashboards Version 2.8.0 + +### Maintenance +* Remove package-lock.json ([#400](https://github.com/opensearch-project/dashboards-maps/pull/400)) From 485ef367c58600b07e56dd09603c4a047d72ac21 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Fri, 26 May 2023 13:25:25 -0500 Subject: [PATCH 71/77] Clean console ReferenceError (#418) Signed-off-by: Junqiu Lei --- public/components/map_container/map_container.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 98c7bd6d..944fda8d 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -241,8 +241,10 @@ export const MapContainer = ({ (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId ); if (!findIndexPattern) { - // @ts-ignore - const newIndexPattern = await indexPatterns.get(selectedLayerConfig.source.indexPatternId); + const newIndexPattern = await services.data.indexPatterns.get( + // @ts-ignore + selectedLayerConfig.source.indexPatternId + ); const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; setLayersIndexPatterns(cloneLayersIndexPatterns); } From f31822af49f4efcceca6895b062801519eb5722f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:21:14 -0700 Subject: [PATCH 72/77] Bump semver from 7.3.8 to 7.5.3 (#431) Bumps [semver](https://github.com/npm/node-semver) from 7.3.8 to 7.5.3. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v7.3.8...v7.5.3) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 88cd112d..5566a462 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1336,9 +1336,9 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.3.2: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + version "7.5.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" + integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== dependencies: lru-cache "^6.0.0" From 6f56b2160ca15ae1e0249fbc9ac5deafd7477707 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 11 Jul 2023 11:42:24 -0500 Subject: [PATCH 73/77] Add 2.9.0 release note (#434) Signed-off-by: Junqiu Lei --- .../opensearch-dashboards-maps.release-notes-2.9.0.0.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md new file mode 100644 index 00000000..4d608846 --- /dev/null +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md @@ -0,0 +1,6 @@ +## Version 2.9.0.0 Release Notes +Compatible with OpenSearch and OpenSearch Dashboards Version 2.9.0 + +### Maintenance +Increment version to 2.9.0.0 ([#426](https://github.com/opensearch-project/dashboards-maps/pull/426)) +Bump semver from 7.3.8 to 7.5.3 ([#431](https://github.com/opensearch-project/dashboards-maps/pull/431)) From 57c225a87611aef1b46b037e09a823c4bf33aff0 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 17 Jul 2023 15:42:37 -0500 Subject: [PATCH 74/77] bump tough cookie (#439) Signed-off-by: Junqiu Lei --- package.json | 3 + ...h-dashboards-maps.release-notes-2.9.0.0.md | 1 + yarn.lock | 161 +++++++++++------- 3 files changed, 100 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index 79f24ac3..eb2f533c 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,8 @@ "cypress": "9.5.4", "cypress-multi-reporters": "^1.5.0", "prettier": "^2.1.1" + }, + "resolutions": { + "tough-cookie": "^4.1.3" } } diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md index 4d608846..3e5fdc90 100644 --- a/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md @@ -4,3 +4,4 @@ Compatible with OpenSearch and OpenSearch Dashboards Version 2.9.0 ### Maintenance Increment version to 2.9.0.0 ([#426](https://github.com/opensearch-project/dashboards-maps/pull/426)) Bump semver from 7.3.8 to 7.5.3 ([#431](https://github.com/opensearch-project/dashboards-maps/pull/431)) +Bump tough cookie to ^4.1.3 ([#439](https://github.com/opensearch-project/dashboards-maps/pull/439)) diff --git a/yarn.lock b/yarn.lock index 5566a462..74790124 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,9 +93,9 @@ integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== "@mapbox/mapbox-gl-draw@^1.4.0": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.4.1.tgz#96dcec4d3957150de854423ac15856fde43d1452" - integrity sha512-g6F49KZagF9269/IoF6vZJeail6qtoc5mVF3eVRikNT7UFnY0QASfe2y53mgE99s6GrHdpV+PZuFxaL71hkMhg== + version "1.4.2" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.4.2.tgz#3ec71d496f056313707c67e62728945563336a85" + integrity sha512-Zvl5YN+tIuYZlzPmuzOgkoJsZX6sHMQsnFI+O3ox8EwYkpLO2w0U2FvVhQuVnq1Yys12x6UnF+0IPoEdBy2UfA== dependencies: "@mapbox/geojson-area" "^0.2.2" "@mapbox/geojson-extent" "^1.0.1" @@ -139,7 +139,7 @@ "@opensearch-dashboards-test/opensearch-dashboards-test-library@git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main": version "1.0.4" - resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#7066f7f48ed049fdae700c2542e23b927d2accf3" + resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#03df3943ee7fbcc65fa6816626a671a23f674ac5" "@types/geojson@*", "@types/geojson@^7946.0.10": version "7946.0.10" @@ -147,16 +147,16 @@ integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== "@types/mapbox-gl@*": - version "2.7.10" - resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-2.7.10.tgz#a3a32a366bad8966c0a40b78209ed430ba018ce1" - integrity sha512-nMVEcu9bAcenvx6oPWubQSPevsekByjOfKjlkr+8P91vawtkxTnopDoXXq1Qn/f4cg3zt0Z2W9DVsVsKRNXJTw== + version "2.7.11" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-2.7.11.tgz#c9b9ed2ed24970aeef947609fdfcfcf826a3aa49" + integrity sha512-4vSwPSTQIawZTFRiTY2R74aZwAiM9gE6KGj871xdyAPpa+DmEObXxQQXqL2PsMH31/rP9nxJ2Kv0boeTVJMXVw== dependencies: "@types/geojson" "*" "@types/mapbox__mapbox-gl-draw@^1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@types/mapbox__mapbox-gl-draw/-/mapbox__mapbox-gl-draw-1.3.3.tgz#b96cce3e3bcd3ed2c4243a848725c66328737797" - integrity sha512-VJYdbxPxLNd0rUmgD3h7lm743A5J0uVj03Wd7P3Z+plLhUmPYIipAS8F3DhiP1x5dhIo9r27spFp3oASovd1sg== + version "1.4.0" + resolved "https://registry.yarnpkg.com/@types/mapbox__mapbox-gl-draw/-/mapbox__mapbox-gl-draw-1.4.0.tgz#2881b403a7353263fe15468eb449f1728b344a95" + integrity sha512-hcr3NA9Yt3fML6SXaeaQzC6x2ftESob0CrZXR4dUBrZ//SPCU02dGlws3aCXlwqDoQW0mLuK5/UExStJgp0BOg== dependencies: "@types/geojson" "*" "@types/mapbox-gl" "*" @@ -176,14 +176,14 @@ "@types/pbf" "*" "@types/node@*": - version "18.13.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" - integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== + version "20.4.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" + integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw== "@types/node@^14.14.31": - version "14.18.36" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835" - integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ== + version "14.18.53" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.53.tgz#42855629b8773535ab868238718745bf56c56219" + integrity sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A== "@types/pbf@*", "@types/pbf@^3.0.2": version "3.0.2" @@ -203,18 +203,18 @@ "@types/react" "*" "@types/react@*": - version "18.0.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" - integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== + version "18.2.15" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.15.tgz#14792b35df676c20ec3cf595b262f8c615a73066" + integrity sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" "@types/scheduler@*": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + version "0.16.3" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" + integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" @@ -227,9 +227,9 @@ integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== "@types/wellknown@^0.5.4": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@types/wellknown/-/wellknown-0.5.4.tgz#1f12a2ca9cda236673272688b7549a30f0c9e3bb" - integrity sha512-zcsf4oHeEcvpvWhDOB1+qNK04oFJtsmv7Otb1Vy8w8GAqQkgRrVkI/C76PdwtpiT216aM1SbhkKgKWzGLhHKng== + version "0.5.5" + resolved "https://registry.yarnpkg.com/@types/wellknown/-/wellknown-0.5.5.tgz#956a61e7da1a2d12ebe996c35d30446835c1c96d" + integrity sha512-dlG6+Tm5Wkt5YKuECshk1K0qaA6AQ3epSuRXNw9BCzu5gBnMBjg54A7sWd0mpY6gntX3DPjCVcI1lrzlMaxeKg== "@types/yauzl@^2.9.1": version "2.10.0" @@ -443,9 +443,9 @@ color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colorette@^2.0.16: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" @@ -503,9 +503,9 @@ csscolorparser@~1.0.3: integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== csstype@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== cypress-file-upload@^5.0.8: version "5.0.8" @@ -513,12 +513,12 @@ cypress-file-upload@^5.0.8: integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== cypress-multi-reporters@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.2.tgz#129dfeffa00d4deca3e9f58d84570b9962c28c2b" - integrity sha512-lvwGwHqZG5CwGxBJ6UJXWaxlWGkJgxBjP0h+IVLrrwRlJpT4coSwwt+UzMdeqEMrzT4IDfhbtmUNOiDleisOYA== + version "1.6.3" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.3.tgz#0f0da8db4caf8d7a21f94e5209148348416d7c71" + integrity sha512-klb9pf6oAF4WCLHotu9gdB8ukYBdeTzbEMuESKB3KT54HhrZj65vQxubAgrULV5H2NWqxHdUhlntPbKZChNvEw== dependencies: - debug "^4.1.1" - lodash "^4.17.15" + debug "^4.3.4" + lodash "^4.17.21" cypress@9.5.4: version "9.5.4" @@ -576,9 +576,9 @@ dashdash@^1.12.0: assert-plus "^1.0.0" dayjs@^1.10.4: - version "1.11.7" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" - integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + version "1.11.9" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" + integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== debug@^3.1.0: version "3.2.7" @@ -587,7 +587,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.1.1, debug@^4.3.2: +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -753,12 +753,13 @@ geojson@^0.5.0: integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== get-intrinsic@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== dependencies: function-bind "^1.1.1" has "^1.0.3" + has-proto "^1.0.1" has-symbols "^1.0.3" get-stream@^5.0.0, get-stream@^5.1.0: @@ -821,15 +822,20 @@ global-prefix@^3.0.0: which "^1.3.1" graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -1027,7 +1033,7 @@ lodash.once@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1213,9 +1219,9 @@ potpack@^1.0.2: integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ== prettier@^2.1.1: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== pretty-bytes@^5.6.0: version "5.6.0" @@ -1237,7 +1243,7 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== -psl@^1.1.28: +psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -1262,6 +1268,11 @@ qs@~6.10.3: dependencies: side-channel "^1.0.4" +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + quickselect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" @@ -1286,6 +1297,11 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-protobuf-schema@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" @@ -1319,9 +1335,9 @@ rw@~0.1.4: integrity sha512-vSj3D96kMcjNyqPcp65wBRIDImGSrUuMxngNNxvw8MQaO+aQ6llzRPH7XcJy5zrpb3wU++045+Uz/IDIM684iw== rxjs@^7.5.1: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" @@ -1336,9 +1352,9 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.3.2: - version "7.5.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" - integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -1470,13 +1486,15 @@ tmp@~0.2.1: dependencies: rimraf "^3.0.0" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +tough-cookie@^4.1.3, tough-cookie@~2.5.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== dependencies: - psl "^1.1.28" + psl "^1.1.33" punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" traverse@~0.6.6: version "0.6.7" @@ -1484,9 +1502,9 @@ traverse@~0.6.6: integrity sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg== tslib@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + version "2.6.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" + integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== tunnel-agent@^0.6.0: version "0.6.0" @@ -1510,6 +1528,11 @@ typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.7.tgz#799207136a37f3b3efb8c66c40010d032714dc73" integrity sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -1520,6 +1543,14 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From a4e92a02070339059a76e30da2889e80572973dd Mon Sep 17 00:00:00 2001 From: Vijayan Balasubramanian Date: Sat, 5 Aug 2023 17:31:25 -0700 Subject: [PATCH 75/77] Allow filtering geoshape fields around map extent (#452) From 2.9, geoshape supports geo bounding box. Hence, remove the constraint that disables geo bounding box for non geopoint fields. Signed-off-by: Vijayan Balasubramanian --- CHANGELOG.md | 3 ++- .../layer_config/documents_config/document_layer_source.tsx | 1 - public/model/layerRenderController.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 694195c7..3dc7609f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Maintenance ### Refactoring -## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.8...2.x) +## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.9...2.x) ### Features +* Allow filtering geo_shape fields around map extent ([#452](https://github.com/opensearch-project/dashboards-maps/pull/452)) ### Enhancements diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx index 80b204a5..260def0f 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -355,7 +355,6 @@ export const DocumentLayerSource = ({ { const geoField = mapLayer.source.geoFieldName; - const geoFieldType = mapLayer.source.geoFieldType; // geo bounding box query supports geo_point fields const mapBounds: GeoBounds = getBounds(maplibreRef.current!); const meta: FilterMeta = { alias: null, - disabled: !mapLayer.source.useGeoBoundingBoxFilter || geoFieldType !== 'geo_point', + disabled: !mapLayer.source.useGeoBoundingBoxFilter, negate: false, type: FILTERS.GEO_BOUNDING_BOX, }; From 5344f279fdfb8215808fba37c03bcd8843707951 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Thu, 17 Aug 2023 21:53:45 -0500 Subject: [PATCH 76/77] Support dark mode in maps-dashboards (#455) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + common/config.ts | 3 +- common/index.ts | 5 ++ public/application.tsx | 9 +- .../add_layer_panel/add_layer_panel.tsx | 5 +- public/components/app.tsx | 11 +-- .../layer_control_panel.tsx | 4 - .../map_container/map_container.scss | 27 ++++++ .../map_container/map_container.tsx | 11 +-- public/components/map_page/map_page.tsx | 19 +---- .../filter-by_shape.test.tsx.snap | 8 +- .../filter_toolbar.test.tsx.snap | 12 ++- .../spatial_filter/filter_by_shape.tsx | 5 +- .../spatial_filter/filter_toolbar.test.tsx | 44 +++++++--- .../toolbar/spatial_filter/filter_toolbar.tsx | 12 ++- .../spatial_filter/spatial_filter.scss | 7 +- public/embeddable/map_component.tsx | 6 +- public/embeddable/map_embeddable.tsx | 8 -- public/embeddable/map_embeddable_factory.tsx | 5 +- .../images/{polygon.svg => polygon-dark.svg} | 2 +- public/images/polygon-light.svg | 4 + public/model/OSMLayerFunctions.ts | 82 ++++++++++++------- public/model/layerRenderController.ts | 6 +- public/model/mapLayerType.ts | 6 -- public/plugin.tsx | 6 +- public/types.ts | 3 + public/utils/getIntialConfig.ts | 9 +- 27 files changed, 186 insertions(+), 134 deletions(-) rename public/images/{polygon.svg => polygon-dark.svg} (93%) create mode 100644 public/images/polygon-light.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc7609f..c97dd857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.9...2.x) ### Features * Allow filtering geo_shape fields around map extent ([#452](https://github.com/opensearch-project/dashboards-maps/pull/452)) +* Support dark mode in maps-dashboards([#455](https://github.com/opensearch-project/dashboards-maps/pull/455)) ### Enhancements diff --git a/common/config.ts b/common/config.ts index 5d70d609..c3bfb289 100644 --- a/common/config.ts +++ b/common/config.ts @@ -10,7 +10,8 @@ export const configSchema = schema.object({ defaultValue: 'https://tiles.maps.opensearch.org/data/v1.json', }), opensearchVectorTileStyleUrl: schema.string({ - defaultValue: 'https://tiles.maps.opensearch.org/styles/basic.json', + // TODO: Change this to the production URL once it is available + defaultValue: 'https://staging.tiles.maps.opensearch.org/styles/manifest.json', }), opensearchVectorTileGlyphsUrl: schema.string({ defaultValue: 'https://tiles.maps.opensearch.org/fonts/{fontstack}/{range}.pbf', diff --git a/common/index.ts b/common/index.ts index 9f7b5cc6..7965790e 100644 --- a/common/index.ts +++ b/common/index.ts @@ -193,3 +193,8 @@ export const DRAW_FILTER_RECTANGLE = 'Draw Rectangle'; export const DRAW_FILTER_SPATIAL_RELATIONS = ['intersects', 'disjoint', 'within']; export const PER_PAGE_REQUEST_NUMBER = 50; + +export const DEFAULT_VECTOR_TILE_STYLES = { + BASIC: 'basic', + DARK: 'dark', +}; diff --git a/public/application.tsx b/public/application.tsx index 024af68b..aab8c3ea 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -9,16 +9,11 @@ import { AppMountParameters } from '../../../src/core/public'; import { MapServices } from './types'; import { MapsDashboardsApp } from './components/app'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; -import { ConfigSchema } from '../common/config'; -export const renderApp = ( - { element }: AppMountParameters, - services: MapServices, - mapConfig: ConfigSchema -) => { +export const renderApp = ({ element }: AppMountParameters, services: MapServices) => { ReactDOM.render( - + , element ); diff --git a/public/components/add_layer_panel/add_layer_panel.tsx b/public/components/add_layer_panel/add_layer_panel.tsx index c1ebed8e..30940b00 100644 --- a/public/components/add_layer_panel/add_layer_panel.tsx +++ b/public/components/add_layer_panel/add_layer_panel.tsx @@ -31,7 +31,6 @@ import { MAX_LAYER_LIMIT, } from '../../../common'; import { getLayerConfigMap } from '../../utils/getIntialConfig'; -import { ConfigSchema } from '../../../common/config'; interface Props { setIsLayerConfigVisible: Function; @@ -40,7 +39,6 @@ interface Props { addLayer: Function; setIsNewLayer: Function; newLayerIndex: number; - mapConfig: ConfigSchema; layerCount: number; } @@ -51,14 +49,13 @@ export const AddLayerPanel = ({ addLayer, setIsNewLayer, newLayerIndex, - mapConfig, layerCount, }: Props) => { const [isAddNewLayerModalVisible, setIsAddNewLayerModalVisible] = useState(false); const [highlightItem, setHighlightItem] = useState(null); function onClickAddNewLayer(layerType: string) { - const initLayerConfig = getLayerConfigMap(mapConfig)[layerType]; + const initLayerConfig = getLayerConfigMap()[layerType]; initLayerConfig.name = NEW_MAP_LAYER_DEFAULT_PREFIX + ' ' + newLayerIndex; setSelectedLayerConfig(initLayerConfig); setIsAddNewLayerModalVisible(false); diff --git a/public/components/app.tsx b/public/components/app.tsx index a0122f46..ac725b24 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -11,12 +11,8 @@ import { MapPage } from './map_page'; import { APP_PATH } from '../../common'; import { useOpenSearchDashboards } from '../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../types'; -import { ConfigSchema } from '../../common/config'; -interface Props { - mapConfig: ConfigSchema; -} -export const MapsDashboardsApp = ({ mapConfig }: Props) => { +export const MapsDashboardsApp = () => { const { services: { appBasePath }, } = useOpenSearchDashboards(); @@ -25,10 +21,7 @@ export const MapsDashboardsApp = ({ mapConfig }: Props) => { - } - /> + } /> } /> diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index e4f0a65d..993fc0b2 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -33,7 +33,6 @@ import { LAYER_ICON_TYPE_MAP } from '../../../common'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { MapState } from '../../model/mapState'; -import { ConfigSchema } from '../../../common/config'; import { moveLayers, removeLayers } from '../../model/map/layer_operations'; import { DeleteLayerModal } from './delete_layer_modal'; import { HideLayer } from './hide_layer_button'; @@ -46,7 +45,6 @@ interface Props { setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; mapState: MapState; zoom: number; - mapConfig: ConfigSchema; isReadOnlyMode: boolean; selectedLayerConfig: MapLayerSpecification | undefined; setSelectedLayerConfig: (layerConfig: MapLayerSpecification | undefined) => void; @@ -59,7 +57,6 @@ export const LayerControlPanel = memo( setLayers, layers, zoom, - mapConfig, isReadOnlyMode, selectedLayerConfig, setSelectedLayerConfig, @@ -393,7 +390,6 @@ export const LayerControlPanel = memo( addLayer={addLayer} newLayerIndex={newLayerIndex()} setIsNewLayer={setIsNewLayer} - mapConfig={mapConfig} layerCount={layers.length} /> {isDeleteLayerModalVisible && ( diff --git a/public/components/map_container/map_container.scss b/public/components/map_container/map_container.scss index 8e3f8cfb..e5c5ae18 100644 --- a/public/components/map_container/map_container.scss +++ b/public/components/map_container/map_container.scss @@ -18,6 +18,33 @@ top: $euiSizeS; } +.maplibregl-ctrl-zoom-in { + background: lightOrDarkTheme($ouiColorEmptyShade, $ouiColorLightestShade) !important; + .maplibregl-ctrl-icon { + background-image: lightOrDarkTheme( + url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E"), + url("data:image/svg+xml,%0A%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")) + !important; + } +} + +.maplibregl-ctrl-zoom-out { + background: lightOrDarkTheme($euiColorEmptyShade, $ouiColorLightestShade) !important; + .maplibregl-ctrl-icon { + background-image: lightOrDarkTheme( + url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E"), + url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")) + !important; + } +} + +.maplibregl-ctrl-compass { + background: lightOrDarkTheme($euiColorEmptyShade, $ouiColorLightestShade) !important; + background-image: lightOrDarkTheme( + url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E"), + url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")) !important; +} + .layerControlPanel-container { z-index: 1; position: absolute; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 944fda8d..8fc7f120 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -24,7 +24,6 @@ import { import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { ResizeChecker } from '../../../../../src/plugins/opensearch_dashboards_utils/public'; import { MapServices } from '../../types'; -import { ConfigSchema } from '../../../common/config'; import { baseLayerTypeLookup } from '../../model/layersFunctions'; import { MapsFooter } from './maps_footer'; import { DisplayFeatures } from '../tooltip/display_features'; @@ -42,7 +41,6 @@ interface MapContainerProps { setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; maplibreRef: React.MutableRefObject; mapState: MapState; - mapConfig: ConfigSchema; isReadOnlyMode: boolean; dashboardProps?: DashboardProps; isUpdatingLayerRender: boolean; @@ -64,7 +62,6 @@ export const MapContainer = ({ setLayersIndexPatterns, maplibreRef, mapState, - mapConfig, isReadOnlyMode, dashboardProps, isUpdatingLayerRender, @@ -93,11 +90,12 @@ export const MapContainer = ({ useEffect(() => { if (!mapContainer.current) return; + const vectorTileFontURL = services.mapConfig.opensearchVectorTileGlyphsUrl; const mbStyle = { version: 8 as 8, sources: {}, layers: [], - glyphs: mapConfig.opensearchVectorTileGlyphsUrl, + glyphs: vectorTileFontURL, }; maplibreRef.current = new Maplibre({ @@ -188,7 +186,7 @@ export const MapContainer = ({ if (isUpdatingLayerRender || isReadOnlyMode) { if (selectedLayerConfig) { if (baseLayerTypeLookup[selectedLayerConfig.type]) { - handleBaseLayerRender(selectedLayerConfig, maplibreRef, onError); + handleBaseLayerRender(selectedLayerConfig, maplibreRef, services, onError); } else { updateIndexPatterns(); handleDataLayerRender( @@ -201,7 +199,7 @@ export const MapContainer = ({ setSelectedLayerConfig(undefined); } else { renderDataLayers(layers, mapState, services, maplibreRef, dashboardProps); - renderBaseLayers(layers, maplibreRef, onError); + renderBaseLayers(layers, maplibreRef, services, onError); // Because of async layer rendering, layers order is not guaranteed, so we need to order layers // after all layers are rendered. maplibreRef.current!.once('idle', orderLayersAfterRenderLoaded); @@ -269,7 +267,6 @@ export const MapContainer = ({ setLayersIndexPatterns={setLayersIndexPatterns} mapState={mapState} zoom={zoom} - mapConfig={mapConfig} isReadOnlyMode={isReadOnlyMode} selectedLayerConfig={selectedLayerConfig} setSelectedLayerConfig={setSelectedLayerConfig} diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index dafb0e21..8f3310fe 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -29,15 +29,10 @@ import { Query, } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; -import { ConfigSchema } from '../../../common/config'; import { GeoShapeFilterMeta, ShapeFilter } from '../../../../../src/plugins/data/common'; import { buildGeoShapeFilterMeta } from '../../model/geo/filter'; import { FilterBar } from '../filter_bar/filter_bar'; -interface MapPageProps { - mapConfig: ConfigSchema; -} - export interface DashboardProps { timeRange?: TimeRange; refreshConfig?: RefreshInterval; @@ -46,16 +41,11 @@ export interface DashboardProps { } interface MapComponentProps { - mapConfig: ConfigSchema; mapIdFromSavedObject: string; dashboardProps?: DashboardProps; } -export const MapComponent = ({ - mapIdFromSavedObject, - mapConfig, - dashboardProps, -}: MapComponentProps) => { +export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapComponentProps) => { const { services } = useOpenSearchDashboards(); const { savedObjects: { client: savedObjectsClient }, @@ -88,7 +78,7 @@ export const MapComponent = ({ setLayersIndexPatterns(savedIndexPatterns); }); } else { - const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap(mapConfig)[ + const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap()[ OPENSEARCH_MAP_LAYER.type ] as MapLayerSpecification; initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; @@ -154,7 +144,6 @@ export const MapComponent = ({ setLayersIndexPatterns={setLayersIndexPatterns} maplibreRef={maplibreRef} mapState={mapState} - mapConfig={mapConfig} isReadOnlyMode={isReadOnlyMode} dashboardProps={dashboardProps} isUpdatingLayerRender={isUpdatingLayerRender} @@ -165,7 +154,7 @@ export const MapComponent = ({ ); }; -export const MapPage = ({ mapConfig }: MapPageProps) => { +export const MapPage = () => { const { id: mapId } = useParams<{ id: string }>(); - return ; + return ; }; diff --git a/public/components/toolbar/spatial_filter/__snapshots__/filter-by_shape.test.tsx.snap b/public/components/toolbar/spatial_filter/__snapshots__/filter-by_shape.test.tsx.snap index f2623b54..e459954e 100644 --- a/public/components/toolbar/spatial_filter/__snapshots__/filter-by_shape.test.tsx.snap +++ b/public/components/toolbar/spatial_filter/__snapshots__/filter-by_shape.test.tsx.snap @@ -15,7 +15,7 @@ exports[`render polygon renders filter by polygon in middle of drawing 1`] = `