From 0647a71e7aefe710d0cdec380bd906c4e2bd9561 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 26 Feb 2024 11:52:40 -0800 Subject: [PATCH 1/6] Update data layer source name (#588) * Update data layer source name Signed-off-by: Junqiu Lei --- .github/workflows/cypress-workflow.yml | 2 +- .github/workflows/unit-tests-workflow.yml | 4 +-- CHANGELOG.md | 1 + DEVELOPER_GUIDE.md | 2 +- cypress/e2e/documentsLayer.cy.js | 4 +-- package.json | 2 +- .../document_layer_source.tsx | 4 +-- yarn.lock | 36 ++++++++----------- 8 files changed, 25 insertions(+), 30 deletions(-) diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index cd2bf864..3023daa2 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -106,7 +106,7 @@ jobs: - name: Bootstrap plugin run: | cd OpenSearch-Dashboards/plugins/dashboards-maps - yarn osd bootstrap + yarn osd bootstrap --single-version=loose - name: Run OpenSearch Dashboards server run: | diff --git a/.github/workflows/unit-tests-workflow.yml b/.github/workflows/unit-tests-workflow.yml index f28413f3..2f817b27 100644 --- a/.github/workflows/unit-tests-workflow.yml +++ b/.github/workflows/unit-tests-workflow.yml @@ -51,7 +51,7 @@ jobs: cd ./OpenSearch-Dashboards/ su `id -un 1000` -c "source $NVM_DIR/nvm.sh && nvm use && node -v && yarn -v && cd ./plugins/dashboards-maps && - whoami && yarn osd bootstrap && yarn run test:jest --coverage" + whoami && yarn osd bootstrap --single-version=loose && yarn run test:jest --coverage" - name: Uploads coverage uses: codecov/codecov-action@v1 @@ -104,7 +104,7 @@ jobs: - name: Bootstrap plugin run: | cd OpenSearch-Dashboards/plugins/dashboards-maps - yarn osd bootstrap + yarn osd bootstrap --single-version=loose - name: Run tests with coverage run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 77328e43..98436282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,5 +18,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Bug Fixes ### Infrastructure ### Documentation +* Update data layer source name [#588](https://github.com/opensearch-project/dashboards-maps/pull/588) ### Maintenance ### Refactoring diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index f17686db..d1d39606 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -44,7 +44,7 @@ Ultimately, your directory structure should look like this: From OpenSearch-Dashboards repo (root folder), the following commands start OpenSearch Dashboards and includes this plugin. ``` -yarn osd bootstrap +yarn osd bootstrap --single-version=loose yarn start --no-base-path ``` diff --git a/cypress/e2e/documentsLayer.cy.js b/cypress/e2e/documentsLayer.cy.js index 2355f468..dcb221d9 100644 --- a/cypress/e2e/documentsLayer.cy.js +++ b/cypress/e2e/documentsLayer.cy.js @@ -24,9 +24,9 @@ describe('Documents layer', () => { cy.contains('Create map').click(); cy.get("button[data-test-subj='addLayerButton']").click(); cy.contains('Documents').click(); - cy.contains('Select data source', { timeout: 60000 }).click({ force: true }); + cy.wait(5000).contains('Select index pattern', { timeout: 60000 }).click({ force: true }); cy.contains('opensearch_dashboards_sample_data_flights').click(); - cy.contains('Select data field', { timeout: 60000 }).click({ force: true }); + cy.wait(5000).contains('Select data field', { timeout: 60000 }).click({ force: true }); cy.contains('DestLocation').click(); cy.get('[data-test-subj="indexPatternSelect"]').should( 'contain', diff --git a/package.json b/package.json index 48a94a6d..3409aeb4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "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", + "@opensearch-dashboards-test/opensearch-dashboards-test-library": "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz", "@types/mapbox__mapbox-gl-draw": "^1.3.3", "@types/wellknown": "^0.5.4", "cypress-file-upload": "^5.0.8", 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 260def0f..69f9ab46 100644 --- a/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -264,7 +264,7 @@ export const DocumentLayerSource = ({ { diff --git a/yarn.lock b/yarn.lock index 48a03fd6..f2c5d089 100644 --- a/yarn.lock +++ b/yarn.lock @@ -137,9 +137,9 @@ resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== -"@opensearch-dashboards-test/opensearch-dashboards-test-library@git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main": - version "1.0.5" - resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#96bb1d8ef0f17e4ae319c10badb194b981e89e5d" +"@opensearch-dashboards-test/opensearch-dashboards-test-library@https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz": + version "1.0.6" + resolved "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz#f2f489832a75191e243c6d2b42d49047265d9ce3" "@types/geojson@*", "@types/geojson@^7946.0.10": version "7946.0.10" @@ -180,11 +180,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377" integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA== -"@types/node@^16.18.39": - version "16.18.46" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.46.tgz#9f2102d0ba74a318fcbe170cbff5463f119eab59" - integrity sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg== - "@types/pbf@*", "@types/pbf@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" @@ -195,10 +190,10 @@ 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== +"@types/react-test-renderer@^18.0.7": + version "18.0.7" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.7.tgz#2cfe657adb3688cdf543995eceb2e062b5a68728" + integrity sha512-1+ANPOWc6rB3IkSnElhjv6VLlKg2dSv/OWClUyZimbLsQyBn8Js9Vtdsi3UICJ2rIQ3k2la06dkB+C92QfhKmg== dependencies: "@types/react" "*" @@ -357,7 +352,7 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -buffer@^5.6.0: +buffer@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -520,20 +515,19 @@ cypress-multi-reporters@^1.5.0: debug "^4.3.4" lodash "^4.17.21" -cypress@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.1.0.tgz#18f268e66662cd91b1766db18bd1f63a66592205" - integrity sha512-LUKxCYlB973QBFls1Up4FAE9QIYobT+2I8NvvAwMfQS2YwsWbr6yx7y9hmsk97iqbHkKwZW3MRjoK1RToBFVdQ== +cypress@^13.6.3: + version "13.6.6" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.6.tgz#5133f231ed1c6e57dc8dcbf60aade220bcd6884b" + integrity sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A== dependencies: "@cypress/request" "^3.0.0" "@cypress/xvfb" "^1.2.4" - "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" blob-util "^2.0.2" bluebird "^3.7.2" - buffer "^5.6.0" + buffer "^5.7.1" cachedir "^2.3.0" chalk "^4.1.0" check-more-types "^2.24.0" @@ -551,7 +545,7 @@ cypress@^13.1.0: figures "^3.2.0" fs-extra "^9.1.0" getos "^3.2.1" - is-ci "^3.0.0" + is-ci "^3.0.1" is-installed-globally "~0.4.0" lazy-ass "^1.6.0" listr2 "^3.8.3" @@ -907,7 +901,7 @@ install@^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: +is-ci@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== From 4f413a7949d4fb0cccf6e138b0953409ef304a80 Mon Sep 17 00:00:00 2001 From: Varun Jain Date: Tue, 19 Mar 2024 06:14:56 +0530 Subject: [PATCH 2/6] Add release notes for 2.13.0 release (#597) * Add release notes for 2.13.0 release Signed-off-by: Varun Jain * Add release notes for 2.13.0 release Signed-off-by: Varun Jain --------- Signed-off-by: Varun Jain --- CHANGELOG.md | 3 +-- .../opensearch-dashboards-maps.release-notes-2.13.0.0.md | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 release-notes/opensearch-dashboards-maps.release-notes-2.13.0.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 98436282..0186244d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,11 @@ 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.12...2.x) +## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.13...2.x) ### Features ### Enhancements ### Bug Fixes ### Infrastructure ### Documentation -* Update data layer source name [#588](https://github.com/opensearch-project/dashboards-maps/pull/588) ### Maintenance ### Refactoring diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.13.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.13.0.0.md new file mode 100644 index 00000000..6b9123b7 --- /dev/null +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.13.0.0.md @@ -0,0 +1,5 @@ +## Version 2.13.0.0 Release Notes +Compatible with OpenSearch and OpenSearch Dashboards Version 2.13.0 + +### Documentation +* Update data layer source name [#588](https://github.com/opensearch-project/dashboards-maps/pull/588) From 4d21bf0809ba5d7b2802fcb7162dc169f2664530 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Tue, 2 Apr 2024 10:23:25 -0700 Subject: [PATCH 3/6] Fix zoom level type error in custom layer (#605) * Fix zoom level type error in custom layer Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + .../layer_basic_settings.test.tsx | 113 +++++++++++++ .../layer_config/layer_basic_settings.tsx | 14 +- public/model/customLayerFunctions.test.ts | 154 ++++++++++++++++++ public/model/customLayerFunctions.ts | 11 +- 5 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 public/components/layer_config/layer_basic_settings.test.tsx create mode 100644 public/model/customLayerFunctions.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0186244d..287942cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Features ### Enhancements ### Bug Fixes +* Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) ### Infrastructure ### Documentation ### Maintenance diff --git a/public/components/layer_config/layer_basic_settings.test.tsx b/public/components/layer_config/layer_basic_settings.test.tsx new file mode 100644 index 00000000..05f18170 --- /dev/null +++ b/public/components/layer_config/layer_basic_settings.test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { LayerBasicSettings } from './layer_basic_settings'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { DASHBOARDS_MAPS_LAYER_TYPE, MAX_LAYER_NAME_LIMIT } from '../../../common'; +import { shallow } from 'enzyme'; +import { EuiDualRange, EuiFieldText, EuiFormRow, EuiRange, EuiTextArea } from '@elastic/eui'; + +describe('LayerBasicSettings', () => { + let wrapper: any; + const mockLayerConfig: MapLayerSpecification = { + name: 'Test Layer', + id: 'test-layer', + type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + description: 'Some description', + zoomRange: [1, 20], + visibility: 'visible', + opacity: 80, + }; + + const mockProps = { + selectedLayerConfig: mockLayerConfig, + setSelectedLayerConfig: jest.fn(), + setIsUpdateDisabled: jest.fn(), + isLayerExists: jest.fn(), + }; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('should render with correct props', () => { + expect(wrapper.find(EuiFieldText).prop('value')).toBe(mockLayerConfig.name); + expect(wrapper.find(EuiTextArea).prop('value')).toBe(mockLayerConfig.description); + expect(wrapper.find(EuiDualRange).prop('value')).toEqual(mockLayerConfig.zoomRange); + expect(wrapper.find(EuiRange).prop('value')).toEqual(mockLayerConfig.opacity); + }); + + it('should call setSelectedLayerConfig with updated zoom range on zoom level change', () => { + const newZoomRange = [2, 15]; + wrapper.find(EuiDualRange).simulate('change', newZoomRange); + + expect(mockProps.setSelectedLayerConfig).toHaveBeenCalledWith({ + ...mockLayerConfig, + zoomRange: newZoomRange, + }); + }); + + it('should call setSelectedLayerConfig with updated opacity on opacity change', () => { + const newOpacity = 50; + wrapper.find(EuiRange).simulate('change', { target: { value: newOpacity.toString() } }); + + expect(mockProps.setSelectedLayerConfig).toHaveBeenCalledWith({ + ...mockLayerConfig, + opacity: newOpacity, + }); + }); + + it('should call setSelectedLayerConfig with updated name on name change', () => { + const newName = 'Updated Test Layer'; + wrapper.find(EuiFieldText).simulate('change', { target: { value: newName } }); + + expect(mockProps.setSelectedLayerConfig).toHaveBeenCalledWith({ + ...mockLayerConfig, + name: newName, + }); + }); + + it('should call setSelectedLayerConfig with updated description on description change', () => { + const newDescription = 'Updated description'; + wrapper.find(EuiTextArea).simulate('change', { target: { value: newDescription } }); + + expect(mockProps.setSelectedLayerConfig).toHaveBeenCalledWith({ + ...mockLayerConfig, + description: newDescription, + }); + }); + + it('should display an error for an empty name', () => { + mockProps.isLayerExists.mockReturnValue(false); // Ensure this returns false for this test + const emptyName = ''; + wrapper.find(EuiFieldText).simulate('change', { target: { value: emptyName } }); + + wrapper.update(); + + expect(wrapper.find(EuiFormRow).first().prop('error')).toContain('Name cannot be empty'); + }); + + it('should display an error for a name exceeding the maximum length', () => { + const longName = 'a'.repeat(MAX_LAYER_NAME_LIMIT + 1); // Create a name longer than MAX_LAYER_NAME_LIMIT + wrapper.find(EuiFieldText).simulate('change', { target: { value: longName } }); + + wrapper.update(); + + expect(wrapper.find(EuiFormRow).first().prop('error')).toContain( + `Name should be less than ${MAX_LAYER_NAME_LIMIT} characters` + ); + }); + + it('should display an error for a name that already exists', () => { + mockProps.isLayerExists.mockReturnValue(true); // Simulate that the name already exists + const existingName = 'Existing Layer Name'; + wrapper.find(EuiFieldText).simulate('change', { target: { value: existingName } }); + + wrapper.update(); + + expect(wrapper.find(EuiFormRow).first().prop('error')).toContain('Name already exists'); + }); +}); diff --git a/public/components/layer_config/layer_basic_settings.tsx b/public/components/layer_config/layer_basic_settings.tsx index 2dbbffc6..1ea5b5c3 100644 --- a/public/components/layer_config/layer_basic_settings.tsx +++ b/public/components/layer_config/layer_basic_settings.tsx @@ -25,6 +25,7 @@ import { MAP_LAYER_DEFAULT_OPACITY_STEP, MAX_LAYER_NAME_LIMIT, } from '../../../common'; +import { ValueMember } from '../../../../../src/plugins/opensearch_dashboards_react/public/validated_range/validated_dual_range'; interface Props { selectedLayerConfig: MapLayerSpecification; @@ -41,6 +42,7 @@ export const LayerBasicSettings = ({ }: Props) => { const [invalid, setInvalid] = useState(selectedLayerConfig.name.length === 0); const [errors, setErrors] = useState([]); + const [zoomLevel, setZoomLevel] = useState<[ValueMember, ValueMember]>(selectedLayerConfig.zoomRange as [ValueMember, ValueMember]); const validateName = (name: string) => { if (name?.length === 0) { @@ -73,8 +75,14 @@ export const LayerBasicSettings = ({ const newLayerConfig = { ...selectedLayerConfig, [key]: value }; setSelectedLayerConfig(newLayerConfig); }; - const onZoomChange = (value: number[]) => { - commonUpdate('zoomRange', value); + + const onZoomChange = (value: [ValueMember, ValueMember]) => { + // if value is not empty string, then update the zoom level to layer config + if (value[0] !== '' && value[1] !== '') { + // change value to number type + commonUpdate('zoomRange', value.map((zoomLevel) => Number(zoomLevel))); + } + setZoomLevel(value); }; const onOpacityChange = (e: any) => { @@ -121,7 +129,7 @@ export const LayerBasicSettings = ({ ({ + Map: jest.fn(() => ({ + on: jest.fn(), + off: jest.fn(), + getStyle: jest.fn(() => mockStyle), + setPaintProperty: jest.fn(), + setLayerZoomRange: jest.fn(), + addSource: jest.fn(), + addLayer: jest.fn(), + // @ts-ignore + getSource: jest.fn().mockImplementation((id) => mockStyle.sources[id]), + getLayer: jest + .fn() + .mockImplementation((id) => mockStyle.layers.find((layer) => layer.id === id)), + triggerRepaint: jest.fn(), + _controls: [], + style: { + sourceCaches: {}, + }, + })), +})); + +describe('CustomLayerFunctions', () => { + let map: MapLibre; + let maplibreRef: MaplibreRef; + + beforeEach(() => { + map = new MapLibre({ + container: document.createElement('div'), + style: 'mock-style-url', + }); + + // Initialize sourceCaches with a mock function for each layer that might be accessed + // @ts-ignore + map.style.sourceCaches['existing-layer'] = { + clearTiles: jest.fn(), + update: jest.fn(), + }; + + maplibreRef = { + current: map, + }; + }); + + it('should add a new layer if it does not exist', () => { + const newLayerConfig: CustomLayerSpecification = { + id: 'new-layer', + source: { + // @ts-ignore + type: DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE.TMS, + tiles: ['https://newtiles.example.com/{z}/{x}/{y}.png'], + attribution: 'Test Attribution', + }, + opacity: 80, + zoomRange: [0, 22], + visibility: 'visible', + }; + + CustomLayerFunctions.render(maplibreRef, newLayerConfig); + + expect(map.addSource).toHaveBeenCalledWith(newLayerConfig.id, expect.any(Object)); + expect(map.addLayer).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('should update an existing layer', () => { + const updatedLayerConfig: CustomLayerSpecification = { + id: 'existing-layer', + source: { + // @ts-ignore + type: DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE.TMS, + tiles: ['https://updatedtiles.example.com/{z}/{x}/{y}.png'], + attribution: 'Updated Test Attribution', + }, + opacity: 50, + zoomRange: [0, 15], + visibility: 'visible', + }; + + CustomLayerFunctions.render(maplibreRef, updatedLayerConfig); + + expect(map.setPaintProperty).toHaveBeenCalledWith( + updatedLayerConfig.id, + 'raster-opacity', + updatedLayerConfig.opacity / 100 + ); + expect(map.setLayerZoomRange).toHaveBeenCalledWith( + updatedLayerConfig.id, + updatedLayerConfig.zoomRange[0], + updatedLayerConfig.zoomRange[1] + ); + }); + + it('should convert zoomRange to a numeric array', () => { + const layerConfig = { + id: 'test-layer', + // Assuming zoomRange might be provided as strings from old versions + zoomRange: ['0', '10'], + }; + + // @ts-ignore + const result = applyZoomRangeToLayer(layerConfig); + + // Expected result should be a numeric array + const expectedResult = [0, 10]; + + // Verify the result matches the expected numeric array + expect(result).toEqual(expectedResult); + }); + + it('should handle mixed types in zoomRange', () => { + // Define layerConfig with zoomRange as a mix of numbers and strings + const layerConfig = { + id: 'mixed-type-layer', + zoomRange: [1, '15'], + }; + + // @ts-ignore + const result = applyZoomRangeToLayer(layerConfig); + + const expectedResult = [1, 15]; + + expect(result).toEqual(expectedResult); + }); +}); diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts index 51fc2a2d..3685dc3a 100644 --- a/public/model/customLayerFunctions.ts +++ b/public/model/customLayerFunctions.ts @@ -51,6 +51,8 @@ const addNewLayer = (layerConfig: CustomLayerSpecification, maplibreRef: Maplibr tileSize: 256, attribution: layerSource?.attribution, }); + // Convert zoomRange to number[] to avoid type error for backport versions + const zoomRange: number[] = applyZoomRangeToLayer(layerConfig); maplibreInstance.addLayer({ id: layerConfig.id, type: 'raster', @@ -61,8 +63,8 @@ const addNewLayer = (layerConfig: CustomLayerSpecification, maplibreRef: Maplibr layout: { visibility: layerConfig.visibility === 'visible' ? 'visible' : 'none', }, - minzoom: layerConfig.zoomRange[0], - maxzoom: layerConfig.zoomRange[1], + minzoom: zoomRange[0], + maxzoom: zoomRange[1], }); } }; @@ -79,6 +81,11 @@ const getCustomMapURL = (layerConfig: CustomLayerSpecification) => { } }; +export const applyZoomRangeToLayer = (layerConfig: CustomLayerSpecification) => { + // Convert zoomRange to number[] to avoid type error for backport versions + return layerConfig.zoomRange.map((zoom) => Number(zoom)); +}; + export const CustomLayerFunctions = { render: (maplibreRef: MaplibreRef, layerConfig: CustomLayerSpecification) => { return hasLayer(maplibreRef.current!, layerConfig.id) From f325212eb75c491b1b9e715403732d810197c869 Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Fri, 19 Apr 2024 13:35:08 -0700 Subject: [PATCH 4/6] Support multi data source display in Maps app (#611) * Support multi data source display in Maps app Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + opensearch_dashboards.json | 2 +- public/application.tsx | 6 +- public/components/app.tsx | 6 +- .../layer_control_panel.tsx | 33 ++++++++- .../map_container/map_container.tsx | 14 ++++ public/components/map_page/map_page.tsx | 70 +++++++++++++------ .../components/map_top_nav/top_nav_menu.tsx | 60 ++++++++++------ public/components/maps_list/maps_list.tsx | 28 ++++++-- public/plugin.tsx | 10 +-- public/types.ts | 5 ++ yarn.lock | 28 ++++---- 12 files changed, 194 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 287942cf..d9a012a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,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.13...2.x) ### Features +Support multi data source display in Maps app([#611](https://github.com/opensearch-project/dashboards-maps/pull/611)) ### Enhancements ### Bug Fixes * Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index e8abd481..1c6b783e 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -5,5 +5,5 @@ "server": true, "ui": true, "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable", "visualizations"], - "optionalPlugins": ["home"] + "optionalPlugins": ["home", "dataSource", "dataSourceManagement"] } diff --git a/public/application.tsx b/public/application.tsx index aab8c3ea..9fec4afe 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -10,7 +10,11 @@ import { MapServices } from './types'; import { MapsDashboardsApp } from './components/app'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; -export const renderApp = ({ element }: AppMountParameters, services: MapServices) => { +export const renderApp = ( + { element }: AppMountParameters, + services: MapServices, + dataSourceManagementEnabled: boolean +) => { ReactDOM.render( diff --git a/public/components/app.tsx b/public/components/app.tsx index ac725b24..f8d1b4a1 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -22,7 +22,11 @@ export const MapsDashboardsApp = () => { } /> - } /> + } + /> diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 993fc0b2..44be9bd9 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -29,7 +29,7 @@ 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 } from '../../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, LAYER_ICON_TYPE_MAP } from '../../../common'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { MapState } from '../../model/mapState'; @@ -49,6 +49,7 @@ interface Props { selectedLayerConfig: MapLayerSpecification | undefined; setSelectedLayerConfig: (layerConfig: MapLayerSpecification | undefined) => void; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; + setDataSourceRefIds: (dataSourceRefIds: string[]) => void; } export const LayerControlPanel = memo( @@ -56,11 +57,14 @@ export const LayerControlPanel = memo( maplibreRef, setLayers, layers, + layersIndexPatterns, + setLayersIndexPatterns, zoom, isReadOnlyMode, selectedLayerConfig, setSelectedLayerConfig, setIsUpdatingLayerRender, + setDataSourceRefIds, }: Props) => { const { services } = useOpenSearchDashboards(); @@ -205,12 +209,39 @@ export const LayerControlPanel = memo( setIsDeleteLayerModalVisible(true); }; + const removeDataLayerDataSource = (layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const indexPatternId = layer.source.indexPatternId; + const indexPattern = layersIndexPatterns.find((idp) => idp.id === indexPatternId); + if (indexPattern) { + const indexPatternClone = [...layersIndexPatterns]; + const index = indexPatternClone.findIndex((idp) => idp.id === indexPatternId); + if (index > -1) { + indexPatternClone.splice(index, 1); + setLayersIndexPatterns(indexPatternClone); + } + // remove duplicate dataSourceRefIds + const updatedDataSourceRefIds : string[] = []; + indexPatternClone.forEach((ip) => { + if (ip.dataSourceRef && !updatedDataSourceRefIds.includes(ip.dataSourceRef.id)) { + updatedDataSourceRefIds.push(ip.dataSourceRef.id); + } else if (!ip.dataSourceRef && !updatedDataSourceRefIds.includes('')) { + updatedDataSourceRefIds.push(''); + } + }); + + setDataSourceRefIds(updatedDataSourceRefIds); + } + } + }; + const onDeleteLayerConfirm = () => { if (selectedDeleteLayer) { removeLayers(maplibreRef.current!, selectedDeleteLayer.id, true); removeLayer(selectedDeleteLayer.id); setIsDeleteLayerModalVisible(false); setSelectedDeleteLayer(undefined); + removeDataLayerDataSource(selectedDeleteLayer); } }; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 8fc7f120..e819ddfa 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -46,6 +46,8 @@ interface MapContainerProps { isUpdatingLayerRender: boolean; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; + dataSourceRefIds: string[]; + setDataSourceRefIds: (refIds: string[]) => void; } export class MapsServiceError extends Error { @@ -67,6 +69,7 @@ export const MapContainer = ({ isUpdatingLayerRender, setIsUpdatingLayerRender, addSpatialFilter, + setDataSourceRefIds, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); @@ -245,6 +248,16 @@ export const MapContainer = ({ ); const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; setLayersIndexPatterns(cloneLayersIndexPatterns); + const updatedDataSourceRefIds: string[] = []; + cloneLayersIndexPatterns.forEach((ip) => { + if (ip.dataSourceRef && !updatedDataSourceRefIds.includes(ip.dataSourceRef.id)) { + updatedDataSourceRefIds.push(ip.dataSourceRef.id); + } else if (!ip.dataSourceRef && !updatedDataSourceRefIds.includes('')) { + // If index pattern of the layer doesn't have reference to a data source, it is using local cluster + updatedDataSourceRefIds.push(''); + } + }); + setDataSourceRefIds(updatedDataSourceRefIds); } }; @@ -271,6 +284,7 @@ export const MapContainer = ({ selectedLayerConfig={selectedLayerConfig} setSelectedLayerConfig={setSelectedLayerConfig} setIsUpdatingLayerRender={setIsUpdatingLayerRender} + setDataSourceRefIds={setDataSourceRefIds} /> )} {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && maplibreRef.current && ( diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index 8f3310fe..997c8806 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -51,13 +51,16 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon savedObjects: { client: savedObjectsClient }, } = services; const [layers, setLayers] = useState([]); - const [savedMapObject, setSavedMapObject] = - useState | null>(); + const [savedMapObject, setSavedMapObject] = useState | null>(); const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); const maplibreRef = useRef(null); const [mapState, setMapState] = useState(getInitialMapState()); const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(true); const isReadOnlyMode = !!dashboardProps; + const [dataSourceRefIds, setDataSourceRefIds] = useState([]); + const [dataLoadReady, setDataLoadReady] = useState(false); useEffect(() => { if (mapIdFromSavedObject) { @@ -68,14 +71,31 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon setMapState(savedMapState); setLayers(layerList); const savedIndexPatterns: IndexPattern[] = []; - layerList.forEach(async (layer: MapLayerSpecification) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - const indexPatternId = layer.source.indexPatternId; - const indexPattern = await services.data.indexPatterns.get(indexPatternId); - savedIndexPatterns.push(indexPattern); - } - }); - setLayersIndexPatterns(savedIndexPatterns); + const remoteDataSourceIds: string[] = []; + + const fetchDataLayer = async () => { + const requests = layerList + .filter((layer) => layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) + .map((layer) => services.data.indexPatterns.get(layer.source.indexPatternId)); + + const resp = await Promise.all(requests); + resp.forEach((response: IndexPattern) => { + savedIndexPatterns.push(response); + if (response.dataSourceRef && !dataSourceRefIds.includes(response.dataSourceRef.id)) { + remoteDataSourceIds.push(response.dataSourceRef.id); + } else if (!response.dataSourceRef && !remoteDataSourceIds.includes('')) { + // If index pattern of the layer doesn't have reference to a data source, it is using local cluster + remoteDataSourceIds.push(''); + } + }); + + setLayers(layerList); + setLayersIndexPatterns(savedIndexPatterns); + setDataSourceRefIds(remoteDataSourceIds); + setDataLoadReady(true); + }; + + fetchDataLayer(); }); } else { const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap()[ @@ -83,6 +103,7 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon ] as MapLayerSpecification; initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; setLayers([initialDefaultLayer]); + setDataLoadReady(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -114,18 +135,21 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon return (
- {isReadOnlyMode ? null : ( - - )} + {isReadOnlyMode + ? null + : dataLoadReady && ( + + )} {!isReadOnlyMode && !!mapState.spatialMetaFilters?.length && (
@@ -149,6 +173,8 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon isUpdatingLayerRender={isUpdatingLayerRender} setIsUpdatingLayerRender={setIsUpdatingLayerRender} addSpatialFilter={addSpatialFilter} + dataSourceRefIds={dataSourceRefIds} + setDataSourceRefIds={setDataSourceRefIds} />
); diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index d6519d94..3fd13f20 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -26,6 +26,7 @@ interface MapTopNavMenuProps { setMapState: (mapState: MapState) => void; originatingApp?: string; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; + dataSourceRefIds: string[]; } export const MapTopNavMenu = ({ @@ -37,6 +38,7 @@ export const MapTopNavMenu = ({ mapState, setMapState, setIsUpdatingLayerRender, + dataSourceRefIds, }: MapTopNavMenuProps) => { const { services } = useOpenSearchDashboards(); const { @@ -48,6 +50,9 @@ export const MapTopNavMenu = ({ application: { navigateToApp }, embeddable, scopedHistory, + dataSourceManagement, + savedObjects: { client: savedObjectsClient }, + notifications, } = services; const [title, setTitle] = useState(''); @@ -132,27 +137,42 @@ export const MapTopNavMenu = ({ }); }, [services, mapIdFromUrl, layers, title, description, mapState, originatingApp]); + const dataSourceManagementEnabled: boolean = !!dataSourceManagement; + return ( // @ts-ignore - + <> + + ); }; diff --git a/public/components/maps_list/maps_list.tsx b/public/components/maps_list/maps_list.tsx index 78ce10fd..7cc57d06 100644 --- a/public/components/maps_list/maps_list.tsx +++ b/public/components/maps_list/maps_list.tsx @@ -22,14 +22,17 @@ import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attri import { MapServices } from '../../types'; import { getMapsLandingBreadcrumbs } from '../../utils/breadcrumbs'; import { APP_PATH, MAPS_APP_ID } from '../../../common'; +import { DataSourceAggregatedViewConfig } from '../../../../../src/plugins/data_source_management/public'; export const MapsList = () => { const { services: { - notifications: { toasts }, + notifications, savedObjects: { client: savedObjectsClient }, application: { navigateToApp }, chrome: { docTitle, setBreadcrumbs }, + dataSourceManagement, + setActionMenu, }, } = useOpenSearchDashboards(); @@ -92,14 +95,14 @@ export const MapsList = () => { await Promise.all( selectedItems.map((item: any) => savedObjectsClient.delete(item.type, item.id)) ).catch((error) => { - toasts.addError(error, { + notifications.toasts.addError(error, { title: i18n.translate('map.mapListingDeleteErrorTitle', { defaultMessage: 'Error deleting map', }), }); }); }, - [savedObjectsClient, toasts] + [savedObjectsClient, notifications.toasts] ); const noMapItem = ( @@ -113,14 +116,29 @@ export const MapsList = () => { ]} /> ); + const dataSourceManagementEnabled: boolean = !!dataSourceManagement; - // Render the map list DOM. return ( <> + {dataSourceManagementEnabled && (() => { + const DataSourcesMenu = dataSourceManagement.ui.getDataSourceMenu(); + return ( + + ); + })()} { tableListTitle={i18n.translate('maps.listing.table.listTitle', { defaultMessage: 'Maps', })} - toastNotifications={toasts} + toastNotifications={notifications.toasts} /> diff --git a/public/plugin.tsx b/public/plugin.tsx index a08c698e..66e2b085 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -38,19 +38,19 @@ 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, embeddable, visualizations }: AppPluginSetupDependencies + { regionMap, embeddable, visualizations, dataSourceManagement }: AppPluginSetupDependencies ): CustomImportMapPluginSetup { const mapConfig: ConfigSchema = { ...this._initializerContext.config.get(), }; + const dataSourceManagentEnabled: boolean = !!dataSourceManagement; // Register an application into the side navigation menu core.application.register({ id: MAPS_APP_ID, @@ -91,10 +91,12 @@ export class CustomImportMapPlugin scopedHistory: params.history, uiSettings: coreStart.uiSettings, mapConfig, + dataSourceManagement, + setActionMenu: params.setHeaderActionMenu, }; params.element.classList.add('mapAppContainer'); // Render the application - return renderApp(params, services); + return renderApp(params, services, dataSourceManagentEnabled); }, }); diff --git a/public/types.ts b/public/types.ts index 8d57c75a..8376ae84 100644 --- a/public/types.ts +++ b/public/types.ts @@ -9,6 +9,7 @@ import { SavedObjectsClient, ToastsStart, ScopedHistory, + MountPoint, } from '../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; @@ -16,6 +17,7 @@ import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; import { ConfigSchema } from '../common/config'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; @@ -41,6 +43,8 @@ export interface MapServices extends CoreStart { chrome: CoreStart['chrome']; uiSettings: CoreStart['uiSettings']; mapConfig: ConfigSchema; + dataSourceManagement: DataSourceManagementPluginSetup; + setActionMenu: (menuMount: MountPoint | undefined) => void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -54,4 +58,5 @@ export interface AppPluginSetupDependencies { embeddable: EmbeddableSetup; visualizations: VisualizationsSetup; data: DataPublicPluginSetup; + dataSourceManagement: DataSourceManagementPluginSetup; } diff --git a/yarn.lock b/yarn.lock index f2c5d089..b811fbcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,9 +8,9 @@ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@cypress/request@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.0.tgz#7f58dfda087615ed4e6aab1b25fffe7630d6dd85" - integrity sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" + integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== 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.10.3" + qs "6.10.4" safe-buffer "^5.1.2" tough-cookie "^4.1.3" tunnel-agent "^0.6.0" @@ -516,9 +516,9 @@ cypress-multi-reporters@^1.5.0: lodash "^4.17.21" cypress@^13.6.3: - version "13.6.6" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.6.tgz#5133f231ed1c6e57dc8dcbf60aade220bcd6884b" - integrity sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A== + version "13.7.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.3.tgz#3e7dcd32e007676a6c8e972293c50d6ef329d991" + integrity sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag== dependencies: "@cypress/request" "^3.0.0" "@cypress/xvfb" "^1.2.4" @@ -1262,10 +1262,10 @@ punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@~6.10.3: - version "6.10.5" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" - integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== +qs@6.10.4: + version "6.10.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7" + integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== dependencies: side-channel "^1.0.4" @@ -1353,9 +1353,9 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.5.3: - 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== + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" From 61d970359c8ae0538f9ddd113ab709407d78f58c Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Fri, 26 Apr 2024 12:27:00 -0700 Subject: [PATCH 5/6] Support multi data source in Region map (#614) --- CHANGELOG.md | 1 + .../components/vector_upload_options.test.tsx | 12 ++++++++- public/components/vector_upload_options.tsx | 6 +++-- public/services.ts | 27 ++++++++++++------- server/plugin.ts | 10 ++++--- server/routes/geospatial.ts | 1 + server/routes/opensearch.ts | 1 + server/services/geospatial_service.js | 20 +++++++++++--- server/services/opensearch_service.js | 23 +++++++++++----- server/types.ts | 2 ++ 10 files changed, 77 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a012a8..d68f5dbd 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.13...2.x) ### Features Support multi data source display in Maps app([#611](https://github.com/opensearch-project/dashboards-maps/pull/611)) +Support multi data source in Region map ([#614](https://github.com/opensearch-project/dashboards-maps/pull/614)) ### Enhancements ### Bug Fixes * Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) diff --git a/public/components/vector_upload_options.test.tsx b/public/components/vector_upload_options.test.tsx index abd51b72..4b0ccecb 100644 --- a/public/components/vector_upload_options.test.tsx +++ b/public/components/vector_upload_options.test.tsx @@ -27,7 +27,17 @@ jest.mock('../../../../src/plugins/opensearch_dashboards_react/public', () => ({ })); describe('vector_upload_options', () => { - const props = {}; + const props = { + vis: { + data: { + indexPattern: { + dataSourceRef: { + id: 'mock-data-source-id', + }, + }, + }, + }, + }; const getIndexResponseWhenIndexIsNotPresent = { ok: false, diff --git a/public/components/vector_upload_options.tsx b/public/components/vector_upload_options.tsx index 89ce3d59..4eb903f3 100644 --- a/public/components/vector_upload_options.tsx +++ b/public/components/vector_upload_options.tsx @@ -78,6 +78,8 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { return document.getElementsByName(elementName)[0]; }; + const dataSourceRefId = props.vis.data.indexPattern?.dataSourceRef?.id || ''; + const validateIndexName = (typedIndexName: string, isIndexNameWithSuffix: boolean) => { const error = []; const errorIndexNameDiv = fetchElementByName('errorIndexName'); @@ -175,7 +177,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { const checkIfIndexExists = async (indexName: string) => { try { - const result = await getIndex(indexName, http); + const result = await getIndex(indexName, http, dataSourceRefId); return result.ok; } catch (e) { return false; @@ -280,7 +282,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { type: GEO_SHAPE_TYPE, data: [JSON.parse(fileData || null)], }; - const result = await postGeojson(JSON.stringify(bodyData), http); + const result = await postGeojson(JSON.stringify(bodyData), http, dataSourceRefId); // error handling logic that displays correct toasts for the end users if (result?.ok) { parsePostGeojsonResult(result, indexName); diff --git a/public/services.ts b/public/services.ts index da4ce07b..0f153ba2 100644 --- a/public/services.ts +++ b/public/services.ts @@ -7,25 +7,34 @@ 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']) => { +export const postGeojson = async ( + requestBody: any, + http: CoreStart['http'], + dataSourceRefId: string +) => { try { - const response = await http.post('../api/custom_import_map/_upload', { + const query = dataSourceRefId ? { dataSourceId: dataSourceRefId } : undefined; + + return await http.post('../api/custom_import_map/_upload', { body: requestBody, + ...(query && { query }), }); - return response; } catch (e) { return e; } }; -export const getIndex = async (indexName: string, http: CoreStart['http']) => { +export const getIndex = async ( + indexName: string, + http: CoreStart['http'], + dataSourceRefId: string +) => { try { - const response = await http.post('../api/custom_import_map/_indices', { - body: JSON.stringify({ - index: indexName, - }), + const query = dataSourceRefId ? { dataSourceId: dataSourceRefId } : undefined; + return await http.post('../api/custom_import_map/_indices', { + body: JSON.stringify({ index: indexName }), + ...(query && { query }), }); - return response; } catch (e) { return e; } diff --git a/server/plugin.ts b/server/plugin.ts index 67243500..5a9fbee6 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -26,10 +26,10 @@ import { geospatial, opensearch, statsRoute } from '../server/routes'; import { mapSavedObjectsType } from './saved_objects'; import { capabilitiesProvider } from './saved_objects/capabilities_provider'; import { ConfigSchema } from '../common/config'; +import GeospatialPlugin from './clusters/geospatial_plugin'; export class CustomImportMapPlugin - implements Plugin -{ + implements Plugin { private readonly logger: Logger; private readonly globalConfig$; private readonly config$; @@ -58,7 +58,11 @@ export class CustomImportMapPlugin const opensearchService = new OpensearchService(geospatialClient); const router = core.http.createRouter(); - const { home } = plugins; + const { home, dataSource } = plugins; + + if (dataSource) { + dataSource.registerCustomApiSchema(GeospatialPlugin); + } // Register server side APIs geospatial(geospatialService, router); diff --git a/server/routes/geospatial.ts b/server/routes/geospatial.ts index c877d449..93255e25 100644 --- a/server/routes/geospatial.ts +++ b/server/routes/geospatial.ts @@ -13,6 +13,7 @@ export default function (services, router) { path: '/api/custom_import_map/_upload', validate: { body: schema.any(), + query: schema.maybe(schema.object({}, { unknowns: 'allow' })), }, options: { body: { diff --git a/server/routes/opensearch.ts b/server/routes/opensearch.ts index 5f5c406f..c64acb27 100644 --- a/server/routes/opensearch.ts +++ b/server/routes/opensearch.ts @@ -14,6 +14,7 @@ export default function (services, router) { body: schema.object({ index: schema.string(), }), + query: schema.maybe(schema.object({}, { unknowns: 'allow' })), }, }, services.getIndex diff --git a/server/services/geospatial_service.js b/server/services/geospatial_service.js index cce9e0c4..48bba20f 100644 --- a/server/services/geospatial_service.js +++ b/server/services/geospatial_service.js @@ -9,11 +9,23 @@ export default class GeospatialService { } uploadGeojson = async (context, req, res) => { + const dataSourceRefId = req.query.dataSourceId; + let uploadResponse; try { - const { callAsCurrentUser } = await this.driver.asScoped(req); - const uploadResponse = await callAsCurrentUser('geospatial.geospatialQuery', { - body: req.body, - }); + if (dataSourceRefId) { + const remoteDataSourceClient = context.dataSource.opensearch.legacy.getClient( + dataSourceRefId + ).callAPI; + uploadResponse = await remoteDataSourceClient('geospatial.geospatialQuery', { + body: req.body, + }); + } else { + const { callAsCurrentUser } = await this.driver.asScoped(req); + uploadResponse = await callAsCurrentUser('geospatial.geospatialQuery', { + body: req.body, + }); + } + return res.ok({ body: { ok: true, diff --git a/server/services/opensearch_service.js b/server/services/opensearch_service.js index ceae5243..68632195 100644 --- a/server/services/opensearch_service.js +++ b/server/services/opensearch_service.js @@ -9,14 +9,23 @@ export default class OpensearchService { } getIndex = async (context, req, res) => { + const dataSourceRefId = req.query.dataSourceId; try { - const { index } = req.body; - const { callAsCurrentUser } = this.driver.asScoped(req); - const indices = await callAsCurrentUser('cat.indices', { - index, - format: 'json', - h: 'health,index,status', - }); + if (dataSourceRefId) { + const remoteDataSourceClient = context.dataSource.opensearch.legacy.getClient( + dataSourceRefId + ).callAPI; + const { index } = req.body; + const indices = await remoteDataSourceClient('cat.indices', { + index, + format: 'json', + h: 'health,index,status', + }); + } else { + const { callAsCurrentUser } = this.driver.asScoped(req); + const { index } = req.body; + const indices = await callAsCurrentUser; + } return res.ok({ body: { ok: true, diff --git a/server/types.ts b/server/types.ts index 0cc8bb06..ae752f08 100644 --- a/server/types.ts +++ b/server/types.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { HomeServerPluginSetup } from '../../../src/plugins/home/server'; +import { DataSourcePluginSetup } from '../../../src/plugins/data_source/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomImportMapPluginSetup {} @@ -11,4 +12,5 @@ export interface CustomImportMapPluginStart {} export interface AppPluginSetupDependencies { home?: HomeServerPluginSetup; + dataSource: DataSourcePluginSetup; } From d53bc2dd5c90f1b0b58655105e278ad6e687843a Mon Sep 17 00:00:00 2001 From: John Mazanec Date: Tue, 30 Apr 2024 17:24:18 -0400 Subject: [PATCH 6/6] Add 2.14 release notes (#616) Signed-off-by: John Mazanec --- CHANGELOG.md | 5 +---- .../opensearch-dashboards-maps.release-notes-2.14.0.0.md | 8 ++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 release-notes/opensearch-dashboards-maps.release-notes-2.14.0.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d68f5dbd..7dfe96ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,13 +12,10 @@ 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.13...2.x) +## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.14...2.x) ### Features -Support multi data source display in Maps app([#611](https://github.com/opensearch-project/dashboards-maps/pull/611)) -Support multi data source in Region map ([#614](https://github.com/opensearch-project/dashboards-maps/pull/614)) ### Enhancements ### Bug Fixes -* Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) ### Infrastructure ### Documentation ### Maintenance diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.14.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.14.0.0.md new file mode 100644 index 00000000..fbb7094c --- /dev/null +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.14.0.0.md @@ -0,0 +1,8 @@ +## Version 2.14.0.0 Release Notes +Compatible with OpenSearch and OpenSearch Dashboards Version 2.14.0 + +### Features +* Support multi data source display in Maps app([#611](https://github.com/opensearch-project/dashboards-maps/pull/611)) +* Support multi data source in Region map ([#614](https://github.com/opensearch-project/dashboards-maps/pull/614)) +### Bug Fixes +* Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) \ No newline at end of file