Enable users to select custom vector map for visualization (#1718)

enable users to select custom vector map for visualization

Signed-off-by: Shivam Dhar <dhshivam@amazon.com>
This commit is contained in:
Shivam Dhar 2022-06-16 15:54:51 -07:00 committed by GitHub
parent 2a159e88e0
commit dfbfec47e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2477 additions and 185 deletions

View File

@ -1,2 +1,2 @@
build
target
target

View File

@ -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/',
'<rootDir>/src/plugins/maps_legacy',
'<rootDir>/src/plugins/region_map',
],
modulePathIgnorePatterns: ['__fixtures__/', 'target/', '<rootDir>/src/plugins/maps_legacy'],
testEnvironment: 'jest-environment-jsdom',
testMatch: ['**/*.test.{js,mjs,ts,tsx}'],
testPathIgnorePatterns: [

View File

@ -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';

View File

@ -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 };

View File

@ -11,7 +11,8 @@
"mapsLegacy",
"opensearchDashboardsLegacy",
"data",
"share"
"share",
"opensearchDashboardsReact"
],
"requiredBundles": [
"opensearchDashboardsUtils",

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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`] = `
<div
className="euiPanel euiPanel--paddingSmall euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow"
>
<h2
className="euiTitle euiTitle--xsmall"
id="styleSettingTitleId"
>
<span>
Style settings
</span>
</h2>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiFormRow euiFormRow--fullWidth euiFormRow--compressed"
id="colorSchemaId-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="colorSchemaId"
>
Color schema
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--fullWidth euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<select
className="euiSelect euiSelect--fullWidth euiSelect--compressed"
id="colorSchemaId"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onMouseUp={[Function]}
value={Object {}}
>
<option
disabled={true}
hidden={true}
value="EMPTY_VALUE"
>
</option>
</select>
<div
className="euiFormControlLayoutIcons euiFormControlLayoutIcons--right"
>
<span
className="euiFormControlLayoutCustomIcon"
>
<span
aria-hidden="true"
className="euiFormControlLayoutCustomIcon__icon"
data-euiicon-type="arrowDown"
size="s"
/>
</span>
</div>
</div>
</div>
</div>
</div>
<div
className="euiFormRow euiFormRow--fullWidth euiFormRow--compressed"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
Border thickness
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--fullWidth euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldNumber euiFieldNumber--fullWidth euiFieldNumber--compressed"
id="generated-id"
min={0}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="number"
value={Object {}}
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -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<IServiceSettings>;
} & VisOptionsProps<RegionMapVisParams>;
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 (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="regionMap.visParams.layerSettingsTitle"
defaultMessage="Layer settings"
/>
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGroup id="defaultMapSelection" direction="column">
<EuiFlexItem grow={false}>
<SelectOption
id="regionMapOptionsSelectLayer"
label={i18n.translate('regionMap.visParams.vectorMapLabel', {
defaultMessage: 'Vector map',
})}
options={vectorLayerOptions}
paramName="selectedLayer"
value={stateParams.selectedLayer && stateParams.selectedLayer.layerId}
setValue={setLayer}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SelectOption
id="regionMapOptionsSelectJoinField"
label={i18n.translate('regionMap.visParams.joinFieldLabel', {
defaultMessage: 'Join field',
})}
options={fieldOptions}
paramName="selectedJoinField"
value={stateParams.selectedJoinField && stateParams.selectedJoinField.name}
setValue={setField}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<SwitchOption
label={i18n.translate('regionMap.visParams.displayWarningsLabel', {
defaultMessage: 'Display warnings',
})}
tooltip={i18n.translate('regionMap.visParams.switchWarningsTipText', {
defaultMessage:
'Turns on/off warnings. When turned on, warning will be shown for each term that cannot be matched to a shape in the vector layer based on the join field. When turned off, these warnings will be turned off.',
})}
paramName="isDisplayWarning"
value={stateParams.isDisplayWarning}
setValue={setValue}
/>
<SwitchOption
label={i18n.translate('regionMap.visParams.showAllShapesLabel', {
defaultMessage: 'Show all shapes',
})}
tooltip={i18n.translate('regionMap.visParams.turnOffShowingAllShapesTipText', {
defaultMessage:
'Turning this off only shows the shapes that were matched with a corresponding term.',
})}
paramName="showAllShapes"
value={stateParams.showAllShapes}
setValue={setValue}
/>
</EuiPanel>
);
}
export { DefaultMapOptions };

View File

@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
.mapChoiceGroup {
font-size: small;
}

View File

@ -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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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(<MapChoiceOptions stateParams={stateParams} vis={vis} {...props} />);
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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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(<MapChoiceOptions stateParams={stateParams} vis={vis} {...props} />);
const defaultVectorSelection = screen.getByTestId('defaultVectorMap');
const customVectorSelection = screen.getByTestId('customVectorMap');
fireEvent.click(customVectorSelection);
await expect(customVectorSelection).toBeChecked;
await expect(defaultVectorSelection).not.toBeChecked;
});
});

View File

