diff --git a/.stylelintignore b/.stylelintignore index b3fdf275c9..6eeec870de 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1,2 @@ build -target \ No newline at end of file +target diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index bf477bc18d..e0730cc0d9 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -75,12 +75,7 @@ export default { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], coverageReporters: ['lcov', 'text-summary'], moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], - modulePathIgnorePatterns: [ - '__fixtures__/', - 'target/', - '/src/plugins/maps_legacy', - '/src/plugins/region_map', - ], + modulePathIgnorePatterns: ['__fixtures__/', 'target/', '/src/plugins/maps_legacy'], testEnvironment: 'jest-environment-jsdom', testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ diff --git a/src/plugins/region_map/common/constants/shared.ts b/src/plugins/region_map/common/constants/shared.ts new file mode 100644 index 0000000000..6d82a3a336 --- /dev/null +++ b/src/plugins/region_map/common/constants/shared.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const DEFAULT_MAP_CHOICE = 'default'; +export const CUSTOM_MAP_CHOICE = 'custom'; diff --git a/src/plugins/region_map/common/index.ts b/src/plugins/region_map/common/index.ts new file mode 100644 index 0000000000..bdda981590 --- /dev/null +++ b/src/plugins/region_map/common/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE } from './constants/shared'; + +export { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE }; diff --git a/src/plugins/region_map/opensearch_dashboards.json b/src/plugins/region_map/opensearch_dashboards.json index 2f107a4156..b285954137 100644 --- a/src/plugins/region_map/opensearch_dashboards.json +++ b/src/plugins/region_map/opensearch_dashboards.json @@ -11,7 +11,8 @@ "mapsLegacy", "opensearchDashboardsLegacy", "data", - "share" + "share", + "opensearchDashboardsReact" ], "requiredBundles": [ "opensearchDashboardsUtils", diff --git a/src/plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js index 4a41910082..e0213108f6 100644 --- a/src/plugins/region_map/public/choropleth_layer.js +++ b/src/plugins/region_map/public/choropleth_layer.js @@ -36,6 +36,8 @@ import * as topojson from 'topojson-client'; import { getNotifications } from './opensearch_dashboards_services'; import { colorUtil, OpenSearchDashboardsMapLayer } from '../../maps_legacy/public'; import { truncatedColorMaps } from '../../charts/public'; +import { getServices } from './services'; +import { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE } from '../common'; const EMPTY_STYLE = { weight: 1, @@ -90,7 +92,9 @@ export class ChoroplethLayer extends OpenSearchDashboardsMapLayer { meta, layerConfig, serviceSettings, - leaflet + leaflet, + layerChosenByUser, + http ) { super(); this._serviceSettings = serviceSettings; @@ -105,6 +109,9 @@ export class ChoroplethLayer extends OpenSearchDashboardsMapLayer { this._layerName = name; this._layerConfig = layerConfig; this._leaflet = leaflet; + this._layerChosenByUser = layerChosenByUser; + this._http = http; + this._visParams = null; // eslint-disable-next-line no-undef this._leafletLayer = this._leaflet.geoJson(null, { @@ -139,7 +146,14 @@ export class ChoroplethLayer extends OpenSearchDashboardsMapLayer { this._isJoinValid = false; this._whenDataLoaded = new Promise(async (resolve) => { try { - const data = await this._makeJsonAjaxCall(); + let data; + if (DEFAULT_MAP_CHOICE === this._layerChosenByUser) { + data = await this._makeJsonAjaxCall(); + } else if (CUSTOM_MAP_CHOICE === this._layerChosenByUser) { + data = await this._fetchCustomLayerData(); + } else { + return; + } let featureCollection; let formatType; if (typeof format === 'string') { @@ -223,6 +237,29 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards return this._serviceSettings.getJsonForRegionLayer(this._layerConfig); } + async _fetchCustomLayerData() { + // fetch data from index and transform it to feature collection + try { + const services = getServices(this._http); + const result = await services.getIndexData(this._layerName); + + const finalResult = { + type: 'FeatureCollection', + features: [], + }; + for (let featureCount = 0; featureCount < result.resp.hits.hits.length; featureCount++) { + finalResult.features.push({ + geometry: result.resp.hits.hits[featureCount]._source.location, + properties: removeKeys(result.resp.hits.hits[featureCount]._source), + type: 'Feature', + }); + } + return finalResult; + } catch (e) { + return false; + } + } + _invalidateJoin() { this._isJoinValid = false; } @@ -298,7 +335,9 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards meta, layerConfig, serviceSettings, - leaflet + leaflet, + layerChosenByUser, + http ) { const clonedLayer = new ChoroplethLayer( name, @@ -308,7 +347,9 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards meta, layerConfig, serviceSettings, - leaflet + leaflet, + layerChosenByUser, + http ); clonedLayer.setJoinField(this._joinField); clonedLayer.setColorRamp(this._colorRamp); @@ -335,6 +376,14 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards return this._whenDataLoaded; } + setLayerChosenByUser(layerChosenByUser) { + this._layerChosenByUser = layerChosenByUser; + } + + setVisParams(visParams) { + this._visParams = visParams; + } + setMetrics(metrics, fieldFormatter, metricTitle) { this._metrics = metrics.slice(); this._valueFormatter = fieldFormatter; @@ -520,3 +569,11 @@ function getChoroplethColor(value, min, max, colorRamp) { return colorUtil.getColor(colorRamp, i); } + +function removeKeys(myObj) { + const array = ['id', 'location']; + for (let index = 0; index < array.length; index++) { + delete myObj[array[index]]; + } + return myObj; +} diff --git a/src/plugins/region_map/public/components/__snapshots__/region_map_options.test.tsx.snap b/src/plugins/region_map/public/components/__snapshots__/region_map_options.test.tsx.snap new file mode 100644 index 0000000000..447d5876fa --- /dev/null +++ b/src/plugins/region_map/public/components/__snapshots__/region_map_options.test.tsx.snap @@ -0,0 +1,1097 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`region_map_options renders the RegionMapOptions with custom option if custom vector maps are found 1`] = ` +
+
+

+ + Layer settings + +

+
+
+ + + Choose a vector map layer + + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+
+ +
+ + + Display warnings + +
+
+
+
+
+
+ +
+ + + Show all shapes + +
+
+
+
+
+
+
+

+ + Style settings + +

+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+

+ + Base layer settings + +

+
+
+
+ +
+ + + WMS map server + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+`; + +exports[`region_map_options renders the RegionMapOptions with default option if no custom vector maps are found 1`] = ` +
+
+

+ + Layer settings + +

+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+
+ +
+ + + Display warnings + +
+
+
+
+
+
+ +
+ + + Show all shapes + +
+
+
+
+
+
+
+

+ + Style settings + +

+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+

+ + Base layer settings + +

+
+
+
+ +
+ + + WMS map server + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+`; diff --git a/src/plugins/region_map/public/components/__snapshots__/style_options.test.tsx.snap b/src/plugins/region_map/public/components/__snapshots__/style_options.test.tsx.snap new file mode 100644 index 0000000000..dddad0fadf --- /dev/null +++ b/src/plugins/region_map/public/components/__snapshots__/style_options.test.tsx.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`style_options renders the Style options comprising of style settings, color schema and border thickness 1`] = ` +
+

+ + Style settings + +

+
+
+
+ +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+`; diff --git a/src/plugins/region_map/public/components/default_map_options.tsx b/src/plugins/region_map/public/components/default_map_options.tsx new file mode 100644 index 0000000000..1ce41474b4 --- /dev/null +++ b/src/plugins/region_map/public/components/default_map_options.tsx @@ -0,0 +1,144 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; +import { SelectOption, SwitchOption } from '../../../charts/public'; +import { RegionMapVisParams } from '../../../maps_legacy/public'; + +const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ + text: name, + value: layerId, +}); + +const mapFieldForOption = ({ description, name }: FileLayerField) => ({ + text: description, + value: name, +}); + +export type DefaultMapOptionsProps = { + getServiceSettings: () => Promise; +} & VisOptionsProps; + +function DefaultMapOptions(props: DefaultMapOptionsProps) { + const { getServiceSettings, stateParams, vis, setValue } = props; + + const vectorLayers = vis.type.editorConfig.collections.vectorLayers; + const vectorLayerOptions = useMemo(() => vectorLayers.map(mapLayerForOption), [vectorLayers]); + + const fieldOptions = useMemo( + () => + ((stateParams.selectedLayer && stateParams.selectedLayer.fields) || []).map( + mapFieldForOption + ), + [stateParams.selectedLayer] + ); + + const setEmsHotLink = useCallback( + async (layer: VectorLayer) => { + const serviceSettings = await getServiceSettings(); + const emsHotLink = await serviceSettings.getEMSHotLink(layer); + setValue('emsHotLink', emsHotLink); + }, + [setValue, getServiceSettings] + ); + + const setLayer = useCallback( + async (paramName: 'selectedLayer', value: VectorLayer['layerId']) => { + const newLayer = vectorLayers.find(({ layerId }: VectorLayer) => layerId === value); + + if (newLayer) { + setValue(paramName, newLayer); + setValue('selectedJoinField', newLayer.fields[0]); + setEmsHotLink(newLayer); + } + }, + [vectorLayers, setEmsHotLink, setValue] + ); + + const setField = useCallback( + (paramName: 'selectedJoinField', value: FileLayerField['name']) => { + if (stateParams.selectedLayer) { + setValue( + paramName, + stateParams.selectedLayer.fields.find((f) => f.name === value) + ); + } + }, + [setValue, stateParams.selectedLayer] + ); + + return ( + + +

+ +

+
+ + + + + + + + + + + + + +
+ ); +} + +export { DefaultMapOptions }; diff --git a/src/plugins/region_map/public/components/map_choice_options.scss b/src/plugins/region_map/public/components/map_choice_options.scss new file mode 100644 index 0000000000..cfd3509ead --- /dev/null +++ b/src/plugins/region_map/public/components/map_choice_options.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.mapChoiceGroup { + font-size: small; +} diff --git a/src/plugins/region_map/public/components/map_choice_options.test.tsx b/src/plugins/region_map/public/components/map_choice_options.test.tsx new file mode 100644 index 0000000000..69b2e71399 --- /dev/null +++ b/src/plugins/region_map/public/components/map_choice_options.test.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from 'react'; +import { MapChoiceOptions } from './map_choice_options'; +import { screen, render } from '@testing-library/react'; +import { fireEvent, getByTestId } from '@testing-library/dom'; +import '@testing-library/jest-dom'; + +describe('map_choice_options', () => { + it('renders the MapChoiceOptions based on the props provided', async () => { + const props = jest.mock; + const vis = { + type: { + editorConfig: { + collections: { + colorSchemas: [], + customVectorLayers: [], + tmsLayers: [], + vectorLayers: [ + { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: [ + { + name: 'name', + type: 'id', + description: 'description', + }, + ], + id: 'sample', + meta: undefined, + name: 'sample', + origin: 'user-upload', + }, + ], + }, + }, + }, + }; + + const stateParams = { + colorSchema: {}, + outlineWeight: {}, + wms: {}, + selectedLayer: { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: [ + { + name: 'name', + type: 'id', + description: 'description', + }, + ], + id: 'sample', + meta: undefined, + name: 'sample', + origin: 'user-upload', + }, + selectedJoinField: { + name: 'name', + type: 'id', + description: 'description', + }, + }; + render(); + const defaultVectorSelection = screen.getByTestId('defaultVectorMap'); + const customVectorSelection = screen.getByTestId('customVectorMap'); + fireEvent.click(defaultVectorSelection); + await expect(defaultVectorSelection).toBeChecked; + await expect(customVectorSelection).not.toBeChecked; + }); + + it('renders the MapChoiceOptions based on the props provided for custom selection', async () => { + const props = jest.mock; + const vis = { + type: { + editorConfig: { + collections: { + colorSchemas: [], + customVectorLayers: [ + { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: [ + { + name: 'name', + type: 'id', + description: 'description', + }, + ], + id: 'sample', + meta: undefined, + name: 'sample', + origin: 'user-upload', + }, + ], + tmsLayers: [], + vectorLayers: [], + }, + }, + }, + }; + + const stateParams = { + colorSchema: {}, + outlineWeight: {}, + wms: {}, + selectedCustomLayer: { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: [ + { + name: 'name', + type: 'id', + description: 'description', + }, + ], + id: 'sample', + meta: undefined, + name: 'sample', + origin: 'user-upload', + }, + selectedCustomJoinField: { + name: 'name', + type: 'id', + description: 'description', + }, + }; + render(); + const defaultVectorSelection = screen.getByTestId('defaultVectorMap'); + const customVectorSelection = screen.getByTestId('customVectorMap'); + fireEvent.click(customVectorSelection); + await expect(customVectorSelection).toBeChecked; + await expect(defaultVectorSelection).not.toBeChecked; + }); +}); diff --git a/src/plugins/region_map/public/components/map_choice_options.tsx b/src/plugins/region_map/public/components/map_choice_options.tsx new file mode 100644 index 0000000000..f18cf88b37 --- /dev/null +++ b/src/plugins/region_map/public/components/map_choice_options.tsx @@ -0,0 +1,265 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './map_choice_options.scss'; +import React, { useCallback, useMemo, useState } from 'react'; +import { + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + EuiCheckableCard, + EuiFlexItem, + EuiFlexGroup, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; +import { SelectOption, SwitchOption } from '../../../charts/public'; +import { RegionMapVisParams } from '../../../maps_legacy/public'; +import { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE } from '../../common'; + +const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ + text: name, + value: layerId, +}); + +const mapFieldForOption = ({ description, name }: FileLayerField) => ({ + text: description, + value: name, +}); + +const mapCustomJoinFieldForOption = ({ description, name }: FileLayerField) => ({ + label: name, + text: description, + value: name, +}); + +export type MapChoiceOptionsProps = { + getServiceSettings: () => Promise; +} & VisOptionsProps; + +function MapChoiceOptions(props: MapChoiceOptionsProps) { + const { getServiceSettings, stateParams, vis, setValue } = props; + const vectorLayers = vis.type.editorConfig.collections.vectorLayers; + const customVectorLayers = vis.type.editorConfig.collections.customVectorLayers; + const vectorLayerOptions = useMemo(() => vectorLayers.map(mapLayerForOption), [vectorLayers]); + const customVectorLayerOptions = useMemo(() => customVectorLayers.map(mapLayerForOption), [ + customVectorLayers, + ]); + + const fieldOptions = useMemo( + () => + ((stateParams.selectedLayer && stateParams.selectedLayer.fields) || []).map( + mapFieldForOption + ), + [stateParams.selectedLayer] + ); + + const customFieldOptions = useMemo( + () => + ((stateParams.selectedCustomLayer && stateParams.selectedCustomLayer.fields) || []).map( + mapCustomJoinFieldForOption + ), + [stateParams.selectedCustomLayer] + ); + + const selectDefaultVectorMap = () => { + setValue('layerChosenByUser', DEFAULT_MAP_CHOICE); + }; + + const selectCustomVectorMap = () => { + setValue('layerChosenByUser', CUSTOM_MAP_CHOICE); + }; + + const setEmsHotLink = useCallback( + async (layer: VectorLayer) => { + const serviceSettings = await getServiceSettings(); + const emsHotLink = await serviceSettings.getEMSHotLink(layer); + setValue('emsHotLink', emsHotLink); + }, + [setValue, getServiceSettings] + ); + + const setLayer = useCallback( + async (paramName: 'selectedLayer', value: VectorLayer['layerId']) => { + const newLayer = vectorLayers.find(({ layerId }: VectorLayer) => layerId === value); + + if (newLayer) { + setValue(paramName, newLayer); + setValue('selectedJoinField', newLayer.fields[0]); + setEmsHotLink(newLayer); + } + }, + [vectorLayers, setEmsHotLink, setValue] + ); + + const setCustomLayer = useCallback( + async (paramName: 'selectedCustomLayer', value: VectorLayer['layerId']) => { + const newLayer = customVectorLayers.find(({ layerId }: VectorLayer) => layerId === value); + + if (newLayer) { + setValue(paramName, newLayer); + setValue('selectedJoinField', newLayer.fields[0]); + } + }, + [customVectorLayers, setValue] + ); + + const setCustomJoinField = useCallback( + async (paramName: 'selectedCustomJoinField', value) => { + if (stateParams.selectedCustomLayer) { + setValue( + paramName, + stateParams.selectedCustomLayer.fields.find((f) => f.name === value) + ); + } + }, + [setValue, stateParams.selectedCustomLayer] + ); + + const setField = useCallback( + (paramName: 'selectedJoinField', value: FileLayerField['name']) => { + if (stateParams.selectedLayer) { + setValue( + paramName, + stateParams.selectedLayer.fields.find((f) => f.name === value) + ); + } + }, + [setValue, stateParams.selectedLayer] + ); + + return ( + + +

+ +

+
+ + + + + Choose a vector map layer + + + + + + + + + + + + + + {DEFAULT_MAP_CHOICE === stateParams.layerChosenByUser ? ( + + + + + + + + + ) : ( + + + + + + + + + )} + + + + + +
+ ); +} + +export { MapChoiceOptions }; diff --git a/src/plugins/region_map/public/components/region_map_options.test.tsx b/src/plugins/region_map/public/components/region_map_options.test.tsx new file mode 100644 index 0000000000..deca781056 --- /dev/null +++ b/src/plugins/region_map/public/components/region_map_options.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from 'react'; +import { RegionMapOptions } from './region_map_options'; +import renderer, { act } from 'react-test-renderer'; + +describe('region_map_options', () => { + it('renders the RegionMapOptions with default option if no custom vector maps are found', async () => { + const props = jest.mock; + const vis = { + type: { + editorConfig: { + collections: { + colorSchemas: [], + customVectorLayers: [], + tmsLayers: [], + vectorLayers: [ + { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: [], + id: 'sample', + meta: undefined, + name: 'sample', + origin: 'user-upload', + }, + ], + }, + }, + }, + }; + const stateParams = { + colorSchema: {}, + outlineWeight: {}, + wms: {}, + selectedJoinField: { + name: 'randomId', + }, + selectedLayer: { + layerId: 'name', + fields: [ + { + name: 'name', + type: 'name', + property: 'name', + }, + ], + }, + }; + + let tree; + await act(async () => { + tree = renderer.create(); + }); + expect(tree.toJSON().props.id).toBe('defaultMapOption'); + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('renders the RegionMapOptions with custom option if custom vector maps are found', async () => { + const props = jest.mock; + const vis = { + type: { + editorConfig: { + collections: { + colorSchemas: [], + customVectorLayers: [ + { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: [], + id: 'sample', + meta: undefined, + name: 'sample', + origin: 'user-upload', + }, + ], + tmsLayers: [], + vectorLayers: [], + }, + }, + }, + }; + const stateParams = { + colorSchema: {}, + outlineWeight: {}, + wms: {}, + selectedJoinField: { + name: 'randomId', + }, + selectedCustomLayer: { + fields: [ + { + name: 'name', + property: 'name', + type: 'name', + }, + ], + layerId: 'sample', + }, + }; + + let tree; + await act(async () => { + tree = renderer.create(); + }); + expect(tree.toJSON().props.id).toBe('customMapOption'); + expect(tree.toJSON()).toMatchSnapshot(); + }); +}); 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 ed3b12baae..d876eda7fd 100644 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -28,176 +28,51 @@ * under the License. */ -import React, { useCallback, useMemo } from 'react'; -import { EuiIcon, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { i18n } from '@osd/i18n'; -import { FormattedMessage } from '@osd/i18n/react'; +import React, { useMemo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; -import { NumberInputOption, SelectOption, SwitchOption } from '../../../charts/public'; +import { VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; import { RegionMapVisParams, WmsOptions } from '../../../maps_legacy/public'; +import { DefaultMapOptions } from './default_map_options'; +import { MapChoiceOptions } from './map_choice_options'; +import { StyleOptions } from './style_options'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ text: name, value: layerId, }); -const mapFieldForOption = ({ description, name }: FileLayerField) => ({ - text: description, - value: name, -}); - export type RegionMapOptionsProps = { getServiceSettings: () => Promise; } & VisOptionsProps; function RegionMapOptions(props: RegionMapOptionsProps) { - const { getServiceSettings, stateParams, vis, setValue } = props; - const { vectorLayers } = vis.type.editorConfig.collections; - const vectorLayerOptions = useMemo(() => vectorLayers.map(mapLayerForOption), [vectorLayers]); - const fieldOptions = useMemo( - () => - ((stateParams.selectedLayer && stateParams.selectedLayer.fields) || []).map( - mapFieldForOption - ), - [stateParams.selectedLayer] - ); + const customVectorLayers = props.vis.type.editorConfig.collections.customVectorLayers; + const customVectorLayerOptions = useMemo(() => customVectorLayers.map(mapLayerForOption), [ + customVectorLayers, + ]); - const setEmsHotLink = useCallback( - async (layer: VectorLayer) => { - const serviceSettings = await getServiceSettings(); - const emsHotLink = await serviceSettings.getEMSHotLink(layer); - setValue('emsHotLink', emsHotLink); - }, - [setValue, getServiceSettings] - ); - - const setLayer = useCallback( - async (paramName: 'selectedLayer', value: VectorLayer['layerId']) => { - const newLayer = vectorLayers.find(({ layerId }: VectorLayer) => layerId === value); - - if (newLayer) { - setValue(paramName, newLayer); - setValue('selectedJoinField', newLayer.fields[0]); - setEmsHotLink(newLayer); - } - }, - [vectorLayers, setEmsHotLink, setValue] - ); - - const setField = useCallback( - (paramName: 'selectedJoinField', value: FileLayerField['name']) => { - if (stateParams.selectedLayer) { - setValue( - paramName, - stateParams.selectedLayer.fields.find((f) => f.name === value) - ); - } - }, - [setValue, stateParams.selectedLayer] - ); - - return ( - <> - - -

- -

-
+ if (customVectorLayerOptions.length === 0) { + return ( +
+ - - - - - - - - - - - - - - -

- -

-
+ - - - - -
- - - - - - ); + +
+ ); + } else { + return ( +
+ + + + + +
+ ); + } } export { RegionMapOptions }; diff --git a/src/plugins/region_map/public/components/style_options.test.tsx b/src/plugins/region_map/public/components/style_options.test.tsx new file mode 100644 index 0000000000..880289f4f7 --- /dev/null +++ b/src/plugins/region_map/public/components/style_options.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from 'react'; +import { StyleOptions } from './style_options'; +import renderer, { act } from 'react-test-renderer'; + +describe('style_options', () => { + it('renders the Style options comprising of style settings, color schema and border thickness', async () => { + const props = jest.mock; + const vis = { + type: { + editorConfig: { + collections: { + colorSchemas: [], + }, + }, + }, + }; + const stateParams = { + colorSchema: {}, + outlineWeight: {}, + }; + + let tree; + await act(async () => { + tree = renderer.create(); + }); + expect(tree.toJSON().children[0].props.id).toBe('styleSettingTitleId'); + expect(tree.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/region_map/public/components/style_options.tsx b/src/plugins/region_map/public/components/style_options.tsx new file mode 100644 index 0000000000..434cdc5a96 --- /dev/null +++ b/src/plugins/region_map/public/components/style_options.tsx @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { IServiceSettings } from '../../../maps_legacy/public'; +import { NumberInputOption, SelectOption } from '../../../charts/public'; +import { RegionMapVisParams } from '../../../maps_legacy/public'; + +export type StyleOptionsProps = { + getServiceSettings: () => Promise; +} & VisOptionsProps; + +function StyleOptions(props: StyleOptionsProps) { + const { stateParams, vis, setValue } = props; + return ( + + +

+ +

+
+ + + + + +
+ ); +} + +export { StyleOptions }; diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index 4bfa410d01..5b0caca137 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -60,7 +60,6 @@ import { SharePluginStart } from '../../share/public'; /** @private */ export interface RegionMapVisualizationDependencies { http: CoreStart['http']; - notifications: CoreStart['notifications']; uiSettings: IUiSettingsClient; regionmapsConfig: RegionMapsConfig; getServiceSettings: () => Promise; @@ -123,9 +122,9 @@ export class RegionMapPlugin implements Plugin = { http: core.http, - notifications: core.notifications, uiSettings: core.uiSettings, regionmapsConfig: config as RegionMapsConfig, getServiceSettings: mapsLegacy.getServiceSettings, diff --git a/src/plugins/region_map/public/region_map_fn.js b/src/plugins/region_map/public/region_map_fn.js index 98e021e01a..a626ada5f1 100644 --- a/src/plugins/region_map/public/region_map_fn.js +++ b/src/plugins/region_map/public/region_map_fn.js @@ -47,7 +47,6 @@ export const createRegionMapFn = () => ({ }, fn(context, args) { const visConfig = JSON.parse(args.visConfig); - return { type: 'render', as: 'visualization', diff --git a/src/plugins/region_map/public/region_map_type.js b/src/plugins/region_map/public/region_map_type.js index b2ae14cfe4..9e2294a1fd 100644 --- a/src/plugins/region_map/public/region_map_type.js +++ b/src/plugins/region_map/public/region_map_type.js @@ -36,10 +36,67 @@ import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../charts/public'; import { Schemas } from '../../vis_default_editor/public'; import { ORIGIN } from '../../maps_legacy/public'; +import { getServices } from './services'; +import { DEFAULT_MAP_CHOICE } from '../common'; export function createRegionMapTypeDefinition(dependencies) { - const { uiSettings, regionmapsConfig, getServiceSettings, additionalOptions } = dependencies; + const { + http, + uiSettings, + regionmapsConfig, + getServiceSettings, + additionalOptions, + } = dependencies; + + const services = getServices(http); const visualization = createRegionMapVisualization(dependencies); + const diffArray = (arr1, arr2) => { + return arr1.concat(arr2).filter((item) => !arr1.includes(item) || !arr2.includes(item)); + }; + + const getCustomIndices = async () => { + try { + const result = await services.getCustomIndices(); + return result.resp; + } catch (e) { + return false; + } + }; + + const getJoinFields = async (indexName) => { + try { + const result = await services.getIndexMapping(indexName); + const properties = diffArray(Object.keys(result.resp[indexName].mappings.properties), [ + 'location', + ]); + return properties.map(function (property) { + return { + type: 'id', + name: property, + description: property, + }; + }); + } catch (e) { + return false; + } + }; + + const addSchemaToCustomLayer = async (customlayer) => { + const joinFields = await getJoinFields(customlayer.index); + const customLayerWithSchema = { + attribution: + 'Made with NaturalEarth', + created_at: '2017-04-26T17:12:15.978370', + format: 'geojson', + fields: joinFields, + id: customlayer.index, + meta: undefined, + name: customlayer.index, + origin: 'user-upload', + }; + + return customLayerWithSchema; + }; return { name: 'region_map', @@ -52,6 +109,7 @@ provided base maps, or add your own. Darker colors represent higher values.', icon: 'visMapRegion', visConfig: { defaults: { + layerChosenByUser: DEFAULT_MAP_CHOICE, legendPosition: 'bottomright', addTooltip: true, colorSchema: 'Yellow to Red', @@ -86,6 +144,7 @@ provided base maps, or add your own. Darker colors represent higher values.', collections: { colorSchemas: truncatedColorSchemas, vectorLayers: [], + customVectorLayers: [], tmsLayers: [], }, schemas: new Schemas([ @@ -127,6 +186,7 @@ provided base maps, or add your own. Darker colors represent higher values.', setup: async (vis) => { const serviceSettings = await getServiceSettings(); const tmsLayers = await serviceSettings.getTMSServices(); + vis.type.editorConfig.collections.tmsLayers = tmsLayers; if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) { vis.params.wms.selectedTmsLayer = tmsLayers[0]; @@ -135,8 +195,16 @@ provided base maps, or add your own. Darker colors represent higher values.', const vectorLayers = regionmapsConfig.layers.map( mapToLayerWithId.bind(null, ORIGIN.OPENSEARCH_DASHBOARDS_YML) ); + const customVectorLayers = regionmapsConfig.layers.map( + mapToLayerWithId.bind(null, ORIGIN.OPENSEARCH_DASHBOARDS_YML) + ); + const customIndices = await getCustomIndices(); + let selectedLayer = vectorLayers[0]; + let selectedCustomLayer = customVectorLayers[0]; let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null; + const selectedCustomJoinField = selectedCustomLayer ? selectedCustomLayer.fields[0] : null; + if (regionmapsConfig.includeOpenSearchMapsService) { const layers = await serviceSettings.getFileLayers(); const newLayers = layers @@ -144,6 +212,8 @@ provided base maps, or add your own. Darker colors represent higher values.', .filter( (layer) => !vectorLayers.some((vectorLayer) => vectorLayer.layerId === layer.layerId) ); + const promises = customIndices.map(addSchemaToCustomLayer); + const newCustomLayers = await Promise.all(promises); // backfill v1 manifest for now newLayers.forEach((layer) => { @@ -154,9 +224,27 @@ provided base maps, or add your own. Darker colors represent higher values.', } }); + newCustomLayers.forEach((layer) => { + if (layer.format === 'geojson') { + layer.format = { + type: 'geojson', + }; + layer.isEMS = false; + layer.layerId = layer.origin + '.' + layer.name; + } + }); + vis.type.editorConfig.collections.vectorLayers = [...vectorLayers, ...newLayers]; + vis.type.editorConfig.collections.customVectorLayers = [ + ...customVectorLayers, + ...newCustomLayers, + ]; [selectedLayer] = vis.type.editorConfig.collections.vectorLayers; + [selectedCustomLayer] = vis.type.editorConfig.collections.customVectorLayers; + vis.params.selectedCustomLayer = selectedCustomLayer; + vis.params.selectedCustomJoinField = selectedCustomJoinField; + selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null; if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) { @@ -169,6 +257,10 @@ provided base maps, or add your own. Darker colors represent higher values.', vis.params.selectedJoinField = selectedJoinField; } + vis.params.layerChosenByUser = vis.params.layerChosenByUser + ? vis.params.layerChosenByUser + : DEFAULT_MAP_CHOICE; + return vis; }, }; diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index d505015e8a..4580d99112 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -37,8 +37,10 @@ import { import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; import { mapTooltipProvider, ORIGIN, lazyLoadMapsLegacyModules } from '../../maps_legacy/public'; +import { DEFAULT_MAP_CHOICE } from '../common'; export function createRegionMapVisualization({ + http, regionmapsConfig, uiSettings, BaseMapsVisualization, @@ -54,8 +56,14 @@ export function createRegionMapVisualization({ async render(opensearchResponse, visParams) { getOpenSearchDashboardsLegacy().loadFontAwesome(); + this._choroplethLayer?.setLayerChosenByUser(visParams.layerChosenByUser); + this._choroplethLayer?.setVisParams(visParams); await super.render(opensearchResponse, visParams); + + // fetches geojson data if (this._choroplethLayer) { + this._choroplethLayer.setLayerChosenByUser(visParams.layerChosenByUser); + this._choroplethLayer.setVisParams(visParams); await this._choroplethLayer.whenDataLoaded(); } } @@ -75,7 +83,15 @@ export function createRegionMapVisualization({ }); } - const selectedLayer = await this._loadConfig(this._params.selectedLayer); + let selectedLayer; + if (DEFAULT_MAP_CHOICE === this._params.layerChosenByUser) { + selectedLayer = await this._loadConfig(this._params.selectedLayer); + this._params.selectedJoinField = selectedLayer.fields[0]; + } else { + selectedLayer = this._params.selectedCustomLayer; + this._params.selectedJoinField = this._params.selectedCustomJoinField; + } + if (!this._params.selectedJoinField && selectedLayer) { this._params.selectedJoinField = selectedLayer.fields[0]; } @@ -112,7 +128,6 @@ export function createRegionMapVisualization({ // These settings are stored in the URL and can be used to inject dirty display content. const { escape } = await import('lodash'); - if ( fileLayerConfig && (fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS @@ -140,8 +155,14 @@ export function createRegionMapVisualization({ async _updateParams() { await super._updateParams(); - - const selectedLayer = await this._loadConfig(this._params.selectedLayer); + let selectedLayer; + if (DEFAULT_MAP_CHOICE === this._params.layerChosenByUser) { + selectedLayer = await this._loadConfig(this._params.selectedLayer); + this._params.selectedJoinField = selectedLayer.fields[0]; + } else { + selectedLayer = this._params.selectedCustomLayer; + this._params.selectedJoinField = this._params.selectedCustomJoinField; + } if (!this._params.selectedJoinField && selectedLayer) { this._params.selectedJoinField = selectedLayer.fields[0]; @@ -159,6 +180,8 @@ export function createRegionMapVisualization({ const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); + this._choroplethLayer.setLayerChosenByUser(this._params.layerChosenByUser); + this._choroplethLayer.setJoinField(this._params.selectedJoinField.name); this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value); this._choroplethLayer.setLineWeight(this._params.outlineWeight); @@ -170,6 +193,7 @@ export function createRegionMapVisualization({ } async _updateChoroplethLayerForNewMetrics(name, attribution, showAllData, newMetrics) { + this._choroplethLayer.setLayerChosenByUser(this._params.layerChosenByUser); if ( this._choroplethLayer && this._choroplethLayer.canReuseInstanceForNewMetrics(name, showAllData, newMetrics) @@ -187,7 +211,10 @@ export function createRegionMapVisualization({ } async _recreateChoroplethLayer(name, attribution, showAllData) { - const selectedLayer = await this._loadConfig(this._params.selectedLayer); + const selectedLayer = + DEFAULT_MAP_CHOICE === this._params.layerChosenByUser + ? await this._loadConfig(this._params.selectedLayer) + : this._params.selectedCustomLayer; this._opensearchDashboardsMap.removeLayer(this._choroplethLayer); if (this._choroplethLayer) { @@ -199,7 +226,9 @@ export function createRegionMapVisualization({ selectedLayer.meta, selectedLayer, await getServiceSettings(), - (await lazyLoadMapsLegacyModules()).L + (await lazyLoadMapsLegacyModules()).L, + this._params.layerChosenByUser, + http ); } else { const { ChoroplethLayer } = await import('./choropleth_layer'); @@ -211,9 +240,12 @@ export function createRegionMapVisualization({ selectedLayer.meta, selectedLayer, await getServiceSettings(), - (await lazyLoadMapsLegacyModules()).L + (await lazyLoadMapsLegacyModules()).L, + this._params.layerChosenByUser, + http ); } + this._choroplethLayer.setLayerChosenByUser(this._params.layerChosenByUser); this._choroplethLayer.on('select', (event) => { const { rows, columns } = this._chartData; diff --git a/src/plugins/region_map/public/services.ts b/src/plugins/region_map/public/services.ts new file mode 100644 index 0000000000..dd26be3ccd --- /dev/null +++ b/src/plugins/region_map/public/services.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart, HttpFetchError } from 'opensearch-dashboards/public'; + +export interface Services { + getCustomIndices: () => Promise; + getIndexData: (indexName: string) => Promise; + getIndexMapping: (indexName: string) => Promise; +} + +export function getServices(http: CoreStart['http']): Services { + return { + getCustomIndices: async () => { + try { + const response = await http.post('../api/geospatial/_indices', { + body: JSON.stringify({ + index: '*-map', + }), + }); + return response; + } catch (e) { + return e; + } + }, + getIndexData: async (indexName: string) => { + try { + const response = await http.post('../api/geospatial/_search', { + body: JSON.stringify({ + index: indexName, + }), + }); + return response; + } catch (e) { + return e; + } + }, + getIndexMapping: async (indexName: string) => { + try { + const response = await http.post('../api/geospatial/_mappings', { + body: JSON.stringify({ + index: indexName, + }), + }); + return response; + } catch (e) { + return e; + } + }, + }; +} diff --git a/src/plugins/region_map/server/index.ts b/src/plugins/region_map/server/index.ts index 2e721f7175..5cc6167227 100644 --- a/src/plugins/region_map/server/index.ts +++ b/src/plugins/region_map/server/index.ts @@ -29,9 +29,9 @@ */ import { PluginConfigDescriptor } from 'opensearch-dashboards/server'; -import { CoreSetup } from 'src/core/server'; +import { PluginInitializerContext } from 'src/core/server'; import { configSchema, ConfigSchema } from '../config'; -import { getUiSettings } from './ui_settings'; +import { RegionMapPlugin } from './plugin'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -47,10 +47,8 @@ export const config: PluginConfigDescriptor = { ], }; -export const plugin = () => ({ - setup(core: CoreSetup) { - core.uiSettings.register(getUiSettings()); - }, +export function plugin(initializerContext: PluginInitializerContext) { + return new RegionMapPlugin(initializerContext); +} - start() {}, -}); +export { RegionMapPluginSetup, RegionMapPluginStart } from './types'; diff --git a/src/plugins/region_map/server/plugin.js b/src/plugins/region_map/server/plugin.js new file mode 100644 index 0000000000..14aeb0801d --- /dev/null +++ b/src/plugins/region_map/server/plugin.js @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpensearchService } from './services'; +import { opensearch } from '../server/routes'; +import { getUiSettings } from './ui_settings'; + +export class RegionMapPlugin { + constructor(initializerContext) { + this.logger = initializerContext.logger.get(); + } + + async setup(core) { + const opensearchClient = core.opensearch.legacy.createClient('opensearch'); + + // Initialize services + const opensearchService = new OpensearchService(opensearchClient); + + // Register server side APIs + const router = core.http.createRouter(); + core.uiSettings.register(getUiSettings()); + opensearch(opensearchService, router); + + return {}; + } + + async start() { + return {}; + } + + async stop() {} +} diff --git a/src/plugins/region_map/server/routes/index.ts b/src/plugins/region_map/server/routes/index.ts new file mode 100644 index 0000000000..bdbd7bdca8 --- /dev/null +++ b/src/plugins/region_map/server/routes/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import opensearch from './opensearch'; + +export { opensearch }; diff --git a/src/plugins/region_map/server/routes/opensearch.ts b/src/plugins/region_map/server/routes/opensearch.ts new file mode 100644 index 0000000000..2d67bc9c4e --- /dev/null +++ b/src/plugins/region_map/server/routes/opensearch.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; + +// eslint-disable-next-line import/no-default-export +export default function (services, router) { + router.post( + { + path: '/api/geospatial/_indices', + validate: { + body: schema.object({ + index: schema.string(), + }), + }, + }, + services.getIndex + ); + + router.post( + { + path: '/api/geospatial/_search', + validate: { + body: schema.object({ + index: schema.string(), + }), + }, + }, + services.search + ); + + router.post( + { + path: '/api/geospatial/_mappings', + validate: { + body: schema.object({ + index: schema.string(), + }), + }, + }, + services.getMappings + ); +} diff --git a/src/plugins/region_map/server/services/index.js b/src/plugins/region_map/server/services/index.js new file mode 100644 index 0000000000..63a48fdfdd --- /dev/null +++ b/src/plugins/region_map/server/services/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import OpensearchService from './opensearch_service'; + +export { OpensearchService }; diff --git a/src/plugins/region_map/server/services/opensearch_service.js b/src/plugins/region_map/server/services/opensearch_service.js new file mode 100644 index 0000000000..e9be890b72 --- /dev/null +++ b/src/plugins/region_map/server/services/opensearch_service.js @@ -0,0 +1,88 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export default class OpensearchService { + constructor(esDriver) { + this.esDriver = esDriver; + } + + getMappings = async (context, req, res) => { + try { + const { index } = req.body; + const { callAsCurrentUser } = this.esDriver.asScoped(req); + const mappings = await callAsCurrentUser('indices.getMapping', { index }); + return res.ok({ + body: { + ok: true, + resp: mappings, + }, + }); + } catch (err) { + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + }; + + search = async (context, req, res) => { + try { + const { query, index, size } = req.body; + const params = { index, size, body: query }; + const { callAsCurrentUser } = this.esDriver.asScoped(req); + const results = await callAsCurrentUser('search', params); + return res.ok({ + body: { + ok: true, + resp: results, + }, + }); + } catch (err) { + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + }; + + getIndex = async (context, req, res) => { + try { + const { index } = req.body; + const { callAsCurrentUser } = this.esDriver.asScoped(req); + const indices = await callAsCurrentUser('cat.indices', { + index, + format: 'json', + h: 'health,index,status', + }); + return res.ok({ + body: { + ok: true, + resp: indices, + }, + }); + } catch (err) { + // Opensearch throws an index_not_found_exception which we'll treat as a success + if (err.statusCode === 404) { + return res.ok({ + body: { + ok: false, + resp: [], + }, + }); + } else { + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + } + }; +} diff --git a/src/plugins/region_map/server/types.ts b/src/plugins/region_map/server/types.ts new file mode 100644 index 0000000000..b604aaf65b --- /dev/null +++ b/src/plugins/region_map/server/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RegionMapPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RegionMapPluginStart {}