diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 8b94c925..4e1ef1ac 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -33,7 +33,7 @@ echo 'src/plugins/custom_import_map/*' >> .git/info/sparse-checkout git config core.sparseCheckout true git checkout main ``` -6. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/custom_import_map`. +6. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/src/plugins/custom_import_map`. Ultimately, your directory structure should look like this: diff --git a/README.md b/README.md index 56de15ef..04b58295 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Dashboards-maps is a frontend plugin that helps you in uploading custom GeoJSON * [Project Website](https://opensearch.org/) * [Downloads](https://opensearch.org/downloads.html) * [Documentation](https://opensearch.org/docs/latest/) +* [Developer Guide](DEVELOPER_GUIDE.md) * Need help? Try [Forums](https://discuss.opendistrocommunity.dev/) * [Project Principles](https://opensearch.org/#principles) * [Contributing to OpenSearch](CONTRIBUTING.md) diff --git a/src/plugins/custom_import_map/babel.config.js b/src/plugins/custom_import_map/babel.config.js index e5f3822c..fa274ac8 100644 --- a/src/plugins/custom_import_map/babel.config.js +++ b/src/plugins/custom_import_map/babel.config.js @@ -4,15 +4,15 @@ */ module.exports = { - presets: [ - require('@babel/preset-env'), - require('@babel/preset-react'), - require('@babel/preset-typescript'), - ], - plugins: [ - require('@babel/plugin-proposal-class-properties'), - require('@babel/plugin-proposal-object-rest-spread'), - ['@babel/plugin-transform-modules-commonjs', { allowTopLevelThis: true }], - [require('@babel/plugin-transform-runtime'), { regenerator: true }], - ], - }; + presets: [ + require('@babel/preset-env'), + require('@babel/preset-react'), + require('@babel/preset-typescript'), + ], + plugins: [ + require('@babel/plugin-proposal-class-properties'), + require('@babel/plugin-proposal-object-rest-spread'), + ['@babel/plugin-transform-modules-commonjs', { allowTopLevelThis: true }], + [require('@babel/plugin-transform-runtime'), { regenerator: true }], + ], +}; diff --git a/src/plugins/custom_import_map/common/index.ts b/src/plugins/custom_import_map/common/index.ts index 82114374..109ca6e7 100644 --- a/src/plugins/custom_import_map/common/index.ts +++ b/src/plugins/custom_import_map/common/index.ts @@ -3,7 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fromMBtoBytes } from "./util"; -import { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, PLUGIN_NAME } from "./constants/shared"; +import { fromMBtoBytes } from './util'; +import { + MAX_FILE_PAYLOAD_SIZE, + MAX_FILE_PAYLOAD_SIZE_IN_MB, + PLUGIN_ID, + PLUGIN_NAME, +} from './constants/shared'; -export { fromMBtoBytes, MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, PLUGIN_NAME }; +export { + fromMBtoBytes, + MAX_FILE_PAYLOAD_SIZE, + MAX_FILE_PAYLOAD_SIZE_IN_MB, + PLUGIN_ID, + PLUGIN_NAME, +}; diff --git a/src/plugins/custom_import_map/common/util.ts b/src/plugins/custom_import_map/common/util.ts index dc898855..afc2ef97 100644 --- a/src/plugins/custom_import_map/common/util.ts +++ b/src/plugins/custom_import_map/common/util.ts @@ -4,5 +4,5 @@ */ export const fromMBtoBytes = (sizeInMB: number) => { - return sizeInMB * 1024 * 1024; -} + return sizeInMB * 1024 * 1024; +}; diff --git a/src/plugins/custom_import_map/package.json b/src/plugins/custom_import_map/package.json index 00ecf7f9..ed63c87b 100644 --- a/src/plugins/custom_import_map/package.json +++ b/src/plugins/custom_import_map/package.json @@ -5,7 +5,9 @@ "build": "yarn plugin-helpers build", "plugin-helpers": "node ../../../../scripts/plugin_helpers", "osd": "node ../../../../scripts/osd", - "lint": "node ../../../../scripts/eslint .", + "lint": "yarn run lint:es && yarn run lint:style", + "lint:es": "node ../../../../scripts/eslint", + "lint:style": "node ../../../../scripts/stylelint", "test:jest": "TZ=UTC ../../../../node_modules/.bin/jest --config ./test/jest.config.js" }, "husky": { diff --git a/src/plugins/custom_import_map/public/components/__snapshots__/show_error_modal.test.tsx.snap b/src/plugins/custom_import_map/public/components/__snapshots__/show_error_modal.test.tsx.snap new file mode 100644 index 00000000..29791d94 --- /dev/null +++ b/src/plugins/custom_import_map/public/components/__snapshots__/show_error_modal.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`display error modal renders the error modal based on the props passed 1`] = ` +
+ +
+`; diff --git a/src/plugins/custom_import_map/public/components/show_error_modal.test.tsx b/src/plugins/custom_import_map/public/components/show_error_modal.test.tsx new file mode 100644 index 00000000..10a0d2b4 --- /dev/null +++ b/src/plugins/custom_import_map/public/components/show_error_modal.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from 'react'; +import { ShowErrorModal } from './show_error_modal'; +import renderer, { act } from 'react-test-renderer'; +import { fireEvent } from '@testing-library/dom'; +import { screen, render } from '@testing-library/react'; + +describe('display error modal', () => { + const props = { + modalTitle: 'testModalTitle', + modalBody: 'testModalBody', + buttonText: 'testButtonText', + }; + + it('renders the error modal based on the props passed', async () => { + let tree; + await act(async () => { + tree = renderer.create(); + }); + expect(tree.toJSON().props.id).toBe('showModalOption'); + expect(tree.toJSON()).toMatchSnapshot(); + }); + + it('renders the error modal when showModal button is clicked', () => { + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + const closeButton = screen.getByTestId('closeModal'); + fireEvent.click(closeButton); + }); +}); diff --git a/src/plugins/custom_import_map/public/components/show_error_modal.tsx b/src/plugins/custom_import_map/public/components/show_error_modal.tsx new file mode 100644 index 00000000..bfeed956 --- /dev/null +++ b/src/plugins/custom_import_map/public/components/show_error_modal.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiCodeBlock, +} from '@elastic/eui'; + +export interface ShowErrorModalProps { + modalTitle: string; + modalBody: string; + buttonText: string; +} + +const ShowErrorModal = (props: ShowErrorModalProps) => { + const [isModalVisible, setIsModalVisible] = useState(false); + + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); + + let modal; + + if (isModalVisible) { + modal = ( + + + +

{props.modalTitle}

+
+
+ + + {props.modalBody} + + + + + Close + + +
+ ); + } + + return ( +
+ {props.buttonText} + {modal} +
+ ); +}; + +export { ShowErrorModal }; diff --git a/src/plugins/custom_import_map/public/components/vector_upload_options.test.tsx b/src/plugins/custom_import_map/public/components/vector_upload_options.test.tsx index c4080182..86439e30 100644 --- a/src/plugins/custom_import_map/public/components/vector_upload_options.test.tsx +++ b/src/plugins/custom_import_map/public/components/vector_upload_options.test.tsx @@ -13,17 +13,21 @@ import * as serviceApiCalls from '../services'; jest.mock('../../../../../../src/plugins/opensearch_dashboards_react/public', () => ({ useOpenSearchDashboards: jest.fn().mockReturnValue({ services: { - http: { post: () => {Promise.resolve({});} }, - notifications: { toasts: { addSuccess: jest.fn(), addDanger: jest.fn(), addWarning: jest.fn() } }, + http: { + post: () => { + Promise.resolve({}); + }, + }, + notifications: { + toasts: { addSuccess: jest.fn(), addDanger: jest.fn(), addWarning: jest.fn() }, + }, }, }), toMountPoint: jest.fn().mockReturnValue({}), })); - describe('vector_upload_options', () => { - const props = { - }; + const props = {}; const getIndexResponseWhenIndexIsNotPresent = { ok: false, @@ -104,7 +108,7 @@ describe('vector_upload_options', () => { await expect(tree.findAllByText(message)).toBeTruthy(); }; - const addUserInputToDOM = async () => { + const addUserInputToDOM = async () => { const jsonData = { type: 'FeatureCollection', name: 'sample', @@ -120,7 +124,7 @@ describe('vector_upload_options', () => { const indexName = screen.getByTestId('customIndex'); fireEvent.change(indexName, { target: { value: 'sample' } }); const uploader = getByTestId('filePicker'); - + const str = JSON.stringify([jsonData]); const blob = new Blob([str]); const file = new File([blob], 'sample.json', { type: 'application/JSON' }); @@ -152,25 +156,34 @@ describe('vector_upload_options', () => { it('renders the VectorUploadOptions component with error message when index name has upper case letters', async () => { try { - await vectorUploadOptionsWithIndexNameRendererUtil('ABC', 'Upper case letters are not allowed'); + await vectorUploadOptionsWithIndexNameRendererUtil( + 'ABC', + 'Upper case letters are not allowed' + ); } catch (err) {} }); it('renders the VectorUploadOptions component with error message when index name has special characters', async () => { try { - await vectorUploadOptionsWithIndexNameRendererUtil('a#bc', 'Special characters are not allowed.'); + await vectorUploadOptionsWithIndexNameRendererUtil( + 'a#bc', + 'Special characters are not allowed.' + ); } catch (err) {} }); it('renders the VectorUploadOptions component with error message when index name has -map as suffix', async () => { try { - await vectorUploadOptionsWithIndexNameRendererUtil('sample-map', "Map name can't end with -map."); + await vectorUploadOptionsWithIndexNameRendererUtil( + 'sample-map', + "Map name can't end with -map." + ); } catch (err) {} }); it('renders the VectorUploadOptions component when we have successfully indexed all the data', async () => { addUserInputToDOM(); - console.log("test case for successfully indexed file data"); + console.log('test case for successfully indexed file data'); const button = screen.getByRole('button', { name: 'import-file-button' }); jest.spyOn(serviceApiCalls, 'getIndex').mockImplementation(() => { return Promise.resolve(getIndexResponseWhenIndexIsNotPresent); @@ -185,7 +198,7 @@ describe('vector_upload_options', () => { it('renders the VectorUploadOptions component when we have partial failures during indexing', async () => { addUserInputToDOM(); - console.log("test case for partial failures during indexing"); + console.log('test case for partial failures during indexing'); const button = screen.getByRole('button', { name: 'import-file-button' }); jest.spyOn(serviceApiCalls, 'getIndex').mockImplementation(() => { return Promise.resolve(getIndexResponseWhenIndexIsNotPresent); @@ -200,7 +213,7 @@ describe('vector_upload_options', () => { it('renders the VectorUploadOptions component when all the documents fail to index', async () => { addUserInputToDOM(); - console.log("test case for failed documents"); + console.log('test case for failed documents'); const button = screen.getByRole('button', { name: 'import-file-button' }); jest.spyOn(serviceApiCalls, 'getIndex').mockImplementation(() => { return Promise.resolve(getIndexResponseWhenIndexIsNotPresent); @@ -215,7 +228,7 @@ describe('vector_upload_options', () => { it('renders the VectorUploadOptions component when postGeojson call fails', async () => { addUserInputToDOM(); - console.log("test case for call failure to postGeojson"); + console.log('test case for call failure to postGeojson'); const button = screen.getByRole('button', { name: 'import-file-button' }); jest.spyOn(serviceApiCalls, 'getIndex').mockImplementation(() => { return Promise.resolve(getIndexResponseWhenIndexIsNotPresent); @@ -230,7 +243,7 @@ describe('vector_upload_options', () => { it('renders the VectorUploadOptions component when getIndex returns a duplicate index', async () => { addUserInputToDOM(); - console.log("test case for duplicate index check"); + console.log('test case for duplicate index check'); const button = screen.getByRole('button', { name: 'import-file-button' }); jest.spyOn(serviceApiCalls, 'getIndex').mockImplementation(() => { return Promise.resolve(getIndexResponseWhenIndexIsPresent); diff --git a/src/plugins/custom_import_map/public/components/vector_upload_options.tsx b/src/plugins/custom_import_map/public/components/vector_upload_options.tsx index 9e839de6..6f573f39 100644 --- a/src/plugins/custom_import_map/public/components/vector_upload_options.tsx +++ b/src/plugins/custom_import_map/public/components/vector_upload_options.tsx @@ -18,8 +18,12 @@ import { EuiFormRow, } from '@elastic/eui'; import { getIndex, postGeojson } from '../services'; +import { ShowErrorModal } from './show_error_modal'; import { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB } from '../../common'; -import { toMountPoint, useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; +import { + toMountPoint, + useOpenSearchDashboards, +} from '../../../../../../src/plugins/opensearch_dashboards_react/public'; import { RegionMapOptionsProps } from '../../../../../../src/plugins/region_map/public'; const VectorUploadOptions = (props: RegionMapOptionsProps) => { @@ -56,7 +60,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { }; const validateIndexName = (typedIndexName: string, isIndexNameWithSuffix: boolean) => { - let error = []; + const error = []; const errorIndexNameDiv = fetchElementByName('errorIndexName'); // check for presence of index name entered by the user @@ -109,7 +113,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { ); setLoading(false); return false; - } + } if (files[0].size === 0) { notifications.toasts.addDanger( 'Error. File does not contain valid features. Check your json format.' @@ -130,7 +134,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { fileData = await files[0].text(); } return fileData; - } + }; const handleSubmit = async () => { // show import button as loading @@ -164,6 +168,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { const successfullyIndexedRecordCount = result.resp.success; const failedToIndexRecordCount = result.resp.failure; const totalRecords = result.resp.total; + if (successfullyIndexedRecordCount === totalRecords) { notifications.toasts.addSuccess( 'Successfully added ' + successfullyIndexedRecordCount + ' features to ' + indexName @@ -172,17 +177,27 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { } if (successfullyIndexedRecordCount > 0 && failedToIndexRecordCount > 0) { - const title = 'Partially indexed ' + successfullyIndexedRecordCount + ' of ' + - totalRecords + ' features in ' + indexName; + const title = + 'Partially indexed ' + + successfullyIndexedRecordCount + + ' of ' + + totalRecords + + ' features in ' + + indexName; + const showModalProps = { + modalTitle: 'Error Details', + modalBody: JSON.stringify(result.resp.failures), + buttonText: 'View error details', + }; notifications.toasts.addDanger({ - title: title, + title, iconType: 'alert', text: toMountPoint(

There were {failedToIndexRecordCount} errors processing the custom map.

- View error details +
@@ -195,7 +210,7 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => { 'Error. File does not contain valid features. Check your json format.' ); } - } + }; const uploadGeojson = async (indexName: string, fileData: object) => { const bodyData = { @@ -253,12 +268,8 @@ const VectorUploadOptions = (props: RegionMapOptionsProps) => {

- - Formats accepted: .json, .geojson - - - Max size: 25 MB - + Formats accepted: .json, .geojson + Max size: 25 MB Coordinates must be in EPSG:4326 coordinate reference system. diff --git a/src/plugins/custom_import_map/public/plugin.tsx b/src/plugins/custom_import_map/public/plugin.tsx index c137883a..068e0602 100644 --- a/src/plugins/custom_import_map/public/plugin.tsx +++ b/src/plugins/custom_import_map/public/plugin.tsx @@ -9,28 +9,25 @@ import { CoreSetup, CoreStart, Plugin } from '../../../../../src/core/public'; import { CustomImportMapPluginSetup, CustomImportMapPluginStart, - AppPluginSetupDependencies + AppPluginSetupDependencies, } from './types'; import { RegionMapVisualizationDependencies } from '../../../../../src/plugins/region_map/public'; import { VectorUploadOptions } from './components/vector_upload_options'; export class CustomImportMapPlugin implements Plugin { - public setup(core: CoreSetup, { regionMap }: AppPluginSetupDependencies): CustomImportMapPluginSetup { - + public setup( + core: CoreSetup, + { regionMap }: AppPluginSetupDependencies + ): CustomImportMapPluginSetup { regionMap.addOptionTab({ name: 'controls', - title: i18n.translate( - 'regionMap.mapVis.regionMapEditorConfig.controlTabs.controlsTitle', - { - defaultMessage: 'Import Vector Map', - } - ), - editor: (props : RegionMapVisualizationDependencies) => ( - - ), - }) - + title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.controlTabs.controlsTitle', { + defaultMessage: 'Import Vector Map', + }), + editor: (props: RegionMapVisualizationDependencies) => , + }); + // Return methods that should be available to other plugins return {}; } diff --git a/src/plugins/custom_import_map/public/services.ts b/src/plugins/custom_import_map/public/services.ts index aeedf84b..f2312fc8 100644 --- a/src/plugins/custom_import_map/public/services.ts +++ b/src/plugins/custom_import_map/public/services.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart } from "../../../../../src/core/public"; +import { CoreStart } from '../../../../../src/core/public'; export const postGeojson = async (requestBody: any, http: CoreStart['http']) => { try { diff --git a/src/plugins/custom_import_map/public/types.ts b/src/plugins/custom_import_map/public/types.ts index f1266b7d..593f1b10 100644 --- a/src/plugins/custom_import_map/public/types.ts +++ b/src/plugins/custom_import_map/public/types.ts @@ -13,9 +13,9 @@ export interface CustomImportMapPluginSetup {} export interface CustomImportMapPluginStart {} export interface AppPluginStartDependencies { - navigation: NavigationPublicPluginStart; + navigation: NavigationPublicPluginStart; } export interface AppPluginSetupDependencies { - regionMap: RegionMapPluginSetup; + regionMap: RegionMapPluginSetup; } diff --git a/src/plugins/custom_import_map/server/plugin.ts b/src/plugins/custom_import_map/server/plugin.ts index 80eabc91..b094bb6c 100644 --- a/src/plugins/custom_import_map/server/plugin.ts +++ b/src/plugins/custom_import_map/server/plugin.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { first } from 'rxjs/operators'; import { PluginInitializerContext, CoreSetup, @@ -12,7 +13,6 @@ import { } from '../../../../../src/core/server'; import { CustomImportMapPluginSetup, CustomImportMapPluginStart } from './types'; -import { first } from 'rxjs/operators'; import { createGeospatialCluster } from './clusters'; import { GeospatialService, OpensearchService } from './services'; import { geospatial, opensearch } from '../server/routes';