@ -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<IServiceSettings>;
} & VisOptionsProps<RegionMapVisParams>;
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 (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="regionMap.visParams.layerSettingsTitle"
defaultMessage="Layer settings"
/>
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText size="xs">
<strong>
<EuiTextColor color="default">Choose a vector map layer</EuiTextColor>
</strong>
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup className="mapChoiceGroup">
<EuiFlexItem>
<EuiCheckableCard
id="defaultVectorMap"
data-test-subj="defaultVectorMap"
label="Default vector map"
name="defaultVectorMap"
value="default"
checked={DEFAULT_MAP_CHOICE === stateParams.layerChosenByUser}
onChange={selectDefaultVectorMap}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCheckableCard
id="customVectorMap"
data-test-subj="customVectorMap"
label="Custom vector map"
name="customVectorMap"
value="custom"
checked={CUSTOM_MAP_CHOICE === stateParams.layerChosenByUser}
onChange={selectCustomVectorMap}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
{DEFAULT_MAP_CHOICE === stateParams.layerChosenByUser ? (
<EuiFlexGroup id="defaultMapSelection" direction="column">
<EuiFlexItem grow={false}>
<SelectOption
id="regionMapOptionsSelectLayer"
label={i18n.translate('regionMap.visParams.vectorMapLabel', {
defaultMessage: 'Vector map',
})}
options={vectorLayerOptions}
paramName="selectedLayer"
value={stateParams.selectedLayer && stateParams.selectedLayer.layerId}
setValue={setLayer}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SelectOption
id="regionMapOptionsSelectJoinField"
label={i18n.translate('regionMap.visParams.joinFieldLabel', {
defaultMessage: 'Join field',
})}
options={fieldOptions}
paramName="selectedJoinField"
value={stateParams.selectedJoinField && stateParams.selectedJoinField.name}
setValue={setField}
/>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup id="customMapSelection" direction="column">
<EuiFlexItem grow={false}>
<SelectOption
id="regionMapOptionsCustomSelectLayer"
label="Vector map"
options={customVectorLayerOptions}
paramName="selectedCustomLayer"
value={stateParams.selectedCustomLayer && stateParams.selectedCustomLayer.layerId}
setValue={setCustomLayer}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SelectOption
id="regionMapOptionsCustomSelectJoinField"
label="Join field"
options={customFieldOptions}
paramName="selectedCustomJoinField"
value={
stateParams.selectedCustomJoinField && stateParams.selectedCustomJoinField.name
}
setValue={setCustomJoinField}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiSpacer size="m" />
<SwitchOption
label={i18n.translate('regionMap.visParams.displayWarningsLabel', {
defaultMessage: 'Display warnings',
})}
tooltip={i18n.translate('regionMap.visParams.switchWarningsTipText', {
defaultMessage:
'Turns on/off warnings. When turned on, warning will be shown for each term that cannot be matched to a shape in the vector layer based on the join field. When turned off, these warnings will be turned off.',
})}
paramName="isDisplayWarning"
value={stateParams.isDisplayWarning}
setValue={setValue}
/>
<SwitchOption
label={i18n.translate('regionMap.visParams.showAllShapesLabel', {
defaultMessage: 'Show all shapes',
})}
tooltip={i18n.translate('regionMap.visParams.turnOffShowingAllShapesTipText', {
defaultMessage:
'Turning this off only shows the shapes that were matched with a corresponding term.',
})}
paramName="showAllShapes"
value={stateParams.showAllShapes}
setValue={setValue}
/>
</EuiPanel>
);
}
export { MapChoiceOptions };

View File

@ -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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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(<RegionMapOptions stateParams={stateParams} vis={vis} {...props} />);
});
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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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(<RegionMapOptions stateParams={stateParams} vis={vis} {...props} />);
});
expect(tree.toJSON().props.id).toBe('customMapOption');
expect(tree.toJSON()).toMatchSnapshot();
});
});

View File

