From f4da95a261dee7cc8e32cfe18b1b7701b657a5ac Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Fri, 30 Apr 2021 12:42:06 -0700 Subject: [PATCH] [Maps] Update default map client (#323) * [Build] Update default maps client Update to not use EMS so it works out of the box. Related to: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/221 Fails linter and should problem write more unit tests but working with smoke tests. Signed-off-by: Kawika Avilla * [Build] fix linter for map messages Signed-off-by: Kawika Avilla * [Build] enable and update map func tests Passing on the CI. Needed to update the casing of the expected response but that was all that was needed. Signed-off-by: Kawika Avilla * [Rename] rename to osd_version Signed-off-by: Kawika Avilla --- .../bin/opensearch-dashboards-docker | 4 +- src/legacy/server/config/schema.js | 70 ++++++++++++++++++- src/plugins/maps_legacy/config.ts | 3 + .../__tests__/map/ems_mocks/sample_files.json | 8 +-- .../public/common/opensearch_maps_client.js | 35 ++++++++++ .../maps_legacy/public/map/map_messages.js | 61 +++++++++++++++- .../public/map/opensearch_dashboards_map.js | 12 +++- .../public/map/service_settings.js | 40 ++++++++++- .../public/map/service_settings.test.js | 6 +- src/plugins/maps_legacy/server/index.ts | 1 + .../region_map/public/choropleth_layer.js | 4 ++ .../public/components/region_map_options.tsx | 23 ------ .../apps/dashboard/dashboard_state.js | 4 +- test/functional/apps/visualize/_region_map.js | 12 ++-- test/functional/apps/visualize/_tile_map.js | 8 +-- 15 files changed, 237 insertions(+), 54 deletions(-) create mode 100644 src/plugins/maps_legacy/public/common/opensearch_maps_client.js diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index a9b7c527e40b..91a3de5014af 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -64,8 +64,8 @@ opensearch_dashboards_vars=( logging.silent logging.useUTC logging.verbose - map.includeElasticMapsService - map.proxyElasticMapsServiceInMaps + map.includeOpenSearchMapsService + map.proxyOpenSearchMapsServiceInMaps map.regionmap map.tilemap.options.attribution map.tilemap.options.maxZoom diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index be1b856ae4da..67776919bdc6 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -152,7 +152,75 @@ export default () => path: HANDLED_IN_NEW_PLATFORM, stats: HANDLED_IN_NEW_PLATFORM, status: HANDLED_IN_NEW_PLATFORM, - map: HANDLED_IN_NEW_PLATFORM, + + map: Joi.object({ + includeOpenSearchMapsService: Joi.boolean().default(true), + proxyOpenSearchMapsServiceInMaps: Joi.boolean().default(false), + tilemap: Joi.object({ + url: Joi.string(), + options: Joi.object({ + attribution: Joi.string(), + minZoom: Joi.number().min(0, 'Must be 0 or higher').default(0), + maxZoom: Joi.number().default(10), + tileSize: Joi.number(), + subdomains: Joi.array().items(Joi.string()).single(), + errorTileUrl: Joi.string().uri(), + tms: Joi.boolean(), + reuseTiles: Joi.boolean(), + bounds: Joi.array().items(Joi.array().items(Joi.number()).min(2).required()).min(2), + default: Joi.boolean(), + }).default({ + default: true, + }), + }).default(), + regionmap: Joi.object({ + includeOpenSearchMapsService: Joi.boolean().default(true), + layers: Joi.array() + .items( + Joi.object({ + url: Joi.string(), + format: Joi.object({ + type: Joi.string().default('geojson'), + }).default({ + type: 'geojson', + }), + meta: Joi.object({ + feature_collection_path: Joi.string().default('data'), + }).default({ + feature_collection_path: 'data', + }), + attribution: Joi.string(), + name: Joi.string(), + fields: Joi.array().items( + Joi.object({ + name: Joi.string(), + description: Joi.string(), + }) + ), + }) + ) + .default([]), + }).default(), + manifestServiceUrl: Joi.string().default('').allow(''), + opensearchManifestServiceUrl: Joi.string().default( + 'https://maps.search-services.aws.a2z.com/v4/ap-southeast-1/manifest' + ), + emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'), + emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), + emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.9'), + emsFontLibraryUrl: Joi.string().default( + 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' + ), + emsTileLayerId: Joi.object({ + bright: Joi.string().default('road_map'), + desaturated: Joi.string().default('road_map_desaturated'), + dark: Joi.string().default('dark_map'), + }).default({ + bright: 'road_map', + desaturated: 'road_map_desaturated', + dark: 'dark_map', + }), + }).default(), i18n: Joi.object({ locale: Joi.string().default('en'), diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts index 4d9fe15a6a28..16022fb29b0a 100644 --- a/src/plugins/maps_legacy/config.ts +++ b/src/plugins/maps_legacy/config.ts @@ -40,6 +40,9 @@ export const configSchema = schema.object({ tilemap: tilemapSchema, regionmap: regionmapSchema, manifestServiceUrl: schema.string({ defaultValue: '' }), + opensearchManifestServiceUrl: schema.string({ + defaultValue: 'https://maps.search-services.aws.a2z.com/v4/us-east-1/manifest', + }), emsFileApiUrl: schema.string({ defaultValue: 'https://vector.maps.opensearch.org' }), emsTileApiUrl: schema.string({ defaultValue: 'https://tiles.maps.opensearch.org' }), emsLandingPageUrl: schema.string({ defaultValue: 'https://maps.opensearch.org/v7.10' }), diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json index 4e0ab681553e..1c43aa88f77b 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json @@ -33,7 +33,7 @@ "type": "id", "id": "iso2", "label": { - "en": "ISO 3166-1 alpha-2 code", + "en": "ISO 3166-1 alpha-2 Code", "af": "landkode (ISO 3166-1 alpha-2)", "ar": "أيزو 3166-1 حرفي-2", "be": "код краіны (ISO 3166-1 alpha-2)", @@ -115,7 +115,7 @@ "type": "id", "id": "iso3", "label": { - "en": "ISO 3166-1 alpha-3 code", + "en": "ISO 3166-1 alpha-3 Code", "af": "landkode (ISO 3166-1 alpha-3)", "ar": "أيزو 3166-1 حرفي-3", "be": "код краіны (ISO 3166-1 alpha-3)", @@ -195,7 +195,7 @@ "type": "property", "id": "name", "label": { - "en": "name", + "en": "Name", "am": "ስም", "ar": "الاسم", "ast": "alcuñu", @@ -439,7 +439,7 @@ "type": "id", "id": "iso2", "label": { - "en": "ISO 3166-1 alpha-2 code" + "en": "ISO 3166-1 alpha-2 Code" } } ], diff --git a/src/plugins/maps_legacy/public/common/opensearch_maps_client.js b/src/plugins/maps_legacy/public/common/opensearch_maps_client.js new file mode 100644 index 000000000000..72379555ea41 --- /dev/null +++ b/src/plugins/maps_legacy/public/common/opensearch_maps_client.js @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { EMSClient } from '@elastic/ems-client'; + +export class OpenSearchMapsClient extends EMSClient { + constructor({ osdVersion, manifestServiceUrl, language, landingPageUrl, fetchFunction }) { + super({ osdVersion, manifestServiceUrl, language, landingPageUrl, fetchFunction }); + this._queryParams = { + osd_version: osdVersion, + opensearch_tos_agree: true, + }; + this._manifestServiceUrl = manifestServiceUrl; + } + + async isEnabled() { + let result; + try { + result = await this._fetchWithTimeout(this._manifestServiceUrl); + } catch (e) { + // silently ignoring the exception and returning false. + return false; + } + if (result.ok) { + const resultJson = await result.json(); + return resultJson.enabled; + } + return false; + } +} diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js index 6d99c96dab77..cb4f786c9cd1 100644 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ b/src/plugins/maps_legacy/public/map/map_messages.js @@ -30,11 +30,67 @@ * GitHub history for details. */ -import React from 'react'; +/* eslint-disable react/no-multi-comp */ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; import { FormattedMessage } from '@osd/i18n/react'; -import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; +import { EuiSpacer, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui'; import { toMountPoint } from '../../../opensearch_dashboards_react/public'; +export const createRegionBlockedWarning = (function () { + /* eslint-disable react/prefer-stateless-function */ + class RegionBlockedWarningOverlay extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( + The default Web Map Service is currently not available in your region.} + titleSize="xs" + body={ + +

+ You can configure OpenSearch Dash to use a different map server for coordinate maps + by modifying the default WMS properties. +

+
+ } + /> + ); + } + } + return () => { + let messageBlock = document.getElementById('blocker-div'); + if (!messageBlock) { + messageBlock = document.createElement('div'); + messageBlock.id = 'blocker-div'; + messageBlock.setAttribute('class', 'visError leaflet-popup-pane'); + Array.prototype.forEach.call( + document.getElementsByClassName('leaflet-container'), + (leafletDom) => { + ReactDOM.render( + new RegionBlockedWarningOverlay().render(), + leafletDom.appendChild(messageBlock) + ); + } + ); + } + }; +})(); + +export const removeRegionBlockedWarning = (function () { + return () => { + const childEle = document.getElementById('blocker-div'); + if (childEle) { + childEle.parentNode.removeChild(childEle); + } + }; +})(); + export const createZoomWarningMsg = (function () { let disableZoomMsg = false; const setZoomMsg = (boolDisableMsg) => (disableZoomMsg = boolDisableMsg); @@ -59,6 +115,7 @@ export const createZoomWarningMsg = (function () { access to additional zoom levels for free through the {ems}. Or, you can configure your own map server. Please go to { wms } or { configSettings} for more information." + // TODO: [RENAMEME] Need valid URLs values={{ defaultDistribution: ( diff --git a/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js b/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js index c35ea372c2bc..c2c94e8bfa5d 100644 --- a/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js +++ b/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js @@ -31,7 +31,11 @@ */ import { EventEmitter } from 'events'; -import { createZoomWarningMsg } from './map_messages'; +import { + createZoomWarningMsg, + createRegionBlockedWarning, + removeRegionBlockedWarning, +} from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; @@ -606,6 +610,11 @@ export class OpenSearchDashboardsMap extends EventEmitter { baseLayer.on('loading', () => { this.emit('baseLayer:loading'); }); + baseLayer.on('tileerror', () => { + if (baseLayer._url.includes('search-services.aws.a2z.com')) { + createRegionBlockedWarning(); + } + }); this._leafletBaseLayer = baseLayer; if (settings.options.showZoomMessage) { @@ -684,6 +693,7 @@ export class OpenSearchDashboardsMap extends EventEmitter { } _updateDesaturation() { + removeRegionBlockedWarning(); const tiles = $('img.leaflet-tile-loaded'); // Don't apply client-side styling to EMS basemaps if (get(this._baseLayerSettings, 'options.origin') === ORIGIN.EMS) { diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index da38d6fd0be9..5b256a10cfa4 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -33,6 +33,7 @@ import _ from 'lodash'; import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; +import { OpenSearchMapsClient } from '../common/opensearch_maps_client.js'; import { i18n } from '@osd/i18n'; import { getOpenSearchDashboardsVersion } from '../opensearch_dashboards_services'; import { ORIGIN } from '../common/constants/origin'; @@ -46,13 +47,15 @@ export class ServiceSettings { this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; this._showZoomMessage = true; - this._emsClient = new EMSClient({ + this._emsClient = null; + this._opensearchMapsClient = new OpenSearchMapsClient({ language: i18n.getLocale(), appVersion: getOpenSearchDashboardsVersion(), appName: 'opensearch-dashboards', fileApiUrl: this._mapConfig.emsFileApiUrl, tileApiUrl: this._mapConfig.emsTileApiUrl, - landingPageUrl: this._mapConfig.emsLandingPageUrl, + landingPageUrl: '', + manifestServiceUrl: this._mapConfig.opensearchManifestServiceUrl, // Wrap to avoid errors passing window fetch fetchFunction: function (...args) { return fetch(...args); @@ -87,6 +90,7 @@ export class ServiceSettings { } __debugStubManifestCalls(manifestRetrieval) { + this._emsClient = this._opensearchMapsClient; const oldGetManifest = this._emsClient.getManifest; this._emsClient.getManifest = manifestRetrieval; return { @@ -118,11 +122,39 @@ export class ServiceSettings { }; }; + // anyone using this._emsClient should call this method before, to set the right client + async _setMapServices() { + // if client is not null, return immediately. + // Effectively, client creation will be called only once. + if (this._emsClient) { + return; + } + const useOpenSearchMaps = await this._opensearchMapsClient.isEnabled(); + if (useOpenSearchMaps) { + // using OpenSearch Maps. + this._emsClient = this._opensearchMapsClient; + } else { + // not using OpenSearch Maps, fallback to default maps. + this._emsClient = new EMSClient({ + language: i18n.getLocale(), + appVersion: getOpenSearchDashboardsVersion(), + appName: 'opensearch-dashboards', + fileApiUrl: this._mapConfig.emsFileApiUrl, + tileApiUrl: this._mapConfig.emsTileApiUrl, + landingPageUrl: this._mapConfig.emsLandingPageUrl, + fetchFunction: function (...args) { + return fetch(...args); + }, + }); + } + } + async getFileLayers() { if (!this._mapConfig.includeOpenSearchMapsService) { return []; } + await this._setMapServices(); const fileLayers = await this._emsClient.getFileLayers(); return fileLayers.map(this._backfillSettings); } @@ -141,6 +173,7 @@ export class ServiceSettings { allServices.push(tmsService); } + await this._setMapServices(); if (this._mapConfig.includeOpenSearchMapsService) { const servicesFromManifest = await this._emsClient.getTMSServices(); const strippedServiceFromManifest = await Promise.all( @@ -183,6 +216,7 @@ export class ServiceSettings { } async getEMSHotLink(fileLayerConfig) { + await this._setMapServices(); const layer = await this.getFileLayerFromConfig(fileLayerConfig); return layer ? layer.getEMSHotLink() : null; } @@ -193,6 +227,7 @@ export class ServiceSettings { } async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) { + await this._setMapServices(); const tmsServices = await this._emsClient.getTMSServices(); const emsTileLayerId = this._mapConfig.emsTileLayerId; let serviceId; @@ -236,6 +271,7 @@ export class ServiceSettings { } async _getFileUrlFromEMS(fileLayerConfig) { + await this._setMapServices(); const fileLayers = await this._emsClient.getFileLayers(); const layer = fileLayers.find((fileLayer) => { const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 939041b1284b..d2574ac11caf 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -309,9 +309,9 @@ describe('service_settings (FKA tile_map test)', function () { 'Made with NaturalEarth | OpenSearch Maps Service', format: 'geojson', fields: [ - { type: 'id', name: 'iso2', description: 'ISO 3166-1 alpha-2 code' }, - { type: 'id', name: 'iso3', description: 'ISO 3166-1 alpha-3 code' }, - { type: 'property', name: 'name', description: 'name' }, + { type: 'id', name: 'iso2', description: 'ISO 3166-1 alpha-2 Code' }, + { type: 'id', name: 'iso3', description: 'ISO 3166-1 alpha-3 Code' }, + { type: 'property', name: 'name', description: 'Name' }, ], created_at: '2017-04-26T17:12:15.978370', //not present in 6.6 name: 'World Countries', diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index be59c8230b93..bad2223e927d 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -43,6 +43,7 @@ export const config: PluginConfigDescriptor = { tilemap: true, regionmap: true, manifestServiceUrl: true, + opensearchManifestServiceUrl: true, emsFileApiUrl: true, emsTileApiUrl: true, emsLandingPageUrl: true, diff --git a/src/plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js index 690c1bf1e82d..c6b15eeb7bb4 100644 --- a/src/plugins/region_map/public/choropleth_layer.js +++ b/src/plugins/region_map/public/choropleth_layer.js @@ -193,6 +193,10 @@ Make sure the file exists at that location.", values: { name: name }, } ); + } else if (e.config.url.includes('aws.a2z.com')) { + // AES Region Maps will throw CORS exception when accessed from Embargo Regions. + // OPTIONS will fail before GET. Thus CORS error. + errorMessage = 'The vector map ' + name + ' is not available.'; } else { errorMessage = i18n.translate( 'regionMap.choroplethLayer.downloadingVectorDataErrorMessage', diff --git a/src/plugins/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx index d9fd44b3fc22..315fd97e95d6 100644 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -117,29 +117,6 @@ function RegionMapOptions(props: RegionMapOptionsProps) { label={i18n.translate('regionMap.visParams.vectorMapLabel', { defaultMessage: 'Vector map', })} - labelAppend={ - stateParams.emsHotLink && ( - - - {' '} - - - - ) - } options={vectorLayerOptions} paramName="selectedLayer" value={stateParams.selectedLayer && stateParams.selectedLayer.layerId} diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index b579e7b87daa..37d33dfa8843 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -155,9 +155,7 @@ export default function ({ getService, getPageObjects }) { expect(headers.length).to.be(0); }); - // TODO: [RENAMEME] Re-enable once a valid maps service is in place. - // See: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/221 - xit('Tile map with no changes will update with visualization changes', async () => { + it('Tile map with no changes will update with visualization changes', async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); diff --git a/test/functional/apps/visualize/_region_map.js b/test/functional/apps/visualize/_region_map.js index 92b7acd16cf1..4afaea00d9f9 100644 --- a/test/functional/apps/visualize/_region_map.js +++ b/test/functional/apps/visualize/_region_map.js @@ -33,9 +33,7 @@ import expect from '@osd/expect'; export default function ({ getService, getPageObjects }) { - // TODO: [RENAMEME] Re-enable once a valid maps service is in place. - // See: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/221 - xdescribe('vector map', function () { + describe('vector map', function () { const inspector = getService('inspector'); const log = getService('log'); const find = getService('find'); @@ -85,19 +83,19 @@ export default function ({ getService, getPageObjects }) { //ensure all fields are there await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', - 'ISO 3166-1 alpha-2 code' + 'ISO 3166-1 alpha-2 Code' ); await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', - 'ISO 3166-1 alpha-3 code' + 'ISO 3166-1 alpha-3 Code' ); await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', - 'name' + 'Name' ); await PageObjects.visEditor.setSelectByOptionText( 'regionMapOptionsSelectJoinField', - 'ISO 3166-1 alpha-2 code' + 'ISO 3166-1 alpha-2 Code' ); await inspector.open(); diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index 6408c0818b45..12fa012dbba4 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -69,9 +69,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // TODO: [RENAMEME] Re-enable once a valid maps service is in place. - // See: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/221 - xdescribe('complete config', function describeIndexTests() { + describe('complete config', function describeIndexTests() { before(async function () { await browser.setWindowSize(1280, 1000); @@ -242,9 +240,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // TODO: [RENAMEME] Re-enable once a valid maps service is in place. - // See: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/221 - xdescribe('zoom warning behavior', function describeIndexTests() { + describe('zoom warning behavior', function describeIndexTests() { // Zoom warning is only applicable to OSS this.tags(['skipCloud', 'skipFirefox']);