@ -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<IServiceSettings>;
} & VisOptionsProps<RegionMapVisParams>;
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 (
<>
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="regionMap.visParams.layerSettingsTitle"
defaultMessage="Layer settings"
/>
</h2>
</EuiTitle>
if (customVectorLayerOptions.length === 0) {
return (
<div id="defaultMapOption">
<DefaultMapOptions {...props} />
<EuiSpacer size="s" />
<SelectOption
id="regionMapOptionsSelectLayer"
label={i18n.translate('regionMap.visParams.vectorMapLabel', {
defaultMessage: 'Vector map',
})}
options={vectorLayerOptions}
paramName="selectedLayer"
value={stateParams.selectedLayer && stateParams.selectedLayer.layerId}
setValue={setLayer}
/>
<SelectOption
id="regionMapOptionsSelectJoinField"
label={i18n.translate('regionMap.visParams.joinFieldLabel', {
defaultMessage: 'Join field',
})}
options={fieldOptions}
paramName="selectedJoinField"
value={stateParams.selectedJoinField && stateParams.selectedJoinField.name}
setValue={setField}
/>
<SwitchOption
label={i18n.translate('regionMap.visParams.displayWarningsLabel', {
defaultMessage: 'Display warnings',
})}
tooltip={i18n.translate('regionMap.visParams.switchWarningsTipText', {
defaultMessage:
'Turns on/off warnings. When turned on, warning will be shown for each term that cannot be matched to a shape in the vector layer based on the join field. When turned off, these warnings will be turned off.',
})}
paramName="isDisplayWarning"
value={stateParams.isDisplayWarning}
setValue={setValue}
/>
<SwitchOption
label={i18n.translate('regionMap.visParams.showAllShapesLabel', {
defaultMessage: 'Show all shapes',
})}
tooltip={i18n.translate('regionMap.visParams.turnOffShowingAllShapesTipText', {
defaultMessage:
'Turning this off only shows the shapes that were matched with a corresponding term.',
})}
paramName="showAllShapes"
value={stateParams.showAllShapes}
setValue={setValue}
/>
</EuiPanel>
<EuiSpacer size="s" />
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="regionMap.visParams.styleSettingsLabel"
defaultMessage="Style settings"
/>
</h2>
</EuiTitle>
<StyleOptions {...props} />
<EuiSpacer size="s" />
<SelectOption
label={i18n.translate('regionMap.visParams.colorSchemaLabel', {
defaultMessage: 'Color schema',
})}
options={vis.type.editorConfig.collections.colorSchemas}
paramName="colorSchema"
value={stateParams.colorSchema}
setValue={setValue}
/>
<NumberInputOption
label={i18n.translate('regionMap.visParams.outlineWeightLabel', {
defaultMessage: 'Border thickness',
})}
min={0}
paramName="outlineWeight"
value={stateParams.outlineWeight}
setValue={setValue}
/>
</EuiPanel>
<EuiSpacer size="s" />
<WmsOptions {...props} />
</>
);
<WmsOptions {...props} />
</div>
);
} else {
return (
<div id="customMapOption">
<MapChoiceOptions {...props} />
<EuiSpacer size="s" />
<StyleOptions {...props} />
<EuiSpacer size="s" />
<WmsOptions {...props} />
</div>
);
}
}
export { RegionMapOptions };

View File

@ -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(<StyleOptions stateParams={stateParams} vis={vis} {...props} />);
});
expect(tree.toJSON().children[0].props.id).toBe('styleSettingTitleId');
expect(tree.toJSON()).toMatchSnapshot();
});
});

View File

@ -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<IServiceSettings>;
} & VisOptionsProps<RegionMapVisParams>;
function StyleOptions(props: StyleOptionsProps) {
const { stateParams, vis, setValue } = props;
return (
<EuiPanel paddingSize="s">
<EuiTitle size="xs" id="styleSettingTitleId">
<h2>
<FormattedMessage
id="regionMap.visParams.styleSettingsLabel"
defaultMessage="Style settings"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<SelectOption
label={i18n.translate('regionMap.visParams.colorSchemaLabel', {
defaultMessage: 'Color schema',
})}
options={vis.type.editorConfig.collections.colorSchemas}
paramName="colorSchema"
value={stateParams.colorSchema}
setValue={setValue}
id="colorSchemaId"
/>
<NumberInputOption
label={i18n.translate('regionMap.visParams.outlineWeightLabel', {
defaultMessage: 'Border thickness',
})}
min={0}
paramName="outlineWeight"
value={stateParams.outlineWeight}
setValue={setValue}
id="borderThicknessId"
/>
</EuiPanel>
);
}
export { StyleOptions };

View File

@ -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<IServiceSettings>;
@ -123,9 +122,9 @@ export class RegionMapPlugin implements Plugin<RegionMapPluginSetup, RegionMapPl
// ideally constrain regionmap config updates to occur only from this plugin
...mapsLegacy.config.regionmap,
};
const visualizationDependencies: Readonly<RegionMapVisualizationDependencies> = {
http: core.http,
notifications: core.notifications,
uiSettings: core.uiSettings,
regionmapsConfig: config as RegionMapsConfig,
getServiceSettings: mapsLegacy.getServiceSettings,

View File

@ -47,7 +47,6 @@ export const createRegionMapFn = () => ({
},
fn(context, args) {
const visConfig = JSON.parse(args.visConfig);
return {
type: 'render',
as: 'visualization',

View File

@ -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:
'<a rel="noreferrer noopener" href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a>',
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;
},
};

View File

@ -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;

View File

@ -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<undefined | HttpFetchError>;
getIndexData: (indexName: string) => Promise<undefined | HttpFetchError>;
getIndexMapping: (indexName: string) => Promise<undefined | HttpFetchError>;
}
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;
}
},
};
}

View File

@ -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<ConfigSchema> = {
exposeToBrowser: {
@ -47,10 +47,8 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
],
};
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';

View File

@ -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() {}
}

View File

@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import opensearch from './opensearch';
export { opensearch };

View File

@ -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
);
}

View File

@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import OpensearchService from './opensearch_service';
export { OpensearchService };

View File

@ -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,
},
});
}
}
};
}

View File

@ -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 {}