├── .babelrc ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG_TEMPLATE.md │ ├── FEATURE_REQUEST_TEMPLATE.md │ ├── PROPOSAL_TEMPLATE.md │ └── config.yml └── workflows │ ├── add-untriaged.yml │ ├── auto-release.yml │ ├── backport.yml │ ├── changelog_verifier.yml │ ├── remote-ftr-integ-test-workflow.yml │ ├── unit-tests-workflow.yml │ └── verify-binary-installation.yml ├── .gitignore ├── .i18nrc.json ├── .lintstagedrc ├── .opensearch_dashboards-plugin-helpers.json ├── .prettierignore ├── .prettierrc ├── .whitesource ├── ADMINS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER_GUIDE.md ├── LICENSE.txt ├── MAINTAINERS.md ├── NOTICE.txt ├── OpenSearch.svg ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── RELEASING.md ├── SECURITY.md ├── babel.config.js ├── common ├── config.ts ├── constants │ └── shared.ts ├── index.ts ├── map_saved_object_attributes.ts └── util.ts ├── opensearch_dashboards.json ├── package.json ├── public ├── _variables.scss ├── application.tsx ├── components │ ├── __snapshots__ │ │ ├── show_error_modal.test.tsx.snap │ │ └── vector_upload_options.test.tsx.snap │ ├── add_layer_panel │ │ ├── add_layer_panel.scss │ │ ├── add_layer_panel.tsx │ │ └── index.ts │ ├── app.tsx │ ├── draw │ │ └── modes │ │ │ └── rectangle.ts │ ├── filter_bar │ │ ├── __snapshots__ │ │ │ ├── filter_bar.test.tsx.snap │ │ │ ├── filter_editor.test.tsx.snap │ │ │ └── filter_view.test.tsx.snap │ │ ├── filter_actions.test.ts │ │ ├── filter_actions.ts │ │ ├── filter_bar.test.tsx │ │ ├── filter_bar.tsx │ │ ├── filter_editor.test.tsx │ │ ├── filter_editor.tsx │ │ ├── filter_item.tsx │ │ ├── filter_options.tsx │ │ ├── filter_view.test.tsx │ │ └── filter_view.tsx │ ├── layer_config │ │ ├── base_map_layer_config_panel.tsx │ │ ├── cluster_config │ │ │ ├── cluster_layer_config_panel.tsx │ │ │ ├── cluster_layer_source.tsx │ │ │ ├── cluster_section.tsx │ │ │ ├── config.ts │ │ │ ├── data_source_section.tsx │ │ │ ├── filter_section.tsx │ │ │ ├── json_editor.tsx │ │ │ ├── metric_section.tsx │ │ │ └── style │ │ │ │ └── index.tsx │ │ ├── custom_map_config │ │ │ ├── custom_map_config_panel.tsx │ │ │ └── custom_map_source.tsx │ │ ├── documents_config │ │ │ ├── document_layer_config_panel.tsx │ │ │ ├── document_layer_source.tsx │ │ │ └── style │ │ │ │ ├── color_picker.tsx │ │ │ │ ├── document_layer_style.tsx │ │ │ │ ├── label_config.scss │ │ │ │ ├── label_config.test.tsx │ │ │ │ └── label_config.tsx │ │ ├── index.ts │ │ ├── layer_basic_settings.test.tsx │ │ ├── layer_basic_settings.tsx │ │ └── layer_config_panel.tsx │ ├── layer_control_panel │ │ ├── delete_layer_modal.test.tsx │ │ ├── delete_layer_modal.tsx │ │ ├── hide_layer_button.test.tsx │ │ ├── hide_layer_button.tsx │ │ ├── index.ts │ │ ├── layer_control_panel.scss │ │ └── layer_control_panel.tsx │ ├── map_container │ │ ├── index.ts │ │ ├── legend │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ ├── legend_item.tsx │ │ │ └── legend_list.tsx │ │ ├── map_container.scss │ │ ├── map_container.tsx │ │ ├── maps_footer.test.tsx │ │ ├── maps_footer.tsx │ │ └── maps_messages.tsx │ ├── map_page │ │ ├── index.ts │ │ └── map_page.tsx │ ├── map_top_nav │ │ ├── get_top_nav_config.tsx │ │ ├── index.ts │ │ └── top_nav_menu.tsx │ ├── maps_list │ │ ├── index.ts │ │ └── maps_list.tsx │ ├── show_error_modal.test.tsx │ ├── show_error_modal.tsx │ ├── toolbar │ │ └── spatial_filter │ │ │ ├── __snapshots__ │ │ │ ├── filter-by_shape.test.tsx.snap │ │ │ └── filter_toolbar.test.tsx.snap │ │ │ ├── display_draw_helper.test.tsx │ │ │ ├── display_draw_helper.tsx │ │ │ ├── draw_filter_shape.tsx │ │ │ ├── draw_style.ts │ │ │ ├── filter-by_shape.test.tsx │ │ │ ├── filter_by_shape.tsx │ │ │ ├── filter_input_panel.test.tsx │ │ │ ├── filter_input_panel.tsx │ │ │ ├── filter_toolbar.test.tsx │ │ │ ├── filter_toolbar.tsx │ │ │ └── spatial_filter.scss │ ├── tooltip │ │ ├── create_tooltip.scss │ │ ├── create_tooltip.tsx │ │ ├── display_features.tsx │ │ ├── tooltipContainer.tsx │ │ ├── tooltipHeaderContent.tsx │ │ └── tooltipTable.tsx │ ├── vector_upload_options.scss │ ├── vector_upload_options.test.tsx │ └── vector_upload_options.tsx ├── embeddable │ ├── index.ts │ ├── map_component.tsx │ ├── map_embeddable.tsx │ └── map_embeddable_factory.tsx ├── images │ ├── polygon-dark.svg │ └── polygon-light.svg ├── index.scss ├── index.ts ├── model │ ├── OSMLayerFunctions.ts │ ├── agg │ │ └── build_agg.ts │ ├── clusterLayerFunctions.test.ts │ ├── clusterLayerFunctions.ts │ ├── customLayerFunctions.test.ts │ ├── customLayerFunctions.ts │ ├── documentLayerFunctions.ts │ ├── geo │ │ ├── filter.test.ts │ │ └── filter.ts │ ├── layerRenderController.ts │ ├── layersFunction.test.ts │ ├── layersFunctions.ts │ ├── map │ │ ├── __mocks__ │ │ │ ├── layer.ts │ │ │ └── map.ts │ │ ├── bondary.test.ts │ │ ├── boundary.ts │ │ ├── layer_operations.test.ts │ │ └── layer_operations.ts │ ├── mapLayerType.ts │ └── mapState.ts ├── plugin.tsx ├── services.ts ├── types.ts └── utils │ ├── breadcrumbs.ts │ ├── decode.ts │ ├── fields_options.test.ts │ ├── fields_options.ts │ ├── geo_formater.ts │ ├── getIntialConfig.ts │ └── precision.ts ├── release-notes ├── opensearch-dashboards-maps.release-notes-2.10.0.0.md ├── opensearch-dashboards-maps.release-notes-2.11.0.0.md ├── opensearch-dashboards-maps.release-notes-2.12.0.0.md ├── opensearch-dashboards-maps.release-notes-2.13.0.0.md ├── opensearch-dashboards-maps.release-notes-2.14.0.0.md ├── opensearch-dashboards-maps.release-notes-2.15.0.0.md ├── opensearch-dashboards-maps.release-notes-2.16.0.0.md ├── opensearch-dashboards-maps.release-notes-2.17.0.0.md ├── opensearch-dashboards-maps.release-notes-2.18.0.0.md ├── opensearch-dashboards-maps.release-notes-2.19.0.0.md ├── opensearch-dashboards-maps.release-notes-2.2.0.0.md ├── opensearch-dashboards-maps.release-notes-2.3.0.0.md ├── opensearch-dashboards-maps.release-notes-2.4.0.0.md ├── opensearch-dashboards-maps.release-notes-2.5.0.0.md ├── opensearch-dashboards-maps.release-notes-2.6.0.0.md ├── opensearch-dashboards-maps.release-notes-2.7.0.0.md ├── opensearch-dashboards-maps.release-notes-2.8.0.0.md ├── opensearch-dashboards-maps.release-notes-2.9.0.0.md ├── opensearch-dashboards-maps.release-notes-3.0.0.0-alpha1.md ├── opensearch-dashboards-maps.release-notes-3.0.0.0-beta1.md ├── opensearch-dashboards-maps.release-notes-3.0.0.0.md └── opensearch-dashboards-maps.release-notes-3.1.0.0.md ├── server ├── clusters │ ├── geospatial_cluster.js │ ├── geospatial_plugin.ts │ └── index.ts ├── common │ └── stats │ │ ├── stats_helper.test.ts │ │ └── stats_helper.ts ├── index.ts ├── plugin.ts ├── routes │ ├── geospatial.ts │ ├── index.ts │ ├── opensearch.ts │ └── stats_router.ts ├── saved_objects │ ├── capabilities_provider.ts │ ├── index.ts │ └── map_saved_object.ts ├── services │ ├── geospatial_service.js │ ├── index.ts │ ├── opensearch_service.js │ ├── sample_data │ │ └── flights_saved_objects.ts │ └── utils │ │ └── constants.ts └── types.ts ├── test ├── enzyme.js ├── jest.config.js ├── mocks │ └── styleMock.js ├── polyfills.js ├── polyfills │ └── mutationObserver.js ├── setup.jest.js └── setupTests.js ├── translations └── ja-JP.json ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { "node": "10" } 7 | } 8 | ], 9 | "@babel/preset-react", 10 | "@babel/preset-typescript" 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-syntax-jsx", 14 | "@babel/plugin-transform-modules-commonjs", 15 | ["@babel/plugin-transform-runtime", { "regenerator": true }], 16 | "@babel/plugin-transform-class-properties", 17 | "@babel/plugin-transform-object-rest-spread" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], 4 | rules: { 5 | '@osd/eslint/require-license-header': 'off', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This should match the list in MAINTAINERS.md. 2 | * @heemin32 @navneet1v @VijayanB @vamshin @jmazanec15 @naveentatikonda @junqiu-lei @martin-gaievski 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG]' 5 | labels: 'bug, untriaged' 6 | assignees: '' 7 | --- 8 | ### What is the bug? 9 | _A clear and concise description of the bug._ 10 | 11 | ### How can one reproduce the bug? 12 | _Steps to reproduce the behavior._ 13 | 14 | ### What is the expected behavior? 15 | _A clear and concise description of what you expected to happen._ 16 | 17 | ### What is your host/environment? 18 | _Operating system, version._ 19 | 20 | ### Do you have any screenshots? 21 | _If applicable, add screenshots to help explain your problem._ 22 | 23 | ### Do you have any additional context? 24 | _Add any other context about the problem._ 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🎆 Feature request 3 | about: Request a feature in this project 4 | title: '[FEATURE]' 5 | labels: 'enhancement, untriaged' 6 | assignees: '' 7 | --- 8 | ### Is your feature request related to a problem? 9 | _A clear and concise description of what the problem is, e.g. I'm always frustrated when [...]._ 10 | 11 | ### What solution would you like? 12 | _A clear and concise description of what you want to happen._ 13 | 14 | ### What alternatives have you considered? 15 | _A clear and concise description of any alternative solutions or features you've considered._ 16 | 17 | ### Do you have any additional context? 18 | _Add any other context or screenshots about the feature request here._ 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PROPOSAL_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💭 Proposal 3 | about: Suggest an idea for a specific feature you wish to propose to the community for comment 4 | title: '[PROPOSAL]' 5 | labels: proposal 6 | assignees: '' 7 | --- 8 | ### What are you proposing? 9 | _In a few sentences, describe the feature and its core capabilities._ 10 | 11 | ### How did you come up with this proposal? 12 | _Highlight any research, proposals, requests, issues, forum posts, anecdotes that signal this is the right thing to build. Highlight opportunities for additional research._ 13 | 14 | ### What is the user experience going to be? 15 | _Describe the feature requirements and or user stories. You may include low-fidelity sketches, wireframes, APIs stubs, or other examples of how a user would use the feature. Using a bulleted list or simple diagrams to outline features is okay. e.g. As a < type of user > , I want to < achieve a goal > so that < for some reason >._ 16 | 17 | ### Why should it be built? Any reason not to? 18 | _Describe the most important user needs, pain points, and the value that this feature will bring to the OpenSearch community, as well as what impact it has if it isn't built, or new risks if it is. What is preventing you from meeting this need today?_ 19 | 20 | ### What will it take to execute? 21 | _Describe what it will take to build this feature. Are there any assumptions you may be making that could limit scope or add limitations? Are there performance, cost, or technical constraints that may impact the user experience? Does this feature depend on other feature work? What additional risks are there?_ 22 | 23 | ### What are remaining open questions? 24 | _List questions that may need to be answered before proceeding with an implementation._ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: OpenSearch Community Support 3 | url: https://discuss.opendistrocommunity.dev/ 4 | about: Please ask and answer questions here. 5 | - name: AWS/Amazon Security 6 | url: https://aws.amazon.com/security/vulnerability-reporting/ 7 | about: Please report security vulnerabilities here. 8 | -------------------------------------------------------------------------------- /.github/workflows/add-untriaged.yml: -------------------------------------------------------------------------------- 1 | name: Apply 'untriaged' label during issue lifecycle 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened, transferred] 6 | 7 | jobs: 8 | apply-label: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/github-script@v6 12 | with: 13 | script: | 14 | github.rest.issues.addLabels({ 15 | issue_number: context.issue.number, 16 | owner: context.repo.owner, 17 | repo: context.repo.repo, 18 | labels: ['untriaged'] 19 | }) 20 | -------------------------------------------------------------------------------- /.github/workflows/auto-release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: GitHub App token 15 | id: github_app_token 16 | uses: tibdex/github-app-token@v1.5.0 17 | with: 18 | app_id: ${{ secrets.APP_ID }} 19 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 20 | installation_id: 22958780 21 | - name: Get tag 22 | id: tag 23 | uses: dawidd6/action-get-tag@v1 24 | - uses: actions/checkout@v2 25 | - uses: ncipollo/release-action@v1 26 | with: 27 | github_token: ${{ steps.github_app_token.outputs.token }} 28 | bodyFile: release-notes/opensearch-dashboards-maps.release-notes-${{steps.tag.outputs.tag}}.md 29 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request_target: 4 | types: 5 | - closed 6 | - labeled 7 | 8 | jobs: 9 | backport: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | name: Backport 15 | steps: 16 | - name: GitHub App token 17 | id: github_app_token 18 | uses: tibdex/github-app-token@v1.5.0 19 | with: 20 | app_id: ${{ secrets.APP_ID }} 21 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 22 | installation_id: 22958780 23 | 24 | - name: Backport 25 | uses: VachaShah/backport@v1.1.4 26 | with: 27 | github_token: ${{ steps.github_app_token.outputs.token }} 28 | branch_name: backport/backport-${{ github.event.number }} -------------------------------------------------------------------------------- /.github/workflows/changelog_verifier.yml: -------------------------------------------------------------------------------- 1 | name: "Changelog Verifier" 2 | on: 3 | pull_request: 4 | types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] 5 | 6 | jobs: 7 | # Enforces the update of a changelog file on every pull request 8 | verify-changelog: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | ref: ${{ github.event.pull_request.head.sha }} 15 | 16 | - uses: dangoslen/changelog-enforcer@v3 17 | with: 18 | skipLabels: "autocut, skip-changelog" 19 | -------------------------------------------------------------------------------- /.github/workflows/verify-binary-installation.yml: -------------------------------------------------------------------------------- 1 | name: 'Install Dashboards with Plugin via Binary' 2 | 3 | on: [push, pull_request] 4 | env: 5 | OPENSEARCH_VERSION: '3.1.0' 6 | CI: 1 7 | # avoid warnings like "tput: No value for $TERM and no -T specified" 8 | TERM: xterm 9 | 10 | jobs: 11 | verify-binary-installation: 12 | name: Run binary installation 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest] 17 | # TODO: add windows support when OSD core is stable on windows 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout Branch 21 | uses: actions/checkout@v3 22 | 23 | - name: Set env 24 | run: | 25 | opensearch_version=$(node -p "require('./opensearch_dashboards.json').opensearchDashboardsVersion") 26 | plugin_version=$(node -p "require('./opensearch_dashboards.json').version") 27 | echo "OPENSEARCH_VERSION=$opensearch_version" >> $GITHUB_ENV 28 | echo "PLUGIN_VERSION=$plugin_version" >> $GITHUB_ENV 29 | shell: bash 30 | 31 | - name: Set up JDK 21 32 | uses: actions/setup-java@v3 33 | with: 34 | distribution: 'corretto' 35 | java-version: '21' 36 | 37 | - name: Run Opensearch 38 | uses: derek-ho/start-opensearch@v6 39 | with: 40 | opensearch-version: ${{ env.OPENSEARCH_VERSION }} 41 | security-enabled: false 42 | jdk-version: 21 43 | 44 | - name: Run Dashboards 45 | id: setup-dashboards 46 | uses: derek-ho/setup-opensearch-dashboards@v2 47 | with: 48 | plugin_name: dashboards-maps 49 | built_plugin_name: customImportMapDashboards 50 | built_plugin_suffix: ${{ env.OPENSEARCH_VERSION }} 51 | install_zip: true 52 | 53 | - name: Start the binary 54 | run: | 55 | nohup ./bin/opensearch-dashboards & 56 | working-directory: ${{ steps.setup-dashboards.outputs.dashboards-binary-directory }} 57 | shell: bash 58 | 59 | - name: Health check 60 | run: | 61 | timeout 300 bash -c 'while [[ "$(curl http://localhost:5601/api/status | jq -r '.status.overall.state')" != "green" ]]; do sleep 5; done' 62 | shell: bash 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | target/ 3 | build/ 4 | coverage/ 5 | cypress/videos/ 6 | cypress/screenshots/ 7 | yarn-error.log 8 | .DS_Store 9 | .idea 10 | -------------------------------------------------------------------------------- /.i18nrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "customImportMap", 3 | "paths": { 4 | "customImportMap": "." 5 | }, 6 | "translations": ["translations/ja-JP.json"] 7 | } 8 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,json,css,md}": ["prettier --write", "git add"] 3 | } 4 | -------------------------------------------------------------------------------- /.opensearch_dashboards-plugin-helpers.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverSourcePatterns": [ 3 | "package.json", 4 | "tsconfig.json", 5 | "yarn.lock", 6 | ".yarnrc", 7 | "{lib,public,server,webpackShims,translations,utils,models,test,common}/**/*", 8 | "!__tests__" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | coverage 4 | node_modules 5 | npm-debug.log 6 | yarn.lock 7 | *.md 8 | *.lock 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "configMode": "AUTO", 4 | "configExternalURL": "", 5 | "projectToken": "", 6 | "baseBranches": [] 7 | }, 8 | "checkRunSettings": { 9 | "vulnerableCheckRunConclusionLevel": "failure", 10 | "displayMode": "diff", 11 | "useMendCheckNames": true 12 | }, 13 | "issueSettings": { 14 | "minSeverityLevel": "LOW", 15 | "issueType": "DEPENDENCY" 16 | }, 17 | "remediateSettings": { 18 | "workflowRules": { 19 | "enabled": true 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /ADMINS.md: -------------------------------------------------------------------------------- 1 | - [Overview](#overview) 2 | - [Current Admins](#current-admins) 3 | - [Admin Responsibilities](#admin-responsibilities) 4 | - [Prioritize Security](#prioritize-security) 5 | - [Enforce Code of Conduct](#enforce-code-of-conduct) 6 | - [Add/Remove Maintainers](#addremove-maintainers) 7 | - [Adopt Organizational Best Practices](#adopt-organizational-best-practices) 8 | 9 | ## Overview 10 | 11 | This document explains who the admins are (see below), what they do in this repo, and how they should be doing it. If you're interested in becoming a maintainer, see [MAINTAINERS](MAINTAINERS.md). If you're interested in contributing, see [CONTRIBUTING](CONTRIBUTING.md). 12 | 13 | ## Current Admins 14 | 15 | | Admin | GitHub ID | Affiliation | 16 | | --------------- | ------------------------------------------- | ----------- | 17 | | Henri Yandell | [hyandell](https://github.com/hyandell) | Amazon | 18 | 19 | ## Admin Responsibilities 20 | 21 | As an admin you own stewardship of the repository and its settings. Admins have [admin-level permissions on a repository](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization). Use those privileges to serve the community and protect the repository as follows. 22 | 23 | ### Prioritize Security 24 | 25 | Security is your number one priority. Manage security keys and safeguard access to the repository. 26 | 27 | Note that this repository is monitored and supported 24/7 by Amazon Security, see [Reporting a Vulnerability](SECURITY.md) for details. 28 | 29 | ### Enforce Code of Conduct 30 | 31 | Act on [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) violations by revoking access, and blocking malicious actors. 32 | 33 | ### Add/Remove Maintainers 34 | 35 | Perform administrative tasks, such as [adding](MAINTAINERS.md#adding-a-new-maintainer) and [removing maintainers](MAINTAINERS.md#removing-a-maintainer). 36 | 37 | ### Adopt Organizational Best Practices 38 | 39 | Adopt organizational best practices, work in the open, and collaborate with other admins by opening issues before making process changes. Prefer consistency, and avoid diverging from practices in the opensearch-project organization. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | All notable changes to this project are documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). See the [CONTRIBUTING guide](./CONTRIBUTING.md#Changelog) for instructions on how to add changelog entries. 5 | 6 | ## [Unreleased 3.x](https://github.com/opensearch-project/dashboards-maps/compare/main...HEAD) 7 | ### Features 8 | ### Enhancements 9 | ### Bug Fixes 10 | ### Infrastructure 11 | ### Documentation 12 | ### Maintenance 13 | ### Refactoring 14 | 15 | ## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.19...2.x) 16 | ### Features 17 | ### Enhancements 18 | ### Bug Fixes 19 | ### Infrastructure 20 | ### Documentation 21 | ### Maintenance 22 | ### Refactoring 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | This code of conduct applies to all spaces provided by the OpenSource project including in code, documentation, issue trackers, mailing lists, chat channels, wikis, blogs, social media and any other communication channels used by the project. 3 | 4 | **Our open source communities endeavor to:** 5 | 6 | * Be Inclusive: We are committed to being a community where everyone can join and contribute. This means using inclusive and welcoming language. 7 | * Be Welcoming: We are committed to maintaining a safe space for everyone to be able to contribute. 8 | * Be Respectful: We are committed to encouraging differing viewpoints, accepting constructive criticism and work collaboratively towards decisions that help the project grow. Disrespectful and unacceptable behavior will not be tolerated. 9 | * Be Collaborative: We are committed to supporting what is best for our community and users. When we build anything for the benefit of the project, we should document the work we do and communicate to others on how this affects their work. 10 | 11 | **Our Responsibility. As contributors, members, or bystanders we each individually have the responsibility to behave professionally and respectfully at all times. Disrespectful and unacceptable behaviors include, but are not limited to:** 12 | 13 | * The use of violent threats, abusive, discriminatory, or derogatory language; 14 | * Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, race, political or religious affiliation; 15 | * Posting of sexually explicit or violent content; 16 | * The use of sexualized language and unwelcome sexual attention or advances; 17 | * Public or private harassment of any kind; 18 | * Publishing private information, such as physical or electronic address, without permission; 19 | * Other conduct which could reasonably be considered inappropriate in a professional setting; 20 | * Advocating for or encouraging any of the above behaviors. 21 | 22 | **Enforcement and Reporting Code of Conduct Issues:** 23 | 24 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported. [Contact us](mailto:opensource-codeofconduct@amazon.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. 25 | -------------------------------------------------------------------------------- /DEVELOPER_GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Developer Guide 2 | 3 | So you want to contribute code to this project? Excellent! We're glad you're here. Here's what you need to do. 4 | 5 | - [Forking](#forking) 6 | - [Install Prerequisites](#install-prerequisites) 7 | - [Setup](#setup) 8 | - [Run](#run) 9 | - [Build](#build) 10 | - [Test](#test) 11 | - [Submitting Changes](#submitting-changes) 12 | - [Backports](#backports) 13 | 14 | ### Forking 15 | 16 | Fork the repository on GitHub. 17 | 18 | ### Install Prerequisites 19 | 20 | You will need to install [node.js](https://nodejs.org/en/), [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md), and [yarn](https://yarnpkg.com/) in your environment to properly pull down dependencies to build and bootstrap the plugin. 21 | 22 | 23 | ### Setup 24 | 25 | 1. Download OpenSearch [Geospatial](https://github.com/opensearch-project/geospatial) plugin, which requires same version with OpenSearch-Dashboard and dashboards-maps plugin. 26 | 2. Run `./gradlew run` under Geospatial plugin root path to start OpenSearch cluster. 27 | 3. Download the OpenSearch Dashboards source code for the version specified in package.json you want to set up. 28 | 4. Change your node version by `nvm use `to the version specified in `.node-version` inside the OpenSearch Dashboards root directory. 29 | 5. Create a `plugins` directory inside the OpenSearch Dashboards source code directory, if `plugins` directory doesn't exist. 30 | 6. cd into `plugins` directory in the OpenSearch Dashboards source code directory. 31 | 7. Check out this package from version control into the `plugins` directory. 32 | 8. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/dashboards-maps` folder. 33 | 34 | Ultimately, your directory structure should look like this: 35 | 36 | ```md 37 | ├── OpenSearch-Dashboards 38 | │ ├── plugins 39 | │ │ └── dashboards-maps 40 | ``` 41 | 42 | ### Run 43 | 44 | From OpenSearch-Dashboards repo (root folder), the following commands start OpenSearch Dashboards and includes this plugin. 45 | 46 | ``` 47 | yarn osd bootstrap --single-version=loose 48 | yarn start --no-base-path 49 | ``` 50 | 51 | OpenSearch Dashboards will be available on `localhost:5601`. 52 | 53 | ### Build 54 | 55 | To build the plugin's distributable zip simply run `yarn build`. 56 | 57 | Example output: ./build/customImportMapDashboards-1.0.0.0.zip 58 | 59 | ### Test 60 | 61 | From dashboards-maps folder running the following command runs the plugin unit tests: 62 | 63 | #### Unit test 64 | ``` 65 | yarn test:jest 66 | ``` 67 | 68 | #### Integration Tests 69 | All integration tests cases for maps-dashboards plugin are written in [opensearch-dashboards-functional-test](https://github.com/opensearch-project/opensearch-dashboards-functional-test/blob/main/DEVELOPER_GUIDE.md#run-tests) repository using the Cypress test framework. Please follow the [DEVELOPER_GUIDE.md](https://github.com/opensearch-project/opensearch-dashboards-functional-test/blob/main/DEVELOPER_GUIDE.md) to run the integration tests. 70 | The tests are located in the `cypress/fixtures/plugins/custom-import-map-dashboards` directory. 71 | 72 | ### Submitting Changes 73 | 74 | See [CONTRIBUTING](CONTRIBUTING.md). 75 | 76 | ### Backports 77 | 78 | The GitHub backport workflow creates backport PRs automatically for PRs with label `backport `. Label should be attached to the original PR, backport workflow starts when original PR merged to main branch. For example, if a PR on main needs to be backported to `1.x` branch, add a label `backport 1.x` to the PR and make sure the 79 | backport workflow runs on the PR along with other checks. Once this PR is merged to main, the workflow will create a backport PR 80 | to the `1.x` branch. 81 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This document contains a list of maintainers in this repo. See [opensearch-project/.github/RESPONSIBILITIES.md](https://github.com/opensearch-project/.github/blob/main/RESPONSIBILITIES.md#maintainer-responsibilities) that explains what the role of maintainer means, what maintainers do in this and other repos, and how they should be doing it. If you're interested in contributing, and becoming a maintainer, see [CONTRIBUTING](CONTRIBUTING.md). 4 | 5 | ## Current Maintainers 6 | | Maintainer | GitHub ID | Affiliation | 7 | |-------------------------|-------------------------------------------------------|-------------| 8 | | Heemin Kim | [heemin32](https://github.com/heemin32) | Amazon | 9 | | Jack Mazanec | [jmazanec15](https://github.com/jmazanec15) | Amazon | 10 | | Junqiu Lei | [junqiu-lei](https://github.com/junqiu-lei) | Amazon | 11 | | Martin Gaievski | [martin-gaievski](https://github.com/martin-gaievski) | Amazon | 12 | | Naveen Tatikonda | [naveentatikonda](https://github.com/naveentatikonda) | Amazon | 13 | | Navneet Verma | [navneet1v](https://github.com/navneet1v) | Amazon | 14 | | Vamshi Vijay Nakkirtha | [vamshin](https://github.com/vamshin) | Amazon | 15 | | Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | 16 | 17 | ## Emeritus 18 | 19 | | Maintainer | GitHub ID | Affiliation | 20 | |-------------------------|---------------------------------------------| ----------- | 21 | | Shivam Dhar | [Shivamdhar](https://github.com/Shivamdhar) | Amazon | 22 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | OpenSearch (https://opensearch.org/) 2 | Copyright OpenSearch Contributors -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | _Describe what this change achieves._ 3 | 4 | ### Issues Resolved 5 | _List any issues this PR will resolve, e.g. Closes [...]._ 6 | 7 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 8 | For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Unit tests](https://github.com/opensearch-project/dashboards-maps/workflows/Unit%20tests%20workflow/badge.svg)](https://github.com/opensearch-project/dashboards-maps/actions?query=workflow%3A%22Unit+tests+workflow%22) 2 | [![codecov](https://codecov.io/gh/opensearch-project/dashboards-maps/branch/main/graph/badge.svg)](https://codecov.io/gh/opensearch-project/dashboards-maps) 3 | [![Forum](https://img.shields.io/badge/chat-on%20forums-blue)](https://forum.opensearch.org/) 4 | ![PRs welcome!](https://img.shields.io/badge/PRs-welcome!-success) 5 | 6 | ![OpenSearch logo](OpenSearch.svg) 7 | 8 | - [Welcome!](#welcome) 9 | - [Project Resources](#project-resources) 10 | - [Code of Conduct](#code-of-conduct) 11 | - [License](#license) 12 | - [Copyright](#copyright) 13 | 14 | # Dashboards-Maps 15 | 16 | Dashboards-maps is a frontend plugin that helps you in uploading custom GeoJSON to OpenSearch and communicates with the geospatial backend plugin for the same. 17 | 18 | ## Project Resources 19 | 20 | * [Project Website](https://opensearch.org/) 21 | * [Downloads](https://opensearch.org/downloads.html) 22 | * [Documentation](https://opensearch.org/docs/latest/) 23 | * [Developer Guide](DEVELOPER_GUIDE.md) 24 | * Need help? Try [Forums](https://discuss.opendistrocommunity.dev/) 25 | * [Project Principles](https://opensearch.org/#principles) 26 | * [Contributing to OpenSearch](CONTRIBUTING.md) 27 | * [Maintainer Responsibilities](MAINTAINERS.md) 28 | * [Release Management](RELEASING.md) 29 | * [Admin Responsibilities](ADMINS.md) 30 | * [Security](SECURITY.md) 31 | 32 | ## Code of Conduct 33 | 34 | This project has adopted the [Amazon Open Source Code of Conduct](CODE_OF_CONDUCT.md). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq), or contact [opensource-codeofconduct@amazon.com](mailto:opensource-codeofconduct@amazon.com) with any additional questions or comments. 35 | 36 | ## License 37 | 38 | This project is licensed under the [Apache v2.0 License](LICENSE.txt). 39 | 40 | ## Copyright 41 | 42 | Copyright OpenSearch Contributors. See [NOTICE](NOTICE.txt) for details. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting a Vulnerability 2 | 3 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do **not** create a public GitHub issue. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | module.exports = { 7 | presets: [ 8 | require('@babel/preset-env'), 9 | require('@babel/preset-react'), 10 | require('@babel/preset-typescript'), 11 | ], 12 | plugins: [ 13 | require('@babel/plugin-transform-class-properties'), 14 | require('@babel/plugin-transform-object-rest-spread'), 15 | ['@babel/plugin-transform-modules-commonjs', { allowTopLevelThis: true }], 16 | [require('@babel/plugin-transform-runtime'), { regenerator: true }], 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /common/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { schema, TypeOf } from '@osd/config-schema'; 7 | 8 | export const configSchema = schema.object({ 9 | opensearchVectorTileDataUrl: schema.string({ 10 | defaultValue: 'https://tiles.maps.opensearch.org/data/v1.json', 11 | }), 12 | opensearchVectorTileStyleUrl: schema.string({ 13 | defaultValue: 'https://tiles.maps.opensearch.org/v3/manifest.json', 14 | }), 15 | opensearchVectorTileGlyphsUrl: schema.string({ 16 | defaultValue: 'https://tiles.maps.opensearch.org/fonts/{fontstack}/{range}.pbf', 17 | }), 18 | }); 19 | 20 | export type ConfigSchema = TypeOf; 21 | -------------------------------------------------------------------------------- /common/constants/shared.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { fromMBtoBytes } from '../util'; 7 | 8 | export const ALLOWED_FILE_EXTENSIONS = ['.json', '.geojson']; 9 | export const BASE_NODE_API_PATH = '/_plugins/geospatial'; 10 | export const UPLOAD_GEOJSON_API_PATH = `${BASE_NODE_API_PATH}/geojson/_upload`; 11 | export const MAX_FILE_PAYLOAD_SIZE_IN_MB = 25; 12 | export const MAX_FILE_PAYLOAD_SIZE = fromMBtoBytes(MAX_FILE_PAYLOAD_SIZE_IN_MB); 13 | export const PLUGIN_ID = 'customImportMap'; 14 | export const PLUGIN_NAME = 'customImportMap'; 15 | export const MAPS_APP_DISPLAY_NAME = 'Maps'; 16 | export const MAPS_APP_ID = 'maps-dashboards'; 17 | -------------------------------------------------------------------------------- /common/map_saved_object_attributes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { SavedObjectAttributes } from '../../../src/core/types'; 7 | 8 | export interface MapSavedObjectAttributes extends SavedObjectAttributes { 9 | /** Title of the map */ 10 | title: string; 11 | /** Description of the map */ 12 | description?: string; 13 | /** State of the map, which could include current zoom level, lat, lng etc. */ 14 | mapState?: string; 15 | /** Maps-dashboards layers of the map */ 16 | layerList?: string; 17 | /** UI state of the map */ 18 | uiState?: string; 19 | /** Version is used to track version differences in saved object mapping */ 20 | version: number; 21 | /** SearchSourceFields is used to reference other saved objects */ 22 | searchSourceFields?: { 23 | index?: string; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /common/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { i18n } from '@osd/i18n'; 6 | import { OSD_LANGUAGES, FALLBACK_LANGUAGE } from './index'; 7 | 8 | export const fromMBtoBytes = (sizeInMB: number) => { 9 | return sizeInMB * 1024 * 1024; 10 | }; 11 | 12 | export const getMapLanguage = () => { 13 | const OSDLanguage = i18n.getLocale().toLowerCase(), 14 | parts = OSDLanguage.split('-'); 15 | const languageCode = parts.length > 1 ? parts[0] : OSDLanguage; 16 | return OSD_LANGUAGES.includes(languageCode) ? languageCode : FALLBACK_LANGUAGE; 17 | }; 18 | 19 | export function isEscapeKey(e: KeyboardEvent) { 20 | return e.code === 'Escape'; 21 | } 22 | -------------------------------------------------------------------------------- /opensearch_dashboards.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "customImportMapDashboards", 3 | "version": "3.1.0.0", 4 | "opensearchDashboardsVersion": "3.1.0", 5 | "server": true, 6 | "ui": true, 7 | "requiredPlugins": [ 8 | "regionMap", 9 | "opensearchDashboardsReact", 10 | "opensearchDashboardsUtils", 11 | "navigation", 12 | "savedObjects", 13 | "data", 14 | "embeddable", 15 | "visualizations" 16 | ], 17 | "optionalPlugins": [ 18 | "home", 19 | "dataSource", 20 | "dataSourceManagement" 21 | ] 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customImportMap", 3 | "version": "3.1.0.0", 4 | "license": "Apache-2.0", 5 | "config": { 6 | "id": "customImportMapDashboards" 7 | }, 8 | "scripts": { 9 | "build": "yarn plugin-helpers build", 10 | "plugin-helpers": "node ../../scripts/plugin_helpers", 11 | "osd": "node ../../scripts/osd", 12 | "lint": "yarn run lint:es && yarn run lint:style", 13 | "lint:es": "node ../../scripts/eslint", 14 | "lint:style": "node ../../scripts/stylelint", 15 | "test:jest": "../../node_modules/.bin/jest --config ./test/jest.config.js" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged" 20 | } 21 | }, 22 | "dependencies": { 23 | "@mapbox/mapbox-gl-draw": "^1.5.0", 24 | "@opensearch-dashboards-test/opensearch-dashboards-test-library": "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz", 25 | "@types/mapbox__mapbox-gl-draw": "^1.4.8", 26 | "@types/wellknown": "^0.5.8", 27 | "geojson": "^0.5.0", 28 | "h3-js": "^4.1.0", 29 | "install": "^0.13.0", 30 | "maplibre-gl": "^5.2.0", 31 | "prettier": "^2.1.1", 32 | "uuid": "3.3.2", 33 | "wellknown": "^0.5.0" 34 | }, 35 | "devDependencies": { 36 | "@types/react-test-renderer": "^19.0.0", 37 | "prettier": "^2.1.1" 38 | } 39 | } -------------------------------------------------------------------------------- /public/_variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | $mapHeaderOffset: 154px; 7 | -------------------------------------------------------------------------------- /public/application.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { AppMountParameters } from '../../../src/core/public'; 9 | import { MapServices } from './types'; 10 | import { MapsDashboardsApp } from './components/app'; 11 | import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; 12 | 13 | export const renderApp = ( 14 | { element }: AppMountParameters, 15 | services: MapServices, 16 | ) => { 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | element 22 | ); 23 | 24 | return () => ReactDOM.unmountComponentAtNode(element); 25 | }; 26 | -------------------------------------------------------------------------------- /public/components/__snapshots__/show_error_modal.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`display error modal renders the error modal based on the props passed 1`] = ` 4 |
9 | 30 |
31 | `; 32 | -------------------------------------------------------------------------------- /public/components/add_layer_panel/add_layer_panel.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .addLayer__button { 7 | padding: $euiSizeM $euiSizeM; 8 | } 9 | 10 | .addLayerDialog__description { 11 | width: 16 * $euiSizeM; 12 | } 13 | 14 | .addLayer__types { 15 | width: 5 * $euiSizeL; 16 | height: 5 * $euiSizeL; 17 | } 18 | 19 | .addLayer__modalBody { 20 | padding: 0; 21 | 22 | .addLayer__selection { 23 | width: 10 * $euiSizeL 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/components/add_layer_panel/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { AddLayerPanel } from './add_layer_panel'; 7 | -------------------------------------------------------------------------------- /public/components/app.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { Router, Route, Switch } from 'react-router-dom'; 8 | import { I18nProvider } from '@osd/i18n/react'; 9 | import { MapsList } from './maps_list'; 10 | import { MapPage } from './map_page'; 11 | import { APP_PATH } from '../../common'; 12 | import { useOpenSearchDashboards } from '../../../../src/plugins/opensearch_dashboards_react/public'; 13 | import { MapServices } from '../types'; 14 | 15 | export const MapsDashboardsApp = () => { 16 | const { 17 | services: { appBasePath }, 18 | } = useOpenSearchDashboards(); 19 | // Render the application DOM. 20 | return ( 21 | 22 | 23 | 24 | } /> 25 | } 29 | /> 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /public/components/filter_bar/__snapshots__/filter_bar.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders one filter inside filter bar 1`] = ` 4 | 10 | 14 | 21 | 22 | 25 | 32 | 37 | 76 | 77 | 78 | 79 | 80 | `; 81 | -------------------------------------------------------------------------------- /public/components/filter_bar/__snapshots__/filter_editor.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders one filter inside filter bar 1`] = ` 4 |
5 | 6 | 10 | 11 | Edit Filter 12 | 13 | 14 | 15 |
18 | 19 | 28 | 36 | 37 |
38 | 41 | 50 | 55 | 56 |
57 | 60 | 65 | 68 | 74 | Save 75 | 76 | 77 | 80 | 85 | Cancel 86 | 87 | 88 | 89 | 90 |
91 |
92 |
93 | `; 94 | -------------------------------------------------------------------------------- /public/components/filter_bar/__snapshots__/filter_view.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test filter display renders filter display on disabled 1`] = ` 4 | 19 | mylabel 20 | 21 | `; 22 | 23 | exports[`test filter display renders filter display on enabled 1`] = ` 24 | 39 | mylabel 40 | 41 | `; 42 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_actions.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import {disableGeoShapeFilterMeta, enableGeoShapeFilterMeta, toggleGeoShapeFilterMetaNegated} from './filter_actions'; 7 | import { GeoShapeFilterMeta } from '../../../../../src/plugins/data/common'; 8 | 9 | describe('test filter actions', function () { 10 | it('should enable GeoShapeFilterMeta', function () { 11 | const updatedFilterMeta: GeoShapeFilterMeta = enableGeoShapeFilterMeta({ 12 | alias: null, 13 | negate: false, 14 | params: {}, 15 | disabled: true, 16 | }); 17 | expect(updatedFilterMeta.disabled).toEqual(false); 18 | }); 19 | it('should disable GeoShapeFilterMeta', function () { 20 | const updatedFilterMeta: GeoShapeFilterMeta = disableGeoShapeFilterMeta({ 21 | alias: null, 22 | negate: false, 23 | params: {}, 24 | disabled: false, 25 | }); 26 | expect(updatedFilterMeta.disabled).toEqual(true); 27 | }); 28 | it('should toggle GeoShapeFilterMeta negation', function () { 29 | const updatedFilterMeta: GeoShapeFilterMeta = toggleGeoShapeFilterMetaNegated({ 30 | alias: null, 31 | negate: false, 32 | params: {}, 33 | disabled: false, 34 | }); 35 | expect(updatedFilterMeta.negate).toEqual(true); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { GeoShapeFilterMeta } from '../../../../../src/plugins/data/common'; 7 | 8 | export const enableGeoShapeFilterMeta = (meta: GeoShapeFilterMeta) => 9 | !meta.disabled ? meta : toggleGeoShapeFilterMetaDisabled(meta); 10 | 11 | export const disableGeoShapeFilterMeta = (meta: GeoShapeFilterMeta) => 12 | meta.disabled ? meta : toggleGeoShapeFilterMetaDisabled(meta); 13 | 14 | export const toggleGeoShapeFilterMetaNegated = (meta: GeoShapeFilterMeta) => { 15 | const negate = !meta.negate; 16 | return { ...meta, negate }; 17 | }; 18 | export const toggleGeoShapeFilterMetaDisabled = (meta: GeoShapeFilterMeta) => { 19 | const status: boolean = !meta.disabled; 20 | return { ...meta, disabled: status }; 21 | }; 22 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_bar.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { shallow } from 'enzyme'; 7 | import React from 'react'; 8 | import { FilterBar } from './filter_bar'; 9 | import { FILTERS, GeoShapeFilterMeta, ShapeFilter } from '../../../../../src/plugins/data/common'; 10 | import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; 11 | import { Polygon } from 'geojson'; 12 | 13 | it('renders one filter inside filter bar', () => { 14 | const mockCallback = jest.fn(); 15 | const mockPolygon: Polygon = { 16 | type: 'Polygon', 17 | coordinates: [ 18 | [ 19 | [74.006, 40.7128], 20 | [71.0589, 42.3601], 21 | [73.7562, 42.6526], 22 | [74.006, 40.7128], 23 | ], 24 | ], 25 | }; 26 | const expectedParams: { 27 | shape: ShapeFilter; 28 | relation: GeoShapeRelation; 29 | } = { 30 | shape: mockPolygon, 31 | relation: 'intersects', 32 | }; 33 | const mockFilterMeta: GeoShapeFilterMeta = { 34 | alias: 'mylabel', 35 | disabled: false, 36 | negate: false, 37 | type: FILTERS.GEO_SHAPE, 38 | params: expectedParams, 39 | }; 40 | const filterBar = shallow( 41 | 46 | ); 47 | expect(filterBar).toMatchSnapshot(); 48 | }); 49 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_editor.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { shallow } from 'enzyme'; 7 | import React from 'react'; 8 | import { FilterEditor } from './filter_editor'; 9 | 10 | it('renders one filter inside filter bar', () => { 11 | const mockOnSubmitCallback = jest.fn(); 12 | const mockOnCancelCallback = jest.fn(); 13 | const filterEditor = shallow( 14 | 20 | ); 21 | expect(filterEditor).toMatchSnapshot(); 22 | }); 23 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_editor.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { 7 | EuiSmallButton, 8 | EuiSmallButtonEmpty, 9 | EuiCodeEditor, 10 | EuiCompressedFieldText, 11 | EuiFlexGroup, 12 | EuiFlexItem, 13 | EuiForm, 14 | EuiCompressedFormRow, 15 | EuiPopoverTitle, 16 | EuiSpacer, 17 | } from '@elastic/eui'; 18 | import React, { useState } from 'react'; 19 | import { i18n } from '@osd/i18n'; 20 | 21 | interface Props { 22 | content: string; 23 | label: string; 24 | onSubmit: (content: string, label: string) => void; 25 | onCancel: () => void; 26 | } 27 | 28 | const isFilterValid = (content: string) => { 29 | try { 30 | return Boolean(JSON.parse(content)); 31 | } catch (e) { 32 | return false; 33 | } 34 | }; 35 | 36 | export const FilterEditor = ({ content, label, onSubmit, onCancel }: Props) => { 37 | const [filterLabel, setFilterLabel] = useState(label); 38 | const [filterContent, setFilterContent] = useState(content); 39 | 40 | const renderEditor = () => { 41 | return ( 42 | 48 | 55 | 56 | ); 57 | }; 58 | return ( 59 |
60 | 61 | 62 | {'Edit Filter'} 63 | 64 | 65 | 66 |
67 | 68 | {renderEditor()} 69 |
70 | 71 | 77 | setFilterLabel(event.target.value)} 81 | /> 82 | 83 |
84 | 85 | 86 | 87 | onSubmit(filterContent, filterLabel)} 90 | isDisabled={!isFilterValid(filterContent)} 91 | data-test-subj="saveFilter" 92 | > 93 | {'Save'} 94 | 95 | 96 | 97 | 98 | {'Cancel'} 99 | 100 | 101 | 102 | 103 |
104 |
105 |
106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_view.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { shallow } from 'enzyme'; 7 | import React from 'react'; 8 | import { FilterView } from './filter_view'; 9 | 10 | describe('test filter display', function () { 11 | it('renders filter display on disabled', () => { 12 | const mockOnClick = jest.fn(); 13 | const mockOnRemove = jest.fn(); 14 | const filterBar = shallow( 15 | 21 | ); 22 | expect(filterBar).toMatchSnapshot(); 23 | }); 24 | it('renders filter display on enabled', () => { 25 | const mockOnClick = jest.fn(); 26 | const mockOnRemove = jest.fn(); 27 | const filterView = shallow( 28 | 34 | ); 35 | expect(filterView).toMatchSnapshot(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /public/components/filter_bar/filter_view.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiBadge } from '@elastic/eui'; 7 | import { i18n } from '@osd/i18n'; 8 | import React, { FC } from 'react'; 9 | 10 | interface Props { 11 | isDisabled: boolean; 12 | valueLabel: string; 13 | onRemove: () => void; 14 | [propName: string]: any; 15 | } 16 | 17 | export const FilterView: FC = ({ 18 | isDisabled, 19 | onRemove, 20 | onClick, 21 | valueLabel, 22 | ...rest 23 | }: Props) => { 24 | let title = i18n.translate('maps.filter.filterBar.moreFilterActionsMessage', { 25 | defaultMessage: 'Filter: {valueLabel}. Select for more filter actions.', 26 | values: { valueLabel }, 27 | }); 28 | 29 | if (isDisabled) { 30 | title = `${i18n.translate('maps.filter.filterBar.disabledFilterPrefix', { 31 | defaultMessage: 'Disabled', 32 | })} ${title}`; 33 | } 34 | 35 | return ( 36 | 56 | {valueLabel} 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /public/components/layer_config/base_map_layer_config_panel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { Fragment } from 'react'; 7 | import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; 8 | import { MapLayerSpecification } from '../../model/mapLayerType'; 9 | import { LayerBasicSettings } from './layer_basic_settings'; 10 | 11 | interface Props { 12 | selectedLayerConfig: MapLayerSpecification; 13 | setSelectedLayerConfig: Function; 14 | setIsUpdateDisabled: Function; 15 | isLayerExists: Function; 16 | } 17 | 18 | export const BaseMapLayerConfigPanel = (props: Props) => { 19 | const tabs = [ 20 | { 21 | id: 'settings--id', 22 | name: 'Settings', 23 | content: ( 24 | 25 | 26 | 27 | 28 | ), 29 | }, 30 | ]; 31 | return ; 32 | }; 33 | -------------------------------------------------------------------------------- /public/components/layer_config/cluster_config/cluster_layer_config_panel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { Fragment, useState } from 'react'; 7 | import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; 8 | import { ClusterLayerSpecification } from '../../../model/mapLayerType'; 9 | import { LayerBasicSettings } from '../layer_basic_settings'; 10 | import { ClusterLayerSource } from './cluster_layer_source'; 11 | import { ClusterLayerStyle } from './style'; 12 | import { IndexPattern } from '../../../../../../src/plugins/data/common'; 13 | import { useCallback } from 'react'; 14 | 15 | interface Props { 16 | selectedLayerConfig: ClusterLayerSpecification; 17 | setSelectedLayerConfig: Function; 18 | setIsUpdateDisabled: Function; 19 | isLayerExists: Function; 20 | } 21 | 22 | export const ClusterLayerConfigPanel = (props: Props) => { 23 | const [indexPattern, setIndexPattern] = useState(); 24 | const [dataUpdateDisabled, setDataUpdateDisabled] = useState(true); 25 | 26 | const setIsUpdateDisabled = useCallback( 27 | (isUpdateDisabled: boolean, isFromDataPanel = false) => { 28 | //we can't judge whether source is valid only by selectLayerConfig like documents. We need a state to record it. 29 | if (isFromDataPanel) { 30 | setDataUpdateDisabled(isUpdateDisabled); 31 | props.setIsUpdateDisabled(isUpdateDisabled); 32 | } else { 33 | props.setIsUpdateDisabled(dataUpdateDisabled || isUpdateDisabled); 34 | } 35 | }, 36 | [dataUpdateDisabled] 37 | ); 38 | 39 | const newProps = { 40 | ...props, 41 | setIsUpdateDisabled, 42 | indexPattern, 43 | setIndexPattern, 44 | }; 45 | 46 | const tabs = [ 47 | { 48 | id: 'data-source--id', 49 | name: 'Data', 50 | content: ( 51 | 52 | 53 | 54 | 55 | ), 56 | testsubj: 'dataTab', 57 | }, 58 | { 59 | id: 'style--id', 60 | name: 'Style', 61 | content: ( 62 | 63 | 64 | 65 | 66 | ), 67 | testsubj: 'styleTab', 68 | }, 69 | { 70 | id: 'settings--id', 71 | name: 'Settings', 72 | content: ( 73 | 74 | 75 | 76 | 77 | ), 78 | testsubj: 'settingsTab', 79 | }, 80 | ]; 81 | // @ts-ignore 82 | return ; 83 | }; 84 | -------------------------------------------------------------------------------- /public/components/layer_config/cluster_config/data_source_section.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { 8 | EuiFormRow, 9 | } from '@elastic/eui'; 10 | import { IndexPattern } from '../../../../../../src/plugins/data/public'; 11 | import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; 12 | import { MapServices } from '../../../types'; 13 | import { i18n } from '@osd/i18n'; 14 | import { CanUpdateMapType } from './cluster_layer_source'; 15 | import { useEffect } from 'react'; 16 | 17 | interface Props { 18 | indexPattern: IndexPattern | null | undefined; 19 | setIndexPattern: Function; 20 | setCanUpdateMap: React.Dispatch>; 21 | } 22 | 23 | const errorsMap = { 24 | datasource: ['Required'], 25 | geoFields: ['Required'], 26 | }; 27 | 28 | export const DataSourceSection = ({ indexPattern, setIndexPattern, setCanUpdateMap }: Props) => { 29 | const { 30 | services: { 31 | savedObjects: { client: savedObjectsClient }, 32 | data: { 33 | ui: { IndexPatternSelect }, 34 | indexPatterns, 35 | }, 36 | }, 37 | } = useOpenSearchDashboards(); 38 | 39 | useEffect(() => { 40 | setCanUpdateMap((prev) => ({ 41 | ...prev, 42 | index: !!indexPattern, 43 | })); 44 | }, [indexPattern]); 45 | 46 | return ( 47 | 53 | { 60 | const newIndexPattern = await indexPatterns.get(newIndexPatternId); 61 | setIndexPattern(newIndexPattern); 62 | }} 63 | isClearable={false} 64 | data-test-subj={'indexPatternSelect'} 65 | fullWidth={true} 66 | isInvalid={!indexPattern} 67 | /> 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /public/components/layer_config/cluster_config/filter_section.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { useCallback } from 'react'; 7 | import { EuiSpacer, EuiCompressedFormRow, EuiCompressedCheckbox } from '@elastic/eui'; 8 | import { i18n } from '@osd/i18n'; 9 | import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; 10 | import { MapServices } from '../../../types'; 11 | import { ClusterLayerSpecification } from 'public/model/mapLayerType'; 12 | import { Filter, IndexPattern } from '../../../../../../src/plugins/data/public'; 13 | import { useEffect } from 'react'; 14 | 15 | interface Props { 16 | indexPattern: IndexPattern | null | undefined; 17 | selectedLayerConfig: ClusterLayerSpecification; 18 | setSelectedLayerConfig: Function; 19 | } 20 | 21 | export const FilterSection = ({ 22 | indexPattern, 23 | selectedLayerConfig, 24 | setSelectedLayerConfig, 25 | }: Props) => { 26 | const { 27 | services: { 28 | data: { 29 | ui: { SearchBar }, 30 | }, 31 | }, 32 | } = useOpenSearchDashboards(); 33 | 34 | const onFiltersUpdated = useCallback( 35 | (filters: Filter[]) => { 36 | setSelectedLayerConfig({ 37 | ...selectedLayerConfig, 38 | source: { ...selectedLayerConfig.source, filters }, 39 | }); 40 | }, 41 | [selectedLayerConfig, indexPattern] 42 | ); 43 | 44 | const onToggleGeoBoundingBox = (e: React.ChangeEvent) => { 45 | const source = { ...selectedLayerConfig.source, useGeoBoundingBoxFilter: e.target.checked }; 46 | setSelectedLayerConfig({ ...selectedLayerConfig, source }); 47 | }; 48 | 49 | const onApplyGlobalFilters = (e: React.ChangeEvent) => { 50 | const source = { ...selectedLayerConfig.source, applyGlobalFilters: e.target.checked }; 51 | setSelectedLayerConfig({ ...selectedLayerConfig, source }); 52 | }; 53 | 54 | useEffect(() => { 55 | //if index is changed, reset filters 56 | onFiltersUpdated([]); 57 | }, [indexPattern]); 58 | 59 | return ( 60 | <> 61 | 68 | 69 | 70 | 76 | 77 | 78 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /public/components/layer_config/cluster_config/json_editor.tsx: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright OpenSearch Contributors 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | import 'brace/mode/json'; 8 | 9 | import React, { useState, useMemo, useCallback } from 'react'; 10 | 11 | import { EuiFormRow, EuiIconTip, EuiCodeEditor, EuiScreenReaderOnly } from '@elastic/eui'; 12 | import { i18n } from '@osd/i18n'; 13 | 14 | import 'brace/theme/github'; 15 | 16 | interface Props { 17 | setValue: (value: string) => void; 18 | value: string; 19 | showValidation?: boolean; 20 | setValidity: (validity: boolean) => void; 21 | } 22 | 23 | function JsonEditor({ 24 | showValidation = false, 25 | value = '', 26 | setValidity = () => {}, 27 | setValue, 28 | }: Props) { 29 | const [isFieldValid, setFieldValidity] = useState(true); 30 | const [editorReady, setEditorReady] = useState(false); 31 | 32 | const editorTooltipText = useMemo( 33 | () => 34 | i18n.translate('maps.controls.jsonInputTooltip', { 35 | defaultMessage: 36 | "Any JSON formatted properties you add will be merged with the OpenSearch aggregation definition for this section. For example, 'shard_size' on a terms aggregation.", 37 | }), 38 | [] 39 | ); 40 | 41 | const jsonEditorLabelText = useMemo( 42 | () => 43 | i18n.translate('maps.controls.jsonInputLabel', { 44 | defaultMessage: 'JSON input', 45 | }), 46 | [] 47 | ); 48 | 49 | const label = useMemo( 50 | () => ( 51 | <> 52 | {jsonEditorLabelText}{' '} 53 | 54 | 55 | ), 56 | [jsonEditorLabelText, editorTooltipText] 57 | ); 58 | 59 | const onEditorValidate = useCallback( 60 | (annotations: unknown[]) => { 61 | // The first onValidate returned from EuiCodeEditor is a false negative 62 | if (editorReady) { 63 | const validity = annotations.length === 0; 64 | setFieldValidity(validity); 65 | setValidity(validity); 66 | } else { 67 | setEditorReady(true); 68 | } 69 | }, 70 | [setValidity, editorReady] 71 | ); 72 | 73 | return ( 74 | 80 | <> 81 | 95 | 96 |

{editorTooltipText}

97 |
98 | 99 |
100 | ); 101 | } 102 | 103 | export { JsonEditor }; 104 | -------------------------------------------------------------------------------- /public/components/layer_config/custom_map_config/custom_map_config_panel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { Fragment } from 'react'; 7 | import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; 8 | import { CustomLayerSpecification } from '../../../model/mapLayerType'; 9 | import { LayerBasicSettings } from '../layer_basic_settings'; 10 | import { CustomMapSource } from './custom_map_source'; 11 | 12 | interface Props { 13 | selectedLayerConfig: CustomLayerSpecification; 14 | setSelectedLayerConfig: Function; 15 | setIsUpdateDisabled: Function; 16 | isLayerExists: Function; 17 | } 18 | 19 | export const CustomMapConfigPanel = (props: Props) => { 20 | const newProps = { 21 | ...props, 22 | }; 23 | 24 | const tabs = [ 25 | { 26 | id: 'custom-map-source--id', 27 | name: 'Data', 28 | content: ( 29 | 30 | 31 | 32 | 33 | ), 34 | }, 35 | { 36 | id: 'settings--id', 37 | name: 'Settings', 38 | content: ( 39 | 40 | 41 | 42 | 43 | ), 44 | }, 45 | ]; 46 | return ; 47 | }; 48 | -------------------------------------------------------------------------------- /public/components/layer_config/documents_config/document_layer_config_panel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { Fragment, useState } from 'react'; 7 | import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; 8 | import { DocumentLayerSpecification } from '../../../model/mapLayerType'; 9 | import { LayerBasicSettings } from '../layer_basic_settings'; 10 | import { DocumentLayerSource } from './document_layer_source'; 11 | import { DocumentLayerStyle } from './style/document_layer_style'; 12 | import { IndexPattern } from '../../../../../../src/plugins/data/common'; 13 | 14 | interface Props { 15 | selectedLayerConfig: DocumentLayerSpecification; 16 | setSelectedLayerConfig: Function; 17 | setIsUpdateDisabled: Function; 18 | isLayerExists: Function; 19 | } 20 | 21 | export const DocumentLayerConfigPanel = (props: Props) => { 22 | const { selectedLayerConfig } = props; 23 | 24 | const checkKeys = [ 25 | 'name', 26 | { 27 | key: 'source', 28 | children: ['indexPatternId', 'geoFieldName'], 29 | }, 30 | ]; 31 | const setIsUpdateDisabled = (isUpdateDisabled: boolean) => { 32 | const check = (obj: any, keys: any) => { 33 | return keys.some((key: any) => { 34 | if (typeof key === 'string') { 35 | return !obj[key]; 36 | } else { 37 | return !obj[key.key] || check(obj[key.key], key.children); 38 | } 39 | }); 40 | }; 41 | props.setIsUpdateDisabled(check(selectedLayerConfig, checkKeys) || isUpdateDisabled); 42 | }; 43 | 44 | const [indexPattern, setIndexPattern] = useState(); 45 | 46 | const newProps = { 47 | ...props, 48 | setIsUpdateDisabled, 49 | indexPattern, 50 | setIndexPattern, 51 | }; 52 | 53 | const tabs = [ 54 | { 55 | id: 'data-source--id', 56 | name: 'Data', 57 | content: ( 58 | 59 | 60 | 61 | 62 | ), 63 | testsubj: 'dataTab', 64 | }, 65 | { 66 | id: 'style--id', 67 | name: 'Style', 68 | content: ( 69 | 70 | 71 | 72 | 73 | ), 74 | testsubj: 'styleTab', 75 | }, 76 | { 77 | id: 'settings--id', 78 | name: 'Settings', 79 | content: ( 80 | 81 | 82 | 83 | 84 | ), 85 | testsubj: 'settingsTab', 86 | }, 87 | ]; 88 | return ; 89 | }; 90 | -------------------------------------------------------------------------------- /public/components/layer_config/documents_config/style/color_picker.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { memo, useEffect } from 'react'; 7 | import { EuiColorPicker, EuiCompressedFormRow, useColorPickerState } from '@elastic/eui'; 8 | 9 | export interface ColorPickerProps { 10 | originColor: string; 11 | label: string; 12 | selectedLayerConfigId: string; 13 | setIsUpdateDisabled: Function; 14 | onColorChange: (color: string) => void; 15 | } 16 | 17 | export const ColorPicker = memo( 18 | ({ 19 | originColor, 20 | label, 21 | selectedLayerConfigId, 22 | setIsUpdateDisabled, 23 | onColorChange, 24 | }: ColorPickerProps) => { 25 | const [color, setColor, colorErrors] = useColorPickerState(originColor); 26 | 27 | useEffect(() => { 28 | onColorChange(String(color)); 29 | }, [color]); 30 | 31 | useEffect(() => { 32 | setIsUpdateDisabled(!!colorErrors); 33 | }, [colorErrors]); 34 | 35 | // It's used to update the style color when switch layer config between different document layers 36 | useEffect(() => { 37 | setColor(originColor, !!colorErrors ? { isValid: false } : { isValid: true }); 38 | }, [selectedLayerConfigId]); 39 | 40 | return ( 41 | 42 | 49 | 50 | ); 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /public/components/layer_config/documents_config/style/label_config.scss: -------------------------------------------------------------------------------- 1 | .documentsLabel__text { 2 | overflow: hidden 3 | } 4 | -------------------------------------------------------------------------------- /public/components/layer_config/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { LayerConfigPanel } from './layer_config_panel'; 7 | export { BaseMapLayerConfigPanel } from './base_map_layer_config_panel'; 8 | -------------------------------------------------------------------------------- /public/components/layer_control_panel/delete_layer_modal.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { DeleteLayerModal } from './delete_layer_modal'; 7 | import React from 'react'; 8 | import { EuiConfirmModal } from '@elastic/eui'; 9 | import TestRenderer from 'react-test-renderer'; 10 | 11 | describe('test delete layer modal', function () { 12 | it('should show modal', function () { 13 | const deleteLayerModal = TestRenderer.create( 14 | {}} onConfirm={() => {}} /> 15 | ); 16 | const testInstance = deleteLayerModal.root; 17 | //expect(testInstance.findByType(EuiConfirmModal).props.title).toBe('Delete layer'); 18 | expect(testInstance.findByType(EuiConfirmModal).props.title.props.children.props.children).toBe('Delete layer'); 19 | expect(testInstance.findByType(EuiConfirmModal).props.confirmButtonText).toBe('Delete'); 20 | expect(testInstance.findByType(EuiConfirmModal).props.cancelButtonText).toBe('Cancel'); 21 | expect(testInstance.findByType(EuiConfirmModal).props.buttonColor).toBe('danger'); 22 | expect(testInstance.findByType(EuiConfirmModal).props.defaultFocusedButton).toBe('confirm'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /public/components/layer_control_panel/delete_layer_modal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiConfirmModal, EuiText } from '@elastic/eui'; 7 | import React from 'react'; 8 | 9 | interface DeleteLayerModalProps { 10 | onCancel: () => void; 11 | onConfirm: () => void; 12 | layerName: string; 13 | } 14 | export const DeleteLayerModal = ({ onCancel, onConfirm, layerName }: DeleteLayerModalProps) => { 15 | return ( 16 | 19 |

Delete layer

20 | 21 | } 22 | onCancel={onCancel} 23 | onConfirm={onConfirm} 24 | cancelButtonText="Cancel" 25 | confirmButtonText="Delete" 26 | buttonColor="danger" 27 | defaultFocusedButton="confirm" 28 | > 29 | 30 |

31 | Do you want to delete layer {layerName}? 32 |

33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /public/components/layer_control_panel/hide_layer_button.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | const mockStyle = { 7 | layers: [ 8 | { 9 | id: 'layer-1', 10 | type: 'fill', 11 | source: 'layer-1', 12 | }, 13 | ], 14 | }; 15 | // Need put this mock before import HideLayerButton, or it will not work 16 | jest.mock('maplibre-gl', () => ({ 17 | Map: jest.fn(() => ({ 18 | on: jest.fn(), 19 | off: jest.fn(), 20 | getStyle: jest.fn(() => mockStyle), 21 | setLayoutProperty: jest.fn(), 22 | })), 23 | })); 24 | 25 | import { EuiButtonIcon } from '@elastic/eui'; 26 | import React from 'react'; 27 | import { LAYER_VISIBILITY } from '../../../common'; 28 | import { HideLayer } from './hide_layer_button'; 29 | import { MapLayerSpecification } from '../../model/mapLayerType'; 30 | import TestRenderer from 'react-test-renderer'; 31 | import { Map as MapLibre } from 'maplibre-gl'; 32 | 33 | describe('HideLayerButton', () => { 34 | it('should toggle layer visibility on button click', () => { 35 | const exampleLayer: MapLayerSpecification = { 36 | name: 'Layer 1', 37 | id: 'layer-1', 38 | type: 'opensearch_vector_tile_map', 39 | description: 'Some description', 40 | source: { 41 | dataURL: 'https:foo.bar', 42 | }, 43 | style: { 44 | styleURL: 'https://example.com/style.json', 45 | }, 46 | zoomRange: [0, 22], 47 | visibility: LAYER_VISIBILITY.VISIBLE, 48 | opacity: 100, 49 | }; 50 | 51 | const map = new MapLibre({ 52 | container: document.createElement('div'), 53 | style: { 54 | layers: [], 55 | version: 8 as 8, 56 | sources: {}, 57 | }, 58 | }); 59 | 60 | const maplibreRef = { 61 | current: map, 62 | }; 63 | 64 | const updateLayerVisibility = jest.fn(); 65 | 66 | const hideButton = TestRenderer.create( 67 | 72 | ); 73 | 74 | const button = hideButton.root.findByType(EuiButtonIcon); 75 | 76 | expect(button.props.title).toBe('Hide layer'); 77 | expect(button.props.iconType).toBe('eyeClosed'); 78 | 79 | button.props.onClick(); 80 | 81 | expect(button.props.title).toBe('Show layer'); 82 | expect(button.props.iconType).toBe('eye'); 83 | expect(updateLayerVisibility).toBeCalledTimes(1); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /public/components/layer_control_panel/hide_layer_button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; 7 | import React, { useState } from 'react'; 8 | import { 9 | LAYER_PANEL_HIDE_LAYER_ICON, 10 | LAYER_PANEL_SHOW_LAYER_ICON, 11 | LAYER_VISIBILITY, 12 | } from '../../../common'; 13 | import { MapLayerSpecification } from '../../model/mapLayerType'; 14 | import { updateLayerVisibilityHandler } from '../../model/map/layer_operations'; 15 | import { MaplibreRef } from '../../model/layersFunctions'; 16 | 17 | interface HideLayerProps { 18 | layer: MapLayerSpecification; 19 | maplibreRef: MaplibreRef; 20 | updateLayerVisibility: (layerId: string, visibility: string) => void; 21 | } 22 | 23 | export const HideLayer = ({ layer, maplibreRef, updateLayerVisibility }: HideLayerProps) => { 24 | const [layerVisibility, setLayerVisibility] = useState( 25 | new Map([[layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE]]) 26 | ); 27 | 28 | const onLayerVisibilityChange = () => { 29 | let updatedVisibility: string; 30 | if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { 31 | updatedVisibility = LAYER_VISIBILITY.NONE; 32 | } else { 33 | updatedVisibility = LAYER_VISIBILITY.VISIBLE; 34 | } 35 | setLayerVisibility( 36 | new Map(layerVisibility.set(layer.id, updatedVisibility === LAYER_VISIBILITY.VISIBLE)) 37 | ); 38 | updateLayerVisibility(layer.id, updatedVisibility); 39 | updateLayerVisibilityHandler(maplibreRef.current!, layer.id, updatedVisibility); 40 | }; 41 | 42 | return ( 43 | 44 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /public/components/layer_control_panel/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { LayerControlPanel } from './layer_control_panel'; 7 | -------------------------------------------------------------------------------- /public/components/layer_control_panel/layer_control_panel.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | .layerControlPanel--show { 4 | pointer-events: auto; 5 | width: $euiSizeL * 11; 6 | 7 | .layerControlPanel__title { 8 | padding: $euiSizeM $euiSizeM 9 | } 10 | 11 | .layerControlPanel__selected { 12 | background-color: $euiColorLightShade; 13 | } 14 | 15 | .layerControlPanel__layerFunctionButton { 16 | height: $euiSizeL; 17 | width: $euiSizeL; 18 | } 19 | 20 | .layerControlPanel__layerTypeIcon { 21 | padding-left: $euiSizeM; 22 | } 23 | 24 | .euiListGroupItem__label { 25 | width: $euiSizeL * 6; 26 | } 27 | 28 | .euiDroppable { 29 | overflow-y: auto; 30 | overflow-x: hidden; 31 | } 32 | 33 | .euiFlexGroup--directionColumn { 34 | max-height: calc(100vh - #{$mapHeaderOffset} - #{$euiSizeL}); 35 | } 36 | } 37 | 38 | .layerControlPanel--hide { 39 | pointer-events: auto; 40 | 41 | .layerControlPanel__visButton { 42 | background-color: $euiColorEmptyShade; 43 | color: $euiTextColor; 44 | border-color: $euiColorLightShade; 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /public/components/map_container/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { MapContainer } from './map_container'; 7 | -------------------------------------------------------------------------------- /public/components/map_container/legend/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../_variables.scss'; 2 | 3 | .maps-legends { 4 | z-index: 1; 5 | position: absolute; 6 | right: calc(#{$euiSizeXXL} + #{$euiSizeM}); 7 | bottom: calc(#{$euiSizeXXL} + #{$euiSizeM}); 8 | background: rgba(255, 255, 255, 0.9); 9 | border-radius: 4px; 10 | box-shadow: 0 6px 12px -1px rgba(65, 78, 101, 0.1), 0 4px 4px -1px rgba(65, 78, 101, 0.1), 11 | 0 2px 2px 0 rgba(65, 78, 101, 0.1); 12 | padding: $euiSizeS; 13 | height: auto; 14 | //min-height: 120px; 15 | max-height: calc(100% - #{$euiSizeXXL} - #{$euiSizeXXL}); 16 | min-width: 180px; 17 | max-width: 250px; 18 | overflow-y: auto; 19 | overflow-x: hidden; 20 | pointer-events: auto; 21 | 22 | /* Customize scrollbar for better visibility */ 23 | &::-webkit-scrollbar { 24 | width: 6px; 25 | } 26 | 27 | &::-webkit-scrollbar-track { 28 | background: rgba(255, 255, 255, 0.1); 29 | border-radius: 3px; 30 | } 31 | 32 | &::-webkit-scrollbar-thumb { 33 | background: rgba(0, 0, 0, 0.2); 34 | border-radius: 3px; 35 | } 36 | 37 | &::-webkit-scrollbar-thumb:hover { 38 | background: rgba(0, 0, 0, 0.3); 39 | } 40 | 41 | .legend-item { 42 | display: flex; 43 | align-items: center; 44 | padding: 2px 0; 45 | height: $euiSizeL; 46 | } 47 | 48 | .legend-title { 49 | font-weight: 700; 50 | font-size: 14px; 51 | margin-bottom: $euiSizeS; 52 | height: 20px; 53 | } 54 | 55 | .legend-label { 56 | font-weight: 400; 57 | font-size: 14px; 58 | margin-left: $euiSizeXS; 59 | word-break: break-word; 60 | line-height: 20px; 61 | } 62 | } 63 | 64 | /* Handle embedded mode */ 65 | :global(.embeddable-container) .maps-legends { 66 | max-height: calc(100% - #{$euiSizeXXL} - #{$euiSizeXXL}); 67 | right: calc(#{$euiSizeXXL} + #{$euiSizeM}); 68 | bottom: calc(#{$euiSizeXXL} + #{$euiSizeM}); 69 | margin: 0; 70 | z-index: 1000; 71 | } 72 | -------------------------------------------------------------------------------- /public/components/map_container/legend/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useImperativeHandle, useState } from 'react'; 2 | import { cloneDeep } from 'lodash'; 3 | import { LegendList } from './legend_list'; 4 | import './index.scss'; 5 | import { MapLayerSpecification } from '../../../model/mapLayerType'; 6 | import { DASHBOARDS_MAPS_LAYER_TYPE, LAYER_VISIBILITY } from '../../../../common'; 7 | 8 | interface Props { 9 | layers: MapLayerSpecification[]; 10 | zoom: number; 11 | } 12 | 13 | export interface LegendOption { 14 | id: string; 15 | title: string; 16 | list: { 17 | label: string; 18 | color: string; 19 | }[]; 20 | } 21 | 22 | export interface MapsLegendHandle { 23 | updateLegends: (legend: LegendOption, isEmpty: boolean) => void; 24 | deleteLegend: (id: string) => void; 25 | } 26 | 27 | const MapsLegend = forwardRef(({layers, zoom}, ref) => { 28 | const [legends, setLegends] = useState([]); 29 | useImperativeHandle( 30 | ref, 31 | () => { 32 | return { 33 | updateLegends: (option: LegendOption, isEmpty: boolean) => { 34 | const newLegends = cloneDeep(legends); 35 | const { id } = option; 36 | const index = newLegends.findIndex((item) => item.id === id); 37 | if (index === -1) { 38 | if (!isEmpty) { 39 | newLegends.push(option); 40 | } 41 | } else { 42 | if (isEmpty) { 43 | newLegends.splice(index, 1); 44 | } else { 45 | newLegends.splice(index, 1, option); 46 | } 47 | } 48 | setLegends(newLegends); 49 | }, 50 | deleteLegend: (layerId: string) => { 51 | const newLegends = cloneDeep(legends); 52 | const index = newLegends.findIndex((item) => item.id === layerId); 53 | if (index > -1) { 54 | newLegends.splice(index, 1); 55 | } 56 | setLegends(newLegends); 57 | }, 58 | }; 59 | }, 60 | [legends, setLegends] 61 | ); 62 | 63 | const visibleLegends = legends.filter(item => { 64 | const layer = layers.find(l => l.id === item.id); 65 | return layer && 66 | layer.type === DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER && 67 | layer.visibility === LAYER_VISIBILITY.VISIBLE && 68 | zoom >= layer.zoomRange[0] && 69 | zoom <= layer.zoomRange[1]; 70 | }); 71 | 72 | if (visibleLegends.length === 0) { 73 | return null; 74 | } 75 | 76 | return ( 77 |
78 | {visibleLegends.map((item, index) => { 79 | const layer = layers.find(l => l.id === item.id); 80 | return ; 86 | })} 87 |
88 | ); 89 | }); 90 | 91 | export { MapsLegend }; 92 | -------------------------------------------------------------------------------- /public/components/map_container/legend/legend_item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EuiText, EuiIcon } from '@elastic/eui'; 3 | 4 | interface Props { 5 | color: string; 6 | label: string; 7 | } 8 | 9 | export const LegendItem = ({ color, label }: Props) => { 10 | return ( 11 |
12 | 13 | {label} 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /public/components/map_container/legend/legend_list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LegendItem } from './legend_item'; 3 | import { LegendOption } from './index'; 4 | import { EuiHorizontalRule } from '@elastic/eui'; 5 | import { MapLayerSpecification } from '../../../model/mapLayerType'; 6 | import { DASHBOARDS_MAPS_LAYER_TYPE, LAYER_VISIBILITY } from '../../../../common'; 7 | 8 | interface Props { 9 | legend: LegendOption; 10 | isLastOne: boolean; 11 | layer: MapLayerSpecification; 12 | } 13 | 14 | export const LegendList = ({ legend, isLastOne, layer }: Props) => { 15 | const isVisibleClusterLayer = layer.type === DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER && layer.visibility === LAYER_VISIBILITY.VISIBLE; 16 | 17 | if (!isVisibleClusterLayer) { 18 | return null; 19 | } 20 | 21 | return ( 22 | <> 23 |
{legend.title}
24 | {legend.list.map((item, index) => { 25 | return ; 26 | })} 27 | {isLastOne ? null : } 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /public/components/map_container/map_container.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | @import "maplibre-gl/dist/maplibre-gl.css"; 7 | @import "../../variables"; 8 | 9 | .mapAppContainer, .map-page, .map-container, .map-main{ 10 | position: relative; 11 | display: flex; 12 | flex-direction: column; 13 | flex: 1; 14 | } 15 | 16 | .maplibregl-ctrl-top-left { 17 | left: $euiSizeS; 18 | top: $euiSizeS; 19 | } 20 | 21 | .maplibregl-ctrl-zoom-in { 22 | background: lightOrDarkTheme($ouiColorEmptyShade, $ouiColorLightestShade) !important; 23 | .maplibregl-ctrl-icon { 24 | background-image: lightOrDarkTheme( 25 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E"), 26 | url("data:image/svg+xml,%0A%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")) 27 | !important; 28 | } 29 | } 30 | 31 | .maplibregl-ctrl-zoom-out { 32 | background: lightOrDarkTheme($euiColorEmptyShade, $ouiColorLightestShade) !important; 33 | .maplibregl-ctrl-icon { 34 | background-image: lightOrDarkTheme( 35 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E"), 36 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")) 37 | !important; 38 | } 39 | } 40 | 41 | .maplibregl-ctrl-compass { 42 | background: lightOrDarkTheme($euiColorEmptyShade, $ouiColorLightestShade) !important; 43 | background-image: lightOrDarkTheme( 44 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E"), 45 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")) !important; 46 | } 47 | 48 | .layerControlPanel-container { 49 | z-index: 1; 50 | position: absolute; 51 | margin-left: $euiSizeS; 52 | margin-top: $euiSizeS; 53 | } 54 | 55 | .zoombar { 56 | z-index: 1; 57 | position: absolute; 58 | bottom: $euiSizeM; 59 | right: $euiSizeS; 60 | } 61 | 62 | .SpatialFilterToolbar-container { 63 | z-index: 1; 64 | position: absolute; 65 | top: (2 * $euiSizeS) + ($euiSizeXL * 4); 66 | right: $euiSizeS; 67 | } 68 | 69 | .drawFilterShapeHelper { 70 | z-index: 1; 71 | position: absolute; 72 | transform: translate(-50%, -50%); 73 | bottom: 1px; 74 | left: 50%; 75 | } 76 | -------------------------------------------------------------------------------- /public/components/map_container/maps_footer.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { render, unmountComponentAtNode } from 'react-dom'; 8 | import { act } from 'react-dom/test-utils'; 9 | 10 | import { MapsFooter } from './maps_footer'; 11 | import { Map as Maplibre } from 'maplibre-gl'; 12 | 13 | let container: Element | null; 14 | let mockMap: Maplibre; 15 | const mockCallbackMap: Map void> = new Map void>(); 16 | beforeEach(() => { 17 | // setup a DOM element as a render target 18 | container = document.createElement('div'); 19 | document.body.appendChild(container); 20 | mockCallbackMap.clear(); 21 | mockMap = ({ 22 | on: (eventType: string, callback: () => void) => { 23 | mockCallbackMap.set(eventType, callback); 24 | }, 25 | off: (eventType: string, callback: () => void) => { 26 | mockCallbackMap.delete(eventType); 27 | }, 28 | } as unknown) as Maplibre; 29 | }); 30 | 31 | afterEach(() => { 32 | // cleanup on exiting 33 | unmountComponentAtNode(container!); 34 | container?.remove(); 35 | container = null; 36 | }); 37 | 38 | it('renders map footer', () => { 39 | act(() => { 40 | render(, container); 41 | }); 42 | expect(container?.textContent).toBe('zoom: 2'); 43 | expect(mockCallbackMap.size).toEqual(1); 44 | }); 45 | 46 | it('clean up is called', () => { 47 | act(() => { 48 | render(, container); 49 | }); 50 | unmountComponentAtNode(container!); 51 | expect(mockCallbackMap.size).toEqual(0); 52 | }); 53 | -------------------------------------------------------------------------------- /public/components/map_container/maps_footer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiPanel } from '@elastic/eui'; 7 | import React, { memo, useEffect, useState } from 'react'; 8 | import { LngLat, MapEventType } from 'maplibre-gl'; 9 | import { Map as Maplibre } from 'maplibre-gl'; 10 | 11 | const coordinatesRoundOffDigit = 4; 12 | interface MapFooterProps { 13 | map: Maplibre; 14 | zoom: number; 15 | } 16 | 17 | export const MapsFooter = memo(({ map, zoom }: MapFooterProps) => { 18 | const [coordinates, setCoordinates] = useState(); 19 | useEffect(() => { 20 | function onMouseMoveMap(e: MapEventType['mousemove']) { 21 | setCoordinates(e.lngLat.wrap()); 22 | } 23 | 24 | if (map) { 25 | map.on('mousemove', onMouseMoveMap); 26 | } 27 | return () => { 28 | if (map) { 29 | map.off('mousemove', onMouseMoveMap); 30 | } 31 | }; 32 | }, []); 33 | 34 | return ( 35 | 42 | 43 | {coordinates && 44 | `lat: ${coordinates.lat.toFixed( 45 | coordinatesRoundOffDigit 46 | )}, lon: ${coordinates.lng.toFixed(coordinatesRoundOffDigit)}, `} 47 | zoom: {zoom} 48 | 49 | 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /public/components/map_container/maps_messages.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiLink, EuiText } from '@elastic/eui'; 7 | import React from 'react'; 8 | import { ToastInputFields } from '../../../../../src/core/public'; 9 | import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; 10 | 11 | export const MapsServiceErrorMsg: ToastInputFields = { 12 | title: 'The OpenSearch Maps Service is currently not available in your region', 13 | text: toMountPoint( 14 |

15 | You can configure OpenSearch Dashboards to use a{' '} 16 | 20 | custom map 21 | {' '} 22 | server for dashboards maps. 23 |

24 | ), 25 | }; 26 | -------------------------------------------------------------------------------- /public/components/map_page/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { MapPage, MapComponent } from './map_page'; 7 | -------------------------------------------------------------------------------- /public/components/map_top_nav/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { MapTopNavMenu } from './top_nav_menu'; 7 | -------------------------------------------------------------------------------- /public/components/maps_list/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { MapsList } from './maps_list'; 7 | -------------------------------------------------------------------------------- /public/components/show_error_modal.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { ShowErrorModal } from './show_error_modal'; 8 | import renderer, { act } from 'react-test-renderer'; 9 | import { fireEvent } from '@testing-library/dom'; 10 | import { screen, render } from '@testing-library/react'; 11 | 12 | describe('display error modal', () => { 13 | const props = { 14 | modalTitle: 'testModalTitle', 15 | modalBody: 'testModalBody', 16 | buttonText: 'testButtonText', 17 | }; 18 | 19 | it('renders the error modal based on the props passed', async () => { 20 | let tree; 21 | await act(async () => { 22 | tree = renderer.create(); 23 | }); 24 | expect(tree.toJSON().props.id).toBe('showModalOption'); 25 | expect(tree.toJSON()).toMatchSnapshot(); 26 | }); 27 | 28 | it('renders the error modal when showModal button is clicked', () => { 29 | render(); 30 | const button = screen.getByRole('button'); 31 | fireEvent.click(button); 32 | const closeButton = screen.getByTestId('closeModal'); 33 | fireEvent.click(closeButton); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /public/components/show_error_modal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { useState } from 'react'; 7 | import { 8 | EuiSmallButton, 9 | EuiButton, 10 | EuiModal, 11 | EuiModalBody, 12 | EuiModalFooter, 13 | EuiModalHeader, 14 | EuiModalHeaderTitle, 15 | EuiCodeBlock, 16 | EuiText, 17 | } from '@elastic/eui'; 18 | 19 | export interface ShowErrorModalProps { 20 | modalTitle: string; 21 | modalBody: string; 22 | buttonText: string; 23 | } 24 | 25 | const ShowErrorModal = (props: ShowErrorModalProps) => { 26 | const [isModalVisible, setIsModalVisible] = useState(false); 27 | 28 | const closeModal = () => setIsModalVisible(false); 29 | const showModal = () => setIsModalVisible(true); 30 | 31 | let modal; 32 | 33 | if (isModalVisible) { 34 | modal = ( 35 | 36 | 37 | 38 | 39 |

{props.modalTitle}

40 |
41 |
42 |
43 | 44 | 45 | {props.modalBody} 46 | 47 | 48 | 49 | 55 | Close 56 | 57 | 58 |
59 | ); 60 | } 61 | 62 | return ( 63 |
64 | 65 | {props.buttonText} 66 | 67 | {modal} 68 |
69 | ); 70 | }; 71 | 72 | export { ShowErrorModal }; 73 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/__snapshots__/filter_toolbar.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SpatialFilterToolbar tests renders spatial filter before drawing 1`] = ` 4 | 10 | 11 | 20 | 21 | 22 | 31 | 32 | 33 | `; 34 | 35 | exports[`SpatialFilterToolbar tests renders spatial filter while drawing 1`] = ` 36 | 42 | 43 | 52 | 53 | 54 | 63 | 64 | 65 | `; 66 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/display_draw_helper.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; 7 | import React, { memo, useEffect } from 'react'; 8 | import { i18n } from '@osd/i18n'; 9 | import { Map as Maplibre } from 'maplibre-gl'; 10 | import { FILTER_DRAW_MODE } from '../../../../common'; 11 | 12 | interface DrawFilterShapeHelper { 13 | map: Maplibre; 14 | mode: FILTER_DRAW_MODE; 15 | onCancel: () => void; 16 | } 17 | 18 | const getHelpText = (mode: FILTER_DRAW_MODE): string => { 19 | switch (mode) { 20 | case FILTER_DRAW_MODE.POLYGON: 21 | return i18n.translate('maps.drawFilterPolygon.tooltipContent', { 22 | defaultMessage: 'Click to start shape. Click for vertex. Double click to finish.', 23 | }); 24 | case FILTER_DRAW_MODE.RECTANGLE: 25 | return i18n.translate('maps.drawFilterRectangle.tooltipContent', { 26 | defaultMessage: 'Click to start rectangle. Move mouse to adjust size. Click to finish.', 27 | }); 28 | default: 29 | return i18n.translate('maps.drawFilterDefault.tooltipContent', { 30 | defaultMessage: 'Click to start shape. Double click to finish.', 31 | }); 32 | } 33 | }; 34 | export const DrawFilterShapeHelper = memo(({ map, mode, onCancel }: DrawFilterShapeHelper) => { 35 | useEffect(() => { 36 | if (mode !== FILTER_DRAW_MODE.NONE) { 37 | map.getCanvas().style.cursor = 'crosshair'; // Changes cursor to '+' 38 | } else { 39 | map.getCanvas().style.cursor = ''; 40 | } 41 | }, [mode]); 42 | 43 | return mode === FILTER_DRAW_MODE.NONE ? null : ( 44 | 50 | 51 | 52 | {getHelpText(mode)} 53 | 54 | 55 | 56 | {'Cancel'} 57 | 58 | 59 | 60 | 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/draw_filter_shape.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { Fragment, useEffect, useRef } from 'react'; 7 | import { IControl, Map as Maplibre } from 'maplibre-gl'; 8 | import MapboxDraw from '@mapbox/mapbox-gl-draw'; 9 | import { Feature } from 'geojson'; 10 | import { GeoShapeRelation } from '@opensearch-project/opensearch/api/types'; 11 | import { 12 | DrawFilterProperties, 13 | FILTER_DRAW_MODE, 14 | MAPBOX_GL_DRAW_MODES, 15 | MAPBOX_GL_DRAW_CREATE_LISTENER, 16 | } from '../../../../common'; 17 | import { DrawRectangle } from '../../draw/modes/rectangle'; 18 | import {DRAW_SHAPE_STYLE} from "./draw_style"; 19 | import { GeoShapeFilter, ShapeFilter } from '../../../../../../src/plugins/data/common'; 20 | 21 | interface DrawFilterShapeProps { 22 | filterProperties: DrawFilterProperties; 23 | map: Maplibre; 24 | updateFilterProperties: (properties: DrawFilterProperties) => void; 25 | addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; 26 | } 27 | 28 | function getMapboxDrawMode(mode: FILTER_DRAW_MODE): string { 29 | switch (mode) { 30 | case FILTER_DRAW_MODE.POLYGON: 31 | return MAPBOX_GL_DRAW_MODES.DRAW_POLYGON; 32 | case FILTER_DRAW_MODE.RECTANGLE: 33 | return MAPBOX_GL_DRAW_MODES.DRAW_RECTANGLE; 34 | default: 35 | return MAPBOX_GL_DRAW_MODES.SIMPLE_SELECT; 36 | } 37 | } 38 | export const isGeoShapeFilter = (filter: any): filter is GeoShapeFilter => filter?.geo_shape; 39 | 40 | const isShapeFilter = (geometry: any): geometry is ShapeFilter => { 41 | return geometry && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon'); 42 | }; 43 | 44 | const toGeoShapeRelation = (relation?: string): GeoShapeRelation => { 45 | switch (relation) { 46 | case 'intersects': 47 | return relation; 48 | case 'within': 49 | return relation; 50 | case 'disjoint': 51 | return relation; 52 | default: 53 | return 'intersects'; 54 | } 55 | }; 56 | 57 | export const DrawFilterShape = ({ 58 | filterProperties, 59 | map, 60 | updateFilterProperties, 61 | addSpatialFilter, 62 | }: DrawFilterShapeProps) => { 63 | const onDraw = (event: { features: Feature[] }) => { 64 | event.features.map((feature) => { 65 | if (isShapeFilter(feature.geometry)) { 66 | addSpatialFilter( 67 | feature.geometry, 68 | filterProperties.filterLabel || null, 69 | toGeoShapeRelation(filterProperties.relation) 70 | ); 71 | } 72 | }); 73 | updateFilterProperties({ 74 | mode: FILTER_DRAW_MODE.NONE, 75 | }); 76 | }; 77 | const mapboxDrawRef = useRef( 78 | new MapboxDraw({ 79 | displayControlsDefault: false, 80 | modes: { 81 | ...MapboxDraw.modes, 82 | [MAPBOX_GL_DRAW_MODES.DRAW_RECTANGLE]: DrawRectangle, 83 | }, 84 | styles: DRAW_SHAPE_STYLE, 85 | }) 86 | ); 87 | 88 | useEffect(() => { 89 | if (map) { 90 | map.addControl((mapboxDrawRef.current as unknown) as IControl, 'top-right'); 91 | map.on(MAPBOX_GL_DRAW_CREATE_LISTENER, onDraw); 92 | } 93 | return () => { 94 | if (map) { 95 | map.off(MAPBOX_GL_DRAW_CREATE_LISTENER, onDraw); 96 | // eslint-disable-next-line react-hooks/exhaustive-deps 97 | map.removeControl((mapboxDrawRef.current as unknown) as IControl); 98 | } 99 | }; 100 | }, []); 101 | 102 | useEffect(() => { 103 | const mapboxDrawMode: string = getMapboxDrawMode(filterProperties.mode); 104 | mapboxDrawRef.current.changeMode(mapboxDrawMode); 105 | }, [filterProperties.mode]); 106 | 107 | return ; 108 | }; 109 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/draw_style.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | export const DRAW_SHAPE_STYLE = [ 6 | { 7 | id: 'gl-draw-line', 8 | type: 'line', 9 | filter: ['all', ['==', '$type', 'LineString'], ['!=', 'mode', 'static']], 10 | layout: { 11 | 'line-cap': 'round', 12 | 'line-join': 'round', 13 | }, 14 | paint: { 15 | 'line-color': '#845D10', 16 | 'line-dasharray': [0.2, 2], 17 | 'line-width': 2, 18 | }, 19 | }, 20 | // polygon fill 21 | { 22 | id: 'gl-draw-polygon-fill', 23 | type: 'fill', 24 | filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], 25 | paint: { 26 | 'fill-color': '#845D10', 27 | 'fill-outline-color': '#845D10', 28 | 'fill-opacity': 0.2, 29 | }, 30 | }, 31 | // polygon outline stroke 32 | // This doesn't style the first edge of the polygon, which uses the line stroke styling instead 33 | { 34 | id: 'gl-draw-polygon-stroke-active', 35 | type: 'line', 36 | filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], 37 | layout: { 38 | 'line-cap': 'round', 39 | 'line-join': 'round', 40 | }, 41 | paint: { 42 | 'line-color': '#845D10', 43 | 'line-dasharray': [0.2, 2], 44 | 'line-width': 2, 45 | }, 46 | }, 47 | // vertex points 48 | { 49 | id: 'gl-draw-polygon-and-line-vertex-active', 50 | type: 'circle', 51 | filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], 52 | paint: { 53 | 'circle-radius': 3, 54 | 'circle-color': '#845D10', 55 | }, 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/filter-by_shape.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import renderer from 'react-test-renderer'; 6 | import React from 'react'; 7 | import { FilterByShape } from './filter_by_shape'; 8 | import { 9 | DRAW_FILTER_POLYGON, 10 | DRAW_FILTER_POLYGON_DEFAULT_LABEL, 11 | DRAW_FILTER_RECTANGLE, 12 | DRAW_FILTER_RECTANGLE_DEFAULT_LABEL, 13 | FILTER_DRAW_MODE, 14 | } from '../../../../common'; 15 | 16 | describe('render polygon', function () { 17 | it('renders filter by polygon option', () => { 18 | const mockCallback = jest.fn(); 19 | const polygonComponent = renderer 20 | .create( 21 | 29 | ) 30 | .toJSON(); 31 | expect(polygonComponent).toMatchSnapshot(); 32 | }); 33 | 34 | it('renders filter by polygon in middle of drawing', () => { 35 | const mockCallback = jest.fn(); 36 | const polygonComponent = renderer 37 | .create( 38 | 46 | ) 47 | .toJSON(); 48 | expect(polygonComponent).toMatchSnapshot(); 49 | }); 50 | }); 51 | 52 | describe('render rectangle', function () { 53 | it('renders filter by rectangle option', () => { 54 | const mockCallback = jest.fn(); 55 | const rectangle = renderer 56 | .create( 57 | 65 | ) 66 | .toJSON(); 67 | expect(rectangle).toMatchSnapshot(); 68 | }); 69 | 70 | it('renders filter by rectangle in middle of drawing', () => { 71 | const mockCallback = jest.fn(); 72 | const rectangle = renderer 73 | .create( 74 | 82 | ) 83 | .toJSON(); 84 | expect(rectangle).toMatchSnapshot(); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/filter_by_shape.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui'; 7 | import React, { useState } from 'react'; 8 | import { 9 | DRAW_FILTER_CANCEL, 10 | DRAW_FILTER_SHAPE_TITLE, 11 | DRAW_FILTER_SPATIAL_RELATIONS, 12 | DrawFilterProperties, 13 | FILTER_DRAW_MODE, 14 | } from '../../../../common'; 15 | import { FilterInputPanel } from './filter_input_panel'; 16 | 17 | interface FilterByShapeProps { 18 | setDrawFilterProperties: (properties: DrawFilterProperties) => void; 19 | mode: FILTER_DRAW_MODE; 20 | shapeMode: FILTER_DRAW_MODE; 21 | shapeLabel: string; 22 | defaultLabel: string; 23 | iconType: any; 24 | isDarkMode: boolean; 25 | } 26 | 27 | export const FilterByShape = ({ 28 | shapeMode, 29 | setDrawFilterProperties, 30 | mode, 31 | defaultLabel, 32 | iconType, 33 | shapeLabel, 34 | isDarkMode, 35 | }: FilterByShapeProps) => { 36 | const [isPopoverOpen, setPopover] = useState(false); 37 | const isFilterActive: boolean = mode === shapeMode; 38 | 39 | const onClick = () => { 40 | if (isFilterActive) { 41 | setDrawFilterProperties({ 42 | mode: FILTER_DRAW_MODE.NONE, 43 | }); 44 | return; 45 | } 46 | setPopover(!isPopoverOpen); 47 | }; 48 | 49 | const closePopover = () => { 50 | setPopover(false); 51 | }; 52 | 53 | const onSubmit = (input: { relation: string; label: string; mode: FILTER_DRAW_MODE }) => { 54 | setDrawFilterProperties({ 55 | mode: input.mode, 56 | relation: input.relation, 57 | filterLabel: input.label, 58 | }); 59 | closePopover(); 60 | }; 61 | 62 | const panels = [ 63 | { 64 | id: 0, 65 | title: DRAW_FILTER_SHAPE_TITLE, 66 | content: ( 67 | 74 | ), 75 | }, 76 | ]; 77 | 78 | const drawShapeButton = ( 79 | 80 | 92 | 93 | ); 94 | return ( 95 | 104 | 105 | 106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/filter_input_panel.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { EuiButton, EuiForm, EuiFormRow, EuiPanel, EuiSelect } from '@elastic/eui'; 8 | 9 | import { FilterInputPanel } from './filter_input_panel'; 10 | import { FILTER_DRAW_MODE } from '../../../../common'; 11 | import { shallow } from 'enzyme'; 12 | 13 | it('renders filter input panel', () => { 14 | const mockRelations: string[] = ['sample', 'list', 'of', 'relations']; 15 | const mockCallback = jest.fn(); 16 | const wrapper = shallow( 17 | 24 | ); 25 | 26 | const panel = wrapper.find(EuiPanel); 27 | expect(panel.find(EuiForm).find(EuiFormRow).length).toEqual(3); 28 | wrapper.find(EuiButton).simulate('click', {}); 29 | expect(mockCallback).toHaveBeenCalledTimes(1); 30 | const actualRelations = wrapper.find(EuiSelect).prop('options'); 31 | expect(actualRelations).toEqual([ 32 | { 33 | value: 'sample', 34 | text: 'sample', 35 | }, 36 | { 37 | value: 'list', 38 | text: 'list', 39 | }, 40 | { 41 | value: 'of', 42 | text: 'of', 43 | }, 44 | { 45 | value: 'relations', 46 | text: 'relations', 47 | }, 48 | ]); 49 | }); 50 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/filter_input_panel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { useState } from 'react'; 7 | import { EuiButton, EuiFieldText, EuiForm, EuiFormRow, EuiPanel, EuiSelect } from '@elastic/eui'; 8 | import { FILTER_DRAW_MODE } from '../../../../common'; 9 | 10 | const getSpatialRelationshipItems = ( 11 | relations: string[] 12 | ): Array<{ value: string; text: string }> => { 13 | return relations.map((relation) => { 14 | return { 15 | value: relation, 16 | text: relation, 17 | }; 18 | }); 19 | }; 20 | 21 | export interface FilterInputProps { 22 | drawLabel: string; 23 | defaultFilterLabel: string; 24 | mode: FILTER_DRAW_MODE; 25 | readonly relations: string[]; 26 | onSubmit: ({ 27 | relation, 28 | label, 29 | mode, 30 | }: { 31 | relation: string; 32 | label: string; 33 | mode: FILTER_DRAW_MODE; 34 | }) => void; 35 | } 36 | 37 | export const FilterInputPanel = ({ 38 | drawLabel, 39 | defaultFilterLabel, 40 | mode, 41 | relations, 42 | onSubmit, 43 | }: FilterInputProps) => { 44 | const [filterLabel, setFilterLabel] = useState(defaultFilterLabel); 45 | const [spatialRelation, setSpatialRelation] = useState(relations[0]); 46 | 47 | const updateSpatialFilterProperties = () => { 48 | onSubmit({ 49 | relation: spatialRelation, 50 | label: filterLabel, 51 | mode, 52 | }); 53 | }; 54 | 55 | return ( 56 | 57 | 58 | 59 | { 63 | setFilterLabel(event.target.value); 64 | }} 65 | compressed 66 | /> 67 | 68 | 69 | 70 | { 73 | setSpatialRelation(event.target.value); 74 | }} 75 | value={spatialRelation} 76 | compressed 77 | /> 78 | 79 | 80 | 87 | {drawLabel} 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/filter_toolbar.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { shallow } from 'enzyme'; 7 | import React from 'react'; 8 | import { SpatialFilterToolbar } from './filter_toolbar'; 9 | import { FILTER_DRAW_MODE } from '../../../../common'; 10 | import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; 11 | 12 | // Mock the useOpenSearchDashboards hook 13 | jest.mock('../../../../../../src/plugins/opensearch_dashboards_react/public'); 14 | 15 | describe('SpatialFilterToolbar tests', () => { 16 | beforeEach(() => { 17 | // Provide a default mock return value 18 | (useOpenSearchDashboards as jest.Mock).mockReturnValue({ 19 | services: { 20 | uiSettings: { 21 | get: jest.fn().mockReturnValue(false), // default to light mode 22 | }, 23 | }, 24 | }); 25 | }); 26 | 27 | it('renders spatial filter before drawing', () => { 28 | const mockCallback = jest.fn(); 29 | const toolbarComponent = shallow( 30 | 31 | ); 32 | expect(toolbarComponent).toMatchSnapshot(); 33 | }); 34 | 35 | it('renders spatial filter while drawing', () => { 36 | const mockCallback = jest.fn(); 37 | const toolbarComponent = shallow( 38 | 39 | ); 40 | expect(toolbarComponent).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/filter_toolbar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; 8 | import { 9 | DRAW_FILTER_POLYGON, 10 | DRAW_FILTER_POLYGON_DEFAULT_LABEL, 11 | DRAW_FILTER_RECTANGLE, 12 | DRAW_FILTER_RECTANGLE_DEFAULT_LABEL, 13 | DrawFilterProperties, 14 | FILTER_DRAW_MODE, 15 | } from '../../../../common'; 16 | import { FilterByShape } from './filter_by_shape'; 17 | import polygonLight from '../../../images/polygon-light.svg'; 18 | import polygonDark from '../../../images/polygon-dark.svg'; 19 | import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; 20 | import { MapServices } from '../../../types'; 21 | 22 | interface SpatialFilterToolBarProps { 23 | setFilterProperties: (properties: DrawFilterProperties) => void; 24 | mode: FILTER_DRAW_MODE; 25 | } 26 | 27 | export const SpatialFilterToolbar = ({ setFilterProperties, mode }: SpatialFilterToolBarProps) => { 28 | const { services } = useOpenSearchDashboards(); 29 | const isDarkMode = services.uiSettings.get('theme:darkMode'); 30 | const polygonIcon = isDarkMode ? polygonDark : polygonLight; 31 | return ( 32 | 33 | 34 | 43 | 44 | 45 | 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /public/components/toolbar/spatial_filter/spatial_filter.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .spatialFilterToolbar__shape{ 7 | position: relative; 8 | 9 | .euiButtonIcon { 10 | width:30px; 11 | height:30px; 12 | border: transparent; 13 | } 14 | } 15 | .spatialFilterGroup__popoverPanel{ 16 | max-width: 300px; 17 | border: hidden; 18 | } 19 | 20 | .spatialFilterToolbar__shapeButton { 21 | background-image: lightOrDarkTheme( 22 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E"), 23 | url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='rgb(223, 229, 239)'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")) 24 | !important; 25 | }; 26 | -------------------------------------------------------------------------------- /public/components/tooltip/create_tooltip.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .mapTooltip { 7 | div.maplibregl-popup-content.mapboxgl-popup-content { 8 | padding: 0; 9 | background: none; 10 | } 11 | } 12 | 13 | .maplibregl-popup-anchor-top { 14 | .maplibregl-popup-tip { 15 | border-bottom-color:lightOrDarkTheme($ouiColorEmptyShade, $ouiColorLightestShade) !important; 16 | } 17 | } 18 | 19 | .maplibregl-popup-anchor-bottom { 20 | .maplibregl-popup-tip { 21 | border-top-color:lightOrDarkTheme($ouiColorEmptyShade, $ouiColorLightestShade) !important; 22 | } 23 | } 24 | 25 | .maplibregl-popup-anchor-left { 26 | .maplibregl-popup-tip { 27 | border-right-color:lightOrDarkTheme($ouiColorEmptyShade, $ouiColorLightestShade) !important; 28 | } 29 | } 30 | 31 | .maplibregl-popup-anchor-right { 32 | .maplibregl-popup-tip { 33 | border-left-color:lightOrDarkTheme($ouiColorEmptyShade, $ouiColorLightestShade) !important; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/components/tooltip/create_tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Popup, MapGeoJSONFeature, LngLat } from 'maplibre-gl'; 4 | 5 | import { MapLayerSpecification, DocumentLayerSpecification } from '../../model/mapLayerType'; 6 | import { FeatureGroupItem, TooltipContainer } from './tooltipContainer'; 7 | import { MAX_LONGITUDE } from '../../../common'; 8 | import './create_tooltip.scss'; 9 | 10 | interface Options { 11 | features: MapGeoJSONFeature[]; 12 | layers: DocumentLayerSpecification[]; 13 | showCloseButton?: boolean; 14 | showPagination?: boolean; 15 | showLayerSelection?: boolean; 16 | } 17 | 18 | export function isTooltipEnabledLayer( 19 | layer: MapLayerSpecification 20 | ): layer is DocumentLayerSpecification { 21 | return ( 22 | layer.type !== 'opensearch_vector_tile_map' && 23 | layer.type !== 'custom_map' && 24 | layer.source.showTooltips === true && 25 | !!layer.source.tooltipFields?.length 26 | ); 27 | } 28 | 29 | export function isTooltipEnabledOnHover( 30 | layer: MapLayerSpecification 31 | ): layer is DocumentLayerSpecification { 32 | return isTooltipEnabledLayer(layer) && (layer.source?.displayTooltipsOnHover ?? true); 33 | } 34 | 35 | export function groupFeaturesByLayers( 36 | features: MapGeoJSONFeature[], 37 | layers: DocumentLayerSpecification[] 38 | ) { 39 | const featureGroups: FeatureGroupItem[] = []; 40 | if (layers.length > 0) { 41 | layers.forEach((layer) => { 42 | const layerFeatures = features.filter((f) => f.layer.source === layer.id); 43 | if (layerFeatures.length > 0) { 44 | featureGroups.push({ features: layerFeatures, layer }); 45 | } 46 | }); 47 | } 48 | return featureGroups; 49 | } 50 | 51 | export function getPopupLocation(geometry: GeoJSON.Geometry, mousePoint: LngLat) { 52 | // geometry.coordinates is different for different geometry.type, here we use the geometry.coordinates 53 | // of a Point as the position of the popup. For other types, such as Polygon, MultiPolygon, etc, 54 | // use mouse position should be better 55 | if (geometry.type !== 'Point') { 56 | return mousePoint; 57 | } 58 | const coordinates = geometry.coordinates; 59 | // Copied from https://maplibre.org/maplibre-gl-js-docs/example/popup-on-click/ 60 | // Ensure that if the map is zoomed out such that multiple 61 | // copies of the feature are visible, the popup appears 62 | // over the copy being pointed to. 63 | while (Math.abs(mousePoint.lng - coordinates[0]) > MAX_LONGITUDE) { 64 | coordinates[0] += mousePoint.lng > coordinates[0] ? 360 : -360; 65 | } 66 | return [coordinates[0], coordinates[1]] as [number, number]; 67 | } 68 | 69 | export function createPopup({ 70 | features, 71 | layers, 72 | showCloseButton = true, 73 | showPagination = true, 74 | showLayerSelection = true, 75 | }: Options) { 76 | const popup = new Popup({ 77 | closeButton: false, 78 | closeOnClick: false, 79 | maxWidth: 'max-content', 80 | className: 'mapTooltip', 81 | }); 82 | 83 | const featureGroup = groupFeaturesByLayers(features, layers); 84 | 85 | // Don't show popup if no feature 86 | if (featureGroup.length === 0) { 87 | return null; 88 | } 89 | 90 | const div = document.createElement('div'); 91 | ReactDOM.render( 92 | , 99 | div 100 | ); 101 | 102 | return popup.setDOMContent(div); 103 | } 104 | -------------------------------------------------------------------------------- /public/components/tooltip/display_features.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { Map as Maplibre, MapEventType, Popup } from 'maplibre-gl'; 7 | import React, { memo, useEffect, Fragment } from 'react'; 8 | import { 9 | createPopup, 10 | getPopupLocation, 11 | isTooltipEnabledLayer, 12 | isTooltipEnabledOnHover, 13 | } from './create_tooltip'; 14 | import { MapLayerSpecification } from '../../model/mapLayerType'; 15 | 16 | interface Props { 17 | map: Maplibre; 18 | layers: MapLayerSpecification[]; 19 | } 20 | 21 | export const DisplayFeatures = memo(({ map, layers }: Props) => { 22 | useEffect(() => { 23 | let clickPopup: Popup | null = null; 24 | let hoverPopup: Popup | null = null; 25 | 26 | // We don't want to show layer information in the popup for the map tile layer 27 | const tooltipEnabledLayers = layers.filter(isTooltipEnabledLayer); 28 | 29 | function onClickMap(e: MapEventType['click']) { 30 | // remove previous popup 31 | clickPopup?.remove(); 32 | 33 | const features = map.queryRenderedFeatures(e.point); 34 | if (features && map) { 35 | // don't show tooltip from labels 36 | if (features.length === 1 && features[0].layer?.type === 'symbol') { 37 | return; 38 | } 39 | clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); 40 | clickPopup?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)).addTo(map); 41 | } 42 | } 43 | 44 | function onMouseMoveMap(e: MapEventType['mousemove']) { 45 | // remove previous popup 46 | hoverPopup?.remove(); 47 | 48 | const tooltipEnabledLayersOnHover = layers.filter(isTooltipEnabledOnHover); 49 | const features = map.queryRenderedFeatures(e.point); 50 | if (features && map) { 51 | // don't show tooltip from labels 52 | if (features.length === 1 && features[0].layer?.type === 'symbol') { 53 | return; 54 | } 55 | hoverPopup = createPopup({ 56 | features, 57 | layers: tooltipEnabledLayersOnHover, 58 | // enable close button to avoid occasional dangling tooltip that is not cleared during mouse leave action 59 | showCloseButton: true, 60 | showPagination: false, 61 | showLayerSelection: false, 62 | }); 63 | hoverPopup?.setLngLat(getPopupLocation(features[0].geometry, e.lngLat)).addTo(map); 64 | } 65 | } 66 | 67 | if (map) { 68 | map.on('click', onClickMap); 69 | // reset cursor to default when user is no longer hovering over a clickable feature 70 | map.on('mouseleave', () => { 71 | map.getCanvas().style.cursor = ''; 72 | hoverPopup?.remove(); 73 | }); 74 | map.on('mouseenter', () => { 75 | map.getCanvas().style.cursor = 'pointer'; 76 | }); 77 | // add tooltip when users mouse move over a point 78 | map.on('mousemove', onMouseMoveMap); 79 | } 80 | 81 | return () => { 82 | if (map) { 83 | map.off('click', onClickMap); 84 | map.off('mousemove', onMouseMoveMap); 85 | } 86 | }; 87 | }, [layers]); 88 | 89 | return ; 90 | }); 91 | -------------------------------------------------------------------------------- /public/components/tooltip/tooltipContainer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { useMemo, useState } from 'react'; 7 | 8 | import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiText, EuiHorizontalRule } from '@elastic/eui'; 9 | import { MapGeoJSONFeature } from 'maplibre-gl'; 10 | import { TooltipHeaderContent } from './tooltipHeaderContent'; 11 | import { ALL_LAYERS, PageData, TableData, TooltipTable } from './tooltipTable'; 12 | import { DocumentLayerSpecification } from '../../model/mapLayerType'; 13 | 14 | export type FeatureGroupItem = { 15 | features: MapGeoJSONFeature[]; 16 | layer: DocumentLayerSpecification; 17 | }; 18 | 19 | interface TooltipProps { 20 | featureGroup: FeatureGroupItem[]; 21 | onClose: () => void; 22 | showCloseButton?: boolean; 23 | showPagination?: boolean; 24 | showLayerSelection?: boolean; 25 | } 26 | 27 | function featureToTableRow(properties: Record) { 28 | const row: PageData = []; 29 | for (const [k, v] of Object.entries(properties)) { 30 | row.push({ 31 | key: k, 32 | value: `${v}`, 33 | }); 34 | } 35 | return row; 36 | } 37 | 38 | function toTable(featureGroupItem: FeatureGroupItem) { 39 | const table: TableData = []; 40 | for (const feature of featureGroupItem.features) { 41 | if (feature?.properties) { 42 | table.push(featureToTableRow(feature.properties)); 43 | } 44 | } 45 | return { table, layer: featureGroupItem.layer.name }; 46 | } 47 | 48 | function createTableData(featureGroups: FeatureGroupItem[]) { 49 | return featureGroups.map(toTable); 50 | } 51 | 52 | export function TooltipContainer({ 53 | featureGroup, 54 | onClose, 55 | showCloseButton = true, 56 | showPagination = true, 57 | showLayerSelection = true, 58 | }: TooltipProps) { 59 | const [selectedLayerIndexes, setSelectedLayerIndexes] = useState([0]); 60 | const tables = useMemo(() => createTableData(featureGroup), [featureGroup]); 61 | 62 | const title = useMemo(() => { 63 | if (selectedLayerIndexes.includes(ALL_LAYERS)) { 64 | return 'All layers'; 65 | } 66 | if (selectedLayerIndexes.length === 1) { 67 | return tables[selectedLayerIndexes[0]].layer; 68 | } 69 | return ''; 70 | }, [selectedLayerIndexes, tables]); 71 | 72 | return ( 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /public/components/tooltip/tooltipHeaderContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { EuiSmallButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; 7 | import { i18n } from '@osd/i18n'; 8 | import React from 'react'; 9 | 10 | interface Props { 11 | title: string; 12 | showCloseButton: boolean; 13 | onClose: Function; 14 | } 15 | 16 | const TooltipHeaderContent = (props: Props) => { 17 | return ( 18 | 19 | 20 | 21 |

22 | {props.title} 23 |

24 |
25 |
26 | {props.showCloseButton && ( 27 | 28 | { 30 | return props.onClose(); 31 | }} 32 | iconType="cross" 33 | aria-label={i18n.translate('maps.tooltip.closeLabel', { 34 | defaultMessage: 'Close tooltip', 35 | })} 36 | data-test-subj="featureTooltipCloseButton" 37 | /> 38 | 39 | )} 40 |
41 | ); 42 | }; 43 | 44 | export { TooltipHeaderContent }; 45 | -------------------------------------------------------------------------------- /public/components/vector_upload_options.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .formattedSpan { 7 | display: block; 8 | } 9 | 10 | .importFileButton { 11 | text-align: right; 12 | margin: 0 auto; 13 | } 14 | -------------------------------------------------------------------------------- /public/embeddable/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export * from './map_embeddable'; 7 | export * from './map_embeddable_factory'; 8 | -------------------------------------------------------------------------------- /public/embeddable/map_component.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React, { useEffect, useState } from 'react'; 7 | import { 8 | withEmbeddableSubscription, 9 | EmbeddableOutput, 10 | } from '../../../../src/plugins/embeddable/public'; 11 | import { MapEmbeddable, MapInput } from './map_embeddable'; 12 | import { MapComponent } from '../components/map_page/'; 13 | import { OpenSearchDashboardsContextProvider } from '../../../../src/plugins/opensearch_dashboards_react/public'; 14 | import { MapServices } from '../types'; 15 | import { TimeRange } from '../../../../src/plugins/data/common'; 16 | import { DashboardProps } from '../components/map_page/map_page'; 17 | 18 | interface Props { 19 | embeddable: MapEmbeddable; 20 | input: MapInput; 21 | output: EmbeddableOutput; 22 | } 23 | export function MapEmbeddableComponentInner({ embeddable, input }: Props) { 24 | const [timeRange, setTimeRange] = useState(input.timeRange); 25 | const [refreshConfig, setRefreshConfig] = useState(input.refreshConfig); 26 | const [filters, setFilters] = useState(input.filters); 27 | const [query, setQuery] = useState(input.query); 28 | const services: MapServices = { 29 | ...embeddable.getServiceSettings(), 30 | }; 31 | 32 | useEffect(() => { 33 | setTimeRange(input.timeRange); 34 | setRefreshConfig(input.refreshConfig); 35 | setFilters(input.filters); 36 | setQuery(input.query); 37 | }, [input.refreshConfig, input.timeRange, input.filters, input.query]); 38 | 39 | const dashboardProps: DashboardProps = { 40 | timeRange, 41 | refreshConfig, 42 | filters, 43 | query, 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | export const MapEmbeddableComponent = withEmbeddableSubscription< 54 | MapInput, 55 | EmbeddableOutput, 56 | MapEmbeddable 57 | >(MapEmbeddableComponentInner); 58 | -------------------------------------------------------------------------------- /public/embeddable/map_embeddable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { Subscription } from 'rxjs'; 9 | import { MAP_SAVED_OBJECT_TYPE, MAPS_APP_ID } from '../../common'; 10 | import { 11 | Embeddable, 12 | EmbeddableInput, 13 | EmbeddableOutput, 14 | IContainer, 15 | } from '../../../../src/plugins/embeddable/public'; 16 | import { MapEmbeddableComponent } from './map_component'; 17 | import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; 18 | import { IndexPattern, RefreshInterval } from '../../../../src/plugins/data/public'; 19 | 20 | export const MAP_EMBEDDABLE = MAP_SAVED_OBJECT_TYPE; 21 | 22 | export interface MapInput extends EmbeddableInput { 23 | savedObjectId: string; 24 | refreshConfig?: RefreshInterval; 25 | } 26 | 27 | export interface MapOutput extends EmbeddableOutput { 28 | editable: boolean; 29 | editUrl: string; 30 | defaultTitle: string; 31 | editApp: string; 32 | editPath: string; 33 | indexPatterns: IndexPattern[]; 34 | } 35 | 36 | function getOutput( 37 | input: MapInput, 38 | editUrl: string, 39 | tittle: string, 40 | indexPatterns: IndexPattern[] 41 | ): MapOutput { 42 | return { 43 | editable: true, 44 | editUrl, 45 | defaultTitle: tittle, 46 | editApp: MAPS_APP_ID, 47 | editPath: input.savedObjectId, 48 | indexPatterns, 49 | }; 50 | } 51 | 52 | export class MapEmbeddable extends Embeddable { 53 | public readonly type = MAP_EMBEDDABLE; 54 | private subscription: Subscription; 55 | private node?: HTMLElement; 56 | private readonly services: any; 57 | constructor( 58 | initialInput: MapInput, 59 | { 60 | parent, 61 | services, 62 | editUrl, 63 | savedMapAttributes, 64 | indexPatterns, 65 | }: { 66 | parent?: IContainer; 67 | services: any; 68 | editUrl: string; 69 | savedMapAttributes: MapSavedObjectAttributes; 70 | indexPatterns: IndexPattern[]; 71 | } 72 | ) { 73 | super( 74 | initialInput, 75 | getOutput(initialInput, editUrl, savedMapAttributes.title, indexPatterns), 76 | parent 77 | ); 78 | this.services = services; 79 | this.subscription = this.getInput$().subscribe(() => { 80 | this.updateOutput(getOutput(this.input, editUrl, savedMapAttributes.title, indexPatterns)); 81 | }); 82 | } 83 | 84 | public render(node: HTMLElement) { 85 | this.node = node; 86 | if (this.node) { 87 | ReactDOM.unmountComponentAtNode(this.node); 88 | } 89 | ReactDOM.render(, node); 90 | } 91 | 92 | public reload() { 93 | if (this.node) { 94 | this.render(this.node); 95 | } 96 | } 97 | 98 | public destroy() { 99 | super.destroy(); 100 | this.subscription.unsubscribe(); 101 | if (this.node) { 102 | ReactDOM.unmountComponentAtNode(this.node); 103 | } 104 | } 105 | public getServiceSettings() { 106 | return this.services; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /public/images/polygon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/polygon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | // Override EuiCollapsibleNavGroup background color 7 | .layerConfigPanel { 8 | .euiCollapsibleNavGroup { 9 | background: transparent; 10 | 11 | // Remove any background color on hover if it exists 12 | &:hover { 13 | background: transparent; 14 | } 15 | 16 | // Ensure the content area is also transparent 17 | .euiCollapsibleNavGroup__content { 18 | background: transparent; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import './index.scss'; 7 | import { PluginInitializerContext } from '../../../src/core/server'; 8 | 9 | import { CustomImportMapPlugin } from './plugin'; 10 | import { ConfigSchema } from '../common/config'; 11 | 12 | // This exports static code and TypeScript types, 13 | // as well as, OpenSearch Dashboards Platform `plugin()` initializer. 14 | export function plugin(initializerContext: PluginInitializerContext) { 15 | // @ts-ignore 16 | return new CustomImportMapPlugin(initializerContext); 17 | } 18 | export { CustomImportMapPluginSetup, CustomImportMapPluginStart } from './types'; 19 | -------------------------------------------------------------------------------- /public/model/agg/build_agg.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { ClusterLayerSpecification } from '../mapLayerType'; 6 | import { getZoomPrecision } from '../../utils/precision'; 7 | import { MAP_DEFAULT_MAX_ZOOM, MAP_DEFAULT_MIN_ZOOM } from '../../../common'; 8 | 9 | function parseJson(jsonStr: string) { 10 | let json = {}; 11 | try { 12 | json = JSON.parse(jsonStr); 13 | } catch (e) {} 14 | return json; 15 | } 16 | 17 | export const buildAgg = (source: ClusterLayerSpecification['source'], zoom: number) => { 18 | //regular body: 19 | // 2: { 20 | // aggs: { 21 | // 1:{}, 22 | // 3: {}, 23 | // }, 24 | // }, 25 | let agg: Record<'2', Record | any>> = { 26 | 2: {}, 27 | }; 28 | 29 | function generateAggObjectIfEmpty() { 30 | if (!agg[2].aggs) { 31 | agg[2].aggs = {}; 32 | } 33 | } 34 | 35 | const metricAgg = source.metric, 36 | clusterAgg = source.cluster; 37 | if (metricAgg.agg !== 'count') { 38 | generateAggObjectIfEmpty(); 39 | let obj = { 40 | [metricAgg.agg]: { field: metricAgg.field }, 41 | }; 42 | if (metricAgg.json) { 43 | const json = parseJson(metricAgg.json); 44 | obj = { ...obj, ...json }; 45 | } 46 | 47 | agg[2].aggs[1] = obj; 48 | } 49 | 50 | if (clusterAgg.useCentroid) { 51 | generateAggObjectIfEmpty(); 52 | agg[2].aggs[3] = { 53 | geo_centroid: { field: clusterAgg.field }, 54 | }; 55 | } 56 | 57 | let clusterAggObj: Record = { 58 | field: clusterAgg.field, 59 | }; 60 | if (clusterAgg.json) { 61 | const json = parseJson(clusterAgg.json); 62 | clusterAggObj = { ...clusterAggObj, ...json }; 63 | } 64 | if (!clusterAgg.changePrecision) { 65 | clusterAggObj.precision = Number(source.cluster.precision); 66 | } else { 67 | //zoom in maplibre is float, parse it to int 68 | let integerZoom = Math.trunc(zoom); 69 | 70 | //limit zoom in range 71 | integerZoom < MAP_DEFAULT_MIN_ZOOM && (integerZoom = MAP_DEFAULT_MIN_ZOOM); 72 | integerZoom > MAP_DEFAULT_MAX_ZOOM && (integerZoom = MAP_DEFAULT_MAX_ZOOM); 73 | 74 | const zoomPrecision = getZoomPrecision(integerZoom, source.cluster.agg); 75 | 76 | clusterAggObj.precision = zoomPrecision; 77 | } 78 | 79 | agg[2][clusterAgg.agg] = clusterAggObj; 80 | 81 | return agg; 82 | }; 83 | -------------------------------------------------------------------------------- /public/model/customLayerFunctions.ts: -------------------------------------------------------------------------------- 1 | import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; 2 | import { hasLayer, removeLayers } from './map/layer_operations'; 3 | import { MaplibreRef } from './layersFunctions'; 4 | 5 | const refreshLayer = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { 6 | const maplibreInstance = maplibreRef.current; 7 | if (maplibreInstance) { 8 | maplibreInstance.removeLayer(layerConfig.id); 9 | maplibreInstance.removeSource(layerConfig.id); 10 | addNewLayer(layerConfig, maplibreRef); 11 | } 12 | }; 13 | 14 | const addNewLayer = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { 15 | const maplibreInstance = maplibreRef.current; 16 | if (maplibreInstance) { 17 | const tilesURL = getCustomMapURL(layerConfig); 18 | const layerSource = layerConfig?.source; 19 | maplibreInstance.addSource(layerConfig.id, { 20 | type: 'raster', 21 | tiles: [tilesURL], 22 | tileSize: 256, 23 | attribution: layerSource?.attribution, 24 | }); 25 | // Convert zoomRange to number[] to avoid type error for backport versions 26 | const zoomRange: number[] = applyZoomRangeToLayer(layerConfig); 27 | maplibreInstance.addLayer({ 28 | id: layerConfig.id, 29 | type: 'raster', 30 | source: layerConfig.id, 31 | paint: { 32 | 'raster-opacity': layerConfig.opacity / 100, 33 | }, 34 | layout: { 35 | visibility: layerConfig.visibility === 'visible' ? 'visible' : 'none', 36 | }, 37 | minzoom: zoomRange[0], 38 | maxzoom: zoomRange[1], 39 | }); 40 | } 41 | }; 42 | 43 | const getCustomMapURL = (layerConfig: CustomLayerSpecification) => { 44 | const layerSource = layerConfig?.source; 45 | if (layerSource?.customType === 'tms') { 46 | return layerSource?.url; 47 | } else if (layerSource?.customType === 'wms') { 48 | const referenceSystemName = layerSource.version === '1.3.0' ? 'crs' : 'srs'; 49 | if (!layerSource.crs) { 50 | layerSource.crs = "EPSG:3857" 51 | } 52 | return `${layerSource?.url}?service=WMS&version=${layerSource.version}&request=GetMap&format=${layerSource.format}&transparent=true&layers=${layerSource?.layers}&styles=${layerSource.styles}&${referenceSystemName}=${layerSource.crs}&width=256&height=256&bbox={bbox-epsg-3857}`; 53 | } else { 54 | return ''; 55 | } 56 | }; 57 | 58 | export const applyZoomRangeToLayer = (layerConfig: CustomLayerSpecification) => { 59 | // Convert zoomRange to number[] to avoid type error for backport versions 60 | return layerConfig.zoomRange.map((zoom) => Number(zoom)); 61 | }; 62 | 63 | export const CustomLayerFunctions = { 64 | render: (maplibreRef: MaplibreRef, layerConfig: CustomLayerSpecification) => { 65 | return hasLayer(maplibreRef.current!, layerConfig.id) 66 | ? refreshLayer(layerConfig, maplibreRef) 67 | : addNewLayer(layerConfig, maplibreRef); 68 | }, 69 | remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { 70 | removeLayers(maplibreRef.current!, layerConfig.id, true); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /public/model/geo/filter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { GeoShapeRelation, LatLon } from '@opensearch-project/opensearch/api/types'; 7 | import { 8 | FilterMeta, 9 | FILTERS, 10 | FilterState, 11 | FilterStateStore, 12 | GeoBoundingBoxFilter, 13 | GeoShapeFilter, 14 | GeoShapeFilterMeta, 15 | ShapeFilter, 16 | } from '../../../../../src/plugins/data/common'; 17 | import { GeoBounds } from '../map/boundary'; 18 | 19 | export const buildBoundingBox = (mapBounds: GeoBounds) => { 20 | const bottomRight: LatLon = { 21 | lon: mapBounds.bottomRight.lng, 22 | lat: mapBounds.bottomRight.lat, 23 | }; 24 | 25 | const topLeft: LatLon = { 26 | lon: mapBounds.topLeft.lng, 27 | lat: mapBounds.topLeft.lat, 28 | }; 29 | 30 | return { 31 | bottom_right: bottomRight, 32 | top_left: topLeft, 33 | }; 34 | }; 35 | 36 | export const buildBBoxFilter = ( 37 | fieldName: string, 38 | mapBounds: GeoBounds, 39 | filterMeta: FilterMeta 40 | ): GeoBoundingBoxFilter => { 41 | const boundingBox = buildBoundingBox(mapBounds); 42 | return { 43 | meta: { 44 | ...filterMeta, 45 | params: boundingBox, 46 | }, 47 | geo_bounding_box: { 48 | [fieldName]: boundingBox, 49 | }, 50 | }; 51 | }; 52 | 53 | export const buildGeoShapeFilterMeta = ( 54 | filterLabel: string | null, 55 | filterShape: ShapeFilter, 56 | relation: GeoShapeRelation 57 | ): GeoShapeFilterMeta => { 58 | return { 59 | type: FILTERS.GEO_SHAPE, 60 | alias: filterLabel, 61 | disabled: false, 62 | params: { 63 | relation, 64 | shape: filterShape, 65 | }, 66 | negate: false, 67 | }; 68 | }; 69 | 70 | export const buildGeoShapeFilter = ( 71 | fieldName: string, 72 | filterMeta: GeoShapeFilterMeta 73 | ): GeoShapeFilter => { 74 | const $state: FilterState = { 75 | store: FilterStateStore.APP_STATE, 76 | }; 77 | return { 78 | meta: { 79 | ...filterMeta, 80 | key: fieldName, 81 | }, 82 | geo_shape: { 83 | ignore_unmapped: true, 84 | [fieldName]: filterMeta.params, 85 | }, 86 | $state, 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /public/model/layersFunctions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { Map as Maplibre } from 'maplibre-gl'; 7 | import { 8 | DASHBOARDS_MAPS_LAYER_ICON, 9 | DASHBOARDS_MAPS_LAYER_NAME, 10 | DASHBOARDS_MAPS_LAYER_TYPE, 11 | } from '../../common'; 12 | import { OSMLayerFunctions } from './OSMLayerFunctions'; 13 | import { DocumentLayerFunctions } from './documentLayerFunctions'; 14 | import { ClusterLayerFunctions } from './clusterLayerFunctions'; 15 | import { 16 | BaseLayerSpecification, 17 | DataLayerSpecification, 18 | MapLayerSpecification, 19 | } from './mapLayerType'; 20 | import { CustomLayerFunctions } from './customLayerFunctions'; 21 | import { getLayers } from './map/layer_operations'; 22 | 23 | export interface MaplibreRef { 24 | current: Maplibre | null; 25 | } 26 | 27 | export const layersFunctionMap: { [key: string]: any } = { 28 | [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: OSMLayerFunctions, 29 | [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DocumentLayerFunctions, 30 | [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: CustomLayerFunctions, 31 | [DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER]: ClusterLayerFunctions, 32 | }; 33 | 34 | export const layersTypeNameMap: { [key: string]: string } = { 35 | [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_NAME.OPENSEARCH_MAP, 36 | [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_NAME.DOCUMENTS, 37 | [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, 38 | [DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER]: DASHBOARDS_MAPS_LAYER_NAME.CLUSTER, 39 | }; 40 | 41 | export const layersTypeIconMap: { [key: string]: string } = { 42 | [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, 43 | [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, 44 | [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_ICON.CUSTOM_MAP, 45 | [DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER]: DASHBOARDS_MAPS_LAYER_ICON.CLUSTER, 46 | }; 47 | 48 | export const baseLayerTypeLookup = { 49 | [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: true, 50 | [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: true, 51 | [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: false, 52 | [DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER]: false, 53 | }; 54 | 55 | export const getDataLayers = (layers: MapLayerSpecification[]): DataLayerSpecification[] => { 56 | return layers.filter((layer) => !baseLayerTypeLookup[layer.type]) as DataLayerSpecification[]; 57 | }; 58 | 59 | export const getBaseLayers = (layers: MapLayerSpecification[]): BaseLayerSpecification[] => { 60 | return layers.filter((layer) => baseLayerTypeLookup[layer.type]) as BaseLayerSpecification[]; 61 | }; 62 | 63 | export const getMaplibreAboveLayerId = ( 64 | mapLayerId: string, 65 | maplibre: Maplibre 66 | ): string | undefined => { 67 | const currentLoadedMbLayers = getLayers(maplibre); 68 | const matchingMbLayers = currentLoadedMbLayers.filter((mbLayer) => 69 | mbLayer.id.includes(mapLayerId) 70 | ); 71 | if (matchingMbLayers.length > 0) { 72 | const highestMbLayerIndex = currentLoadedMbLayers.indexOf( 73 | matchingMbLayers[matchingMbLayers.length - 1] 74 | ); 75 | const aboveMbLayer = currentLoadedMbLayers[highestMbLayerIndex + 1]; 76 | return aboveMbLayer?.id; 77 | } 78 | return undefined; 79 | }; 80 | -------------------------------------------------------------------------------- /public/model/map/__mocks__/layer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export class MockLayer { 7 | private layerProperties: Map = new Map(); 8 | 9 | constructor(id: string) { 10 | this.layerProperties.set('id', id); 11 | } 12 | 13 | public setProperty(name: string, value: any): this { 14 | if (Array.isArray(value) && value.length !== 0 && value[0] === 'get') { 15 | this.layerProperties.set(name, value[1]); 16 | return this; 17 | } 18 | this.layerProperties.set(name, value); 19 | return this; 20 | } 21 | 22 | public getProperty(name: string): any { 23 | return this.layerProperties.get(name); 24 | } 25 | 26 | public hasProperty(name: string): boolean { 27 | return this.layerProperties.has(name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/model/map/bondary.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { LngLat, LngLatBounds, Map as Maplibre } from 'maplibre-gl'; 7 | import { MockMaplibreMap } from './__mocks__/map'; 8 | import { GeoBounds, getBounds } from './boundary'; 9 | 10 | describe('verify get bounds', function () { 11 | it('should cover complete map if more than on copy is visible', function () { 12 | const ne: LngLat = new LngLat(333.811, 82.8); 13 | const sw: LngLat = new LngLat(-248.8, -79.75); 14 | const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); 15 | const expectedBounds: GeoBounds = { 16 | bottomRight: new LngLat(180, -79.75), 17 | topLeft: new LngLat(-180, 82.8), 18 | }; 19 | expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); 20 | }); 21 | 22 | it('should wrap if map crosses international date line', function () { 23 | const ne: LngLat = new LngLat(11.56, 80.85); 24 | const sw: LngLat = new LngLat(-220.77, 21.52); 25 | const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); 26 | const expectedBounds: GeoBounds = { 27 | bottomRight: new LngLat(11.559999999999945, 21.52), 28 | topLeft: new LngLat(139.23000000000002, 80.85), 29 | }; 30 | expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); 31 | }); 32 | it('should give same value as map bounds for other cases', function () { 33 | const sw: LngLat = new LngLat(-135.18, 27.67); 34 | const ne: LngLat = new LngLat(-2.34, 71.01); 35 | const mockMap = new MockMaplibreMap([], new LngLatBounds(sw, ne)); 36 | const expectedBounds: GeoBounds = { 37 | bottomRight: new LngLat(-2.340000000000032, 27.67), 38 | topLeft: new LngLat(-135.18, 71.01), 39 | }; 40 | expect(getBounds((mockMap as unknown) as Maplibre)).toEqual(expectedBounds); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /public/model/map/boundary.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { LngLat, LngLatBounds, Map as Maplibre } from 'maplibre-gl'; 7 | import { MAX_LONGITUDE, MIN_LONGITUDE } from '../../../common'; 8 | 9 | export interface GeoBounds { 10 | bottomRight: LngLat; 11 | topLeft: LngLat; 12 | } 13 | 14 | // calculate lng limits based on map bounds 15 | // maps can render more than 1 copies of map at lower zoom level and displays 16 | // one side from 1 copy and other side from other copy at higher zoom level if 17 | // screen crosses internation dateline 18 | export function getBounds(map: Maplibre): GeoBounds { 19 | const mapBounds: LngLatBounds = map.getBounds(); 20 | const boundsMinLng: number = mapBounds.getNorthWest().lng; 21 | const boundsMaxLng: number = mapBounds.getSouthEast().lng; 22 | 23 | let right: number; 24 | let left: number; 25 | // if bounds expands more than 360 then, consider complete globe is visible 26 | if (boundsMaxLng - boundsMinLng >= MAX_LONGITUDE - MIN_LONGITUDE) { 27 | right = MAX_LONGITUDE; 28 | left = MIN_LONGITUDE; 29 | } else { 30 | // wrap bounds if only portion of globe is visible 31 | // wrap() returns a new LngLat object whose longitude is 32 | // wrapped to the range (-180, 180). 33 | right = mapBounds.getSouthEast().wrap().lng; 34 | left = mapBounds.getNorthWest().wrap().lng; 35 | } 36 | return { 37 | bottomRight: new LngLat(right, mapBounds.getSouthEast().lat), 38 | topLeft: new LngLat(left, mapBounds.getNorthWest().lat), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /public/model/mapState.ts: -------------------------------------------------------------------------------- 1 | import { GeoShapeFilterMeta, Query, TimeRange } from '../../../../src/plugins/data/common'; 2 | 3 | export interface MapState { 4 | timeRange: TimeRange; 5 | query: Query; 6 | refreshInterval: { 7 | pause: boolean; 8 | value: number; 9 | }; 10 | spatialMetaFilters?: GeoShapeFilterMeta[]; 11 | } 12 | -------------------------------------------------------------------------------- /public/services.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { CoreStart } from '../../../src/core/public'; 7 | import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; 8 | import { TimefilterContract, DataPublicPluginStart } from '../../../src/plugins/data/public'; 9 | 10 | export const postGeojson = async ( 11 | requestBody: any, 12 | http: CoreStart['http'], 13 | dataSourceRefId: string 14 | ) => { 15 | try { 16 | const query = dataSourceRefId ? { dataSourceId: dataSourceRefId } : undefined; 17 | 18 | return await http.post('../api/custom_import_map/_upload', { 19 | body: requestBody, 20 | ...(query && { query }), 21 | }); 22 | } catch (e) { 23 | return e; 24 | } 25 | }; 26 | 27 | export const getIndex = async ( 28 | indexName: string, 29 | http: CoreStart['http'], 30 | dataSourceRefId: string 31 | ) => { 32 | try { 33 | const query = dataSourceRefId ? { dataSourceId: dataSourceRefId } : undefined; 34 | return await http.post('../api/custom_import_map/_indices', { 35 | body: JSON.stringify({ index: indexName }), 36 | ...(query && { query }), 37 | }); 38 | } catch (e) { 39 | return e; 40 | } 41 | }; 42 | 43 | export const [getTimeFilter, setTimeFilter] = createGetterSetter('TimeFilter'); 44 | 45 | export const [getFormatService, setFormatService] = 46 | createGetterSetter('data.fieldFormats'); 47 | -------------------------------------------------------------------------------- /public/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { 7 | AppMountParameters, 8 | CoreStart, 9 | SavedObjectsClient, 10 | ToastsStart, 11 | ScopedHistory, 12 | } from '../../../src/core/public'; 13 | import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; 14 | import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; 15 | import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; 16 | import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; 17 | import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; 18 | import { ConfigSchema } from '../common/config'; 19 | 20 | export interface AppPluginStartDependencies { 21 | navigation: NavigationPublicPluginStart; 22 | savedObjects: SavedObjectsClient; 23 | data: DataPublicPluginStart; 24 | embeddable: EmbeddableStart; 25 | } 26 | 27 | export interface MapServices extends CoreStart { 28 | setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; 29 | appBasePath: AppMountParameters['history']; 30 | element: AppMountParameters['element']; 31 | navigation: NavigationPublicPluginStart; 32 | toastNotifications: ToastsStart; 33 | history: AppMountParameters['history']; 34 | data: DataPublicPluginStart; 35 | application: CoreStart['application']; 36 | i18n: CoreStart['i18n']; 37 | savedObjects: CoreStart['savedObjects']; 38 | overlays: CoreStart['overlays']; 39 | embeddable: EmbeddableStart; 40 | scopedHistory: ScopedHistory; 41 | chrome: CoreStart['chrome']; 42 | uiSettings: CoreStart['uiSettings']; 43 | mapConfig: ConfigSchema; 44 | } 45 | 46 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 47 | export interface CustomImportMapPluginSetup {} 48 | 49 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 50 | export interface CustomImportMapPluginStart {} 51 | 52 | export interface AppPluginSetupDependencies { 53 | regionMap: RegionMapPluginSetup; 54 | embeddable: EmbeddableSetup; 55 | visualizations: VisualizationsSetup; 56 | data: DataPublicPluginSetup; 57 | } 58 | -------------------------------------------------------------------------------- /public/utils/breadcrumbs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { i18n } from '@osd/i18n'; 7 | import { MAPS_APP_ID } from '../../common'; 8 | 9 | export function getMapsLandingBreadcrumbs(navigateToApp: any) { 10 | return [ 11 | { 12 | text: i18n.translate('maps.listing.breadcrumb', { 13 | defaultMessage: 'Maps', 14 | }), 15 | onClick: () => navigateToApp(MAPS_APP_ID), 16 | }, 17 | ]; 18 | } 19 | 20 | export function getCreateBreadcrumbs(navigateToApp: any) { 21 | return [ 22 | ...getMapsLandingBreadcrumbs(navigateToApp), 23 | { 24 | text: i18n.translate('maps.create.breadcrumb', { 25 | defaultMessage: 'Create', 26 | }), 27 | }, 28 | ]; 29 | } 30 | 31 | export function getSavedMapBreadcrumbs(text: string, navigateToApp: any) { 32 | return [ 33 | ...getMapsLandingBreadcrumbs(navigateToApp), 34 | { 35 | text, 36 | }, 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /public/utils/fields_options.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { getFieldsOptions } from './fields_options'; 7 | import { IndexPattern } from '../../../../src/plugins/data/common'; 8 | 9 | describe('getFieldsOptions', () => { 10 | const mockIndexPattern = { 11 | fields: [ 12 | { 13 | name: 'field1', 14 | displayName: 'Field 1', 15 | type: 'text', 16 | }, 17 | { 18 | name: 'field2', 19 | displayName: 'Field 2', 20 | type: 'number', 21 | }, 22 | { 23 | name: 'field3', 24 | displayName: 'Field 3', 25 | type: 'geo_point', 26 | }, 27 | { 28 | name: 'field4', 29 | displayName: 'Field 4', 30 | type: 'geo_shape', 31 | }, 32 | ], 33 | } as IndexPattern; 34 | 35 | it('should return all fields options if no acceptedFieldTypes are specified', () => { 36 | const expectedFieldsOptions = [ 37 | { label: 'text', options: [{ label: 'Field 1' }] }, 38 | { 39 | label: 'number', 40 | options: [{ label: 'Field 2' }], 41 | }, 42 | { 43 | label: 'geo_point', 44 | options: [{ label: 'Field 3' }], 45 | }, 46 | { 47 | label: 'geo_shape', 48 | options: [{ label: 'Field 4' }], 49 | }, 50 | ]; 51 | const fieldsOptions = getFieldsOptions(mockIndexPattern); 52 | expect(fieldsOptions).toEqual(expectedFieldsOptions); 53 | }); 54 | 55 | it('should return only options for acceptedFieldTypes', () => { 56 | const acceptedFieldTypes = ['geo_point', 'geo_shape']; 57 | const expectedFieldsOptions = [ 58 | { label: 'geo_point', options: [{ label: 'Field 3' }] }, 59 | { 60 | label: 'geo_shape', 61 | options: [{ label: 'Field 4' }], 62 | }, 63 | ]; 64 | const fieldsOptions = getFieldsOptions(mockIndexPattern, acceptedFieldTypes); 65 | expect(fieldsOptions).toEqual(expectedFieldsOptions); 66 | }); 67 | 68 | it('should return an empty array if indexPattern is null', () => { 69 | const fieldsOptions = getFieldsOptions(null); 70 | expect(fieldsOptions).toEqual([]); 71 | }); 72 | 73 | it('should return an empty array if indexPattern fields are null', () => { 74 | const mockIndexPatternWithoutFields = { 75 | fields: null, 76 | } as unknown as IndexPattern; 77 | const fieldsOptions = getFieldsOptions(mockIndexPatternWithoutFields); 78 | expect(fieldsOptions).toEqual([]); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /public/utils/fields_options.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import _, { Dictionary } from 'lodash'; 7 | import { IndexPatternField } from '../../../../src/plugins/data/common'; 8 | import { IndexPattern } from '../../../../src/plugins/data/public'; 9 | export const getFieldsOptions = ( 10 | indexPattern: IndexPattern | null | undefined, 11 | acceptedFieldTypes: string[] = [] 12 | ) => { 13 | const fieldList = indexPattern?.fields; 14 | if (!fieldList) return []; 15 | const fieldTypeMap: Dictionary = _.groupBy(fieldList, (field) => field.type); 16 | 17 | const fieldOptions: Array<{ label: string; options: Array<{ label: string }> }> = []; 18 | 19 | Object.entries(fieldTypeMap).forEach(([fieldType, fieldEntries]) => { 20 | const fieldsOfSameType: Array<{ label: string }> = []; 21 | for (const field of fieldEntries) { 22 | if (acceptedFieldTypes.length === 0 || acceptedFieldTypes.includes(field.type)) { 23 | fieldsOfSameType.push({ label: `${field.displayName || field.name}` }); 24 | } 25 | } 26 | if (fieldsOfSameType.length > 0) { 27 | fieldOptions.push({ 28 | label: `${fieldType}`, 29 | options: fieldsOfSameType, 30 | }); 31 | } 32 | }); 33 | return fieldOptions; 34 | }; 35 | 36 | export const formatFieldsStringToComboBox = ( 37 | fields: string[] | null | undefined 38 | ): Array<{ label: string }> => { 39 | if (!fields) return []; 40 | 41 | return fields.map((field) => { 42 | return { 43 | label: field, 44 | }; 45 | }); 46 | }; 47 | 48 | export const formatFieldStringToComboBox = ( 49 | field: string | undefined 50 | ): Array<{ label: string }> => { 51 | if (!field) return []; 52 | 53 | return formatFieldsStringToComboBox([field]); 54 | }; 55 | -------------------------------------------------------------------------------- /public/utils/geo_formater.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | const geoJSONTypes: string[] = [ 7 | 'point', 8 | 'linestring', 9 | 'polygon', 10 | 'multipoint', 11 | 'multilinestring', 12 | 'multipolygon', 13 | 'geometrycollection', 14 | ]; 15 | 16 | export function isGeoJSON(value: { type: any; coordinates: any }) { 17 | if (!value) return false; 18 | if (!value.type || !value.coordinates) { 19 | return false; 20 | } 21 | const geoJSONType = value.type; 22 | if (geoJSONTypes.includes(geoJSONType.toLowerCase())) { 23 | return true; 24 | } 25 | return false; 26 | } 27 | 28 | function buildGeoJSONOfTypePoint(lon: number, lat: number) { 29 | return { 30 | type: 'Point', 31 | coordinates: [lon, lat], 32 | }; 33 | } 34 | 35 | export function convertGeoPointToGeoJSON(location: any) { 36 | // An object with 'lat' and 'lon' properties 37 | if (location?.lat && location?.lon) { 38 | return buildGeoJSONOfTypePoint(location?.lon, location?.lat); 39 | } 40 | // Geopoint as an array && support either (lon/lat) or (lon/lat/z) 41 | if (Array.isArray(location) && (location.length === 2 || location.length === 3)) { 42 | return buildGeoJSONOfTypePoint(location[0], location[1]); 43 | } 44 | 45 | if (typeof location !== 'string') { 46 | return undefined; 47 | } 48 | // Geopoint as a string && support either (lat,lon) or (lat, lon, z) 49 | const values = location.trim().split(','); 50 | if (values && (values.length === 2 || values.length === 3)) { 51 | return buildGeoJSONOfTypePoint(parseFloat(values[1].trim()), parseFloat(values[0].trim())); 52 | } 53 | // TODO Geopoint as geohash 54 | return undefined; 55 | } 56 | -------------------------------------------------------------------------------- /public/utils/precision.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { geohashColumns } from './decode'; 7 | import { MAP_DEFAULT_MAX_ZOOM, MAP_DEFAULT_MIN_ZOOM } from '../../common'; 8 | import { ClusterLayerSpecification } from '../model/mapLayerType'; 9 | import { ClusterAggregations } from '../components/layer_config/cluster_config/config'; 10 | 11 | export const getZoomPrecision = ( 12 | zoom: number, 13 | aggType: ClusterLayerSpecification['source']['cluster']['agg'] 14 | ) => { 15 | if (aggType === 'geohash_grid') { 16 | return getGeoHashZoomPrecision(zoom); 17 | } else { 18 | const percentage = 19 | (zoom - MAP_DEFAULT_MIN_ZOOM) / (MAP_DEFAULT_MAX_ZOOM - MAP_DEFAULT_MIN_ZOOM); 20 | //Different agg type has different precisionRange. 21 | const precisionRange = ClusterAggregations.find( 22 | (item) => item.value === aggType 23 | )!.precisionRange; 24 | 25 | const precisionLength = precisionRange[1] - precisionRange[0]; 26 | const newPrecision = precisionLength * percentage + precisionRange[0]; 27 | return Math.round(newPrecision); 28 | } 29 | }; 30 | 31 | export function getGeoHashZoomPrecision(zoom: number) { 32 | /** 33 | * Map Leaflet zoom levels to geohash precision levels. 34 | * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. 35 | */ 36 | 37 | const zoomPrecision: Record = {}; 38 | const minGeohashPixels = 16; 39 | const maxGeoHashPrecision = 12; 40 | 41 | for (let zoom = MAP_DEFAULT_MIN_ZOOM; zoom < MAP_DEFAULT_MAX_ZOOM; zoom += 1) { 42 | const worldPixels = 256 * Math.pow(2, zoom); 43 | zoomPrecision[zoom] = 1; 44 | for (let precision = 2; precision <= maxGeoHashPrecision; precision += 1) { 45 | const columns = geohashColumns(precision); 46 | if (worldPixels / columns >= minGeohashPixels) { 47 | zoomPrecision[zoom] = precision; 48 | } else { 49 | break; 50 | } 51 | } 52 | } 53 | return zoomPrecision[zoom]; 54 | } 55 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.10.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.10.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.10.0 3 | 4 | ### Features 5 | * Allow filtering geo_shape fields around map extent ([#452](https://github.com/opensearch-project/dashboards-maps/pull/452)) 6 | * Support dark mode in maps-dashboards([#455](https://github.com/opensearch-project/dashboards-maps/pull/455)) 7 | 8 | ### Maintenance 9 | * Bump cypress version to ^13.1.0 ([#462](https://github.com/opensearch-project/dashboards-maps/pull/462)) 10 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.11.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.11.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.11.0 3 | 4 | ### Maintenance 5 | * Increment version to 2.11.0.0 ([#476](https://github.com/opensearch-project/dashboards-maps/pull/476)) 6 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.12.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.12.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.12.0 3 | 4 | ### Bug Fixes 5 | * Fixed maps tooltip display at dark mode[#564](https://github.com/opensearch-project/dashboards-maps/pull/564) 6 | ### Maintenance 7 | * Fix broken build and failing tests [#572](https://github.com/opensearch-project/dashboards-maps/pull/572) 8 | * Update dependencies to align with other plugins [#575](https://github.com/opensearch-project/dashboards-maps/pull/575) 9 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.13.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.13.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.13.0 3 | 4 | ### Documentation 5 | * Update data layer source name [#588](https://github.com/opensearch-project/dashboards-maps/pull/588) 6 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.14.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.14.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.14.0 3 | 4 | ### Features 5 | * Support multi data source display in Maps app([#611](https://github.com/opensearch-project/dashboards-maps/pull/611)) 6 | * Support multi data source in Region map ([#614](https://github.com/opensearch-project/dashboards-maps/pull/614)) 7 | ### Bug Fixes 8 | * Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.15.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.15.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.15.0 3 | 4 | ### Bug Fixes 5 | * Add data source reference id in data layer search request ([#623](https://github.com/opensearch-project/dashboards-maps/pull/623)) 6 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.16.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.16.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.16.0 3 | 4 | ### Features 5 | * Add support new navigation for maps ([#635](https://github.com/opensearch-project/dashboards-maps/pull/635)) 6 | * 7 | ### Bug Fixes 8 | * Fixed broken wms custom layer update ([#601](https://github.com/opensearch-project/dashboards-maps/pull/631)) -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.17.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.17.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.17.0 3 | 4 | ### Features 5 | * Conditionally use the new Page Header variant on the Maps listing page [#653](https://github.com/opensearch-project/dashboards-maps/pull/653) 6 | * Conditionally use the new Application Header variant on the Maps visualization page [#654](https://github.com/opensearch-project/dashboards-maps/pull/654) 7 | * Conditionally use full width for Maps listing page table [#655](https://github.com/opensearch-project/dashboards-maps/pull/655) 8 | ### Infrastructure 9 | * Use functional test repo to run maps integration test workflow [#664](https://github.com/opensearch-project/dashboards-maps/pull/664) 10 | ### Maintenance 11 | * Deprecated maps multi data source display [#651](https://github.com/opensearch-project/dashboards-maps/pull/651) 12 | ### Refactoring 13 | * Consistency and Desntiy changes [#659] (https://github.com/opensearch-project/dashboards-maps/pull/659) -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.18.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.18.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.18.0 3 | 4 | ### Features 5 | * [navigation]feat: update category to flatten menus in analytics(all) use case [#674](https://github.com/opensearch-project/dashboards-maps/pull/674) 6 | ### Enhancements 7 | ### Bug Fixes 8 | * fix: prevent overlay from overlapping new application header([#680] (https://github.com/opensearch-project/dashboards-maps/pull/680)) 9 | * Fix dynamic uses of i18n ([#678](https://github.com/opensearch-project/dashboards-maps/pull/678)) 10 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.19.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.19.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.19.0 3 | 4 | ### Bug Fixes 5 | * Locked custom wms crs input to epsg-3857 ([#632](https://github.com/opensearch-project/dashboards-maps/pull/632)) 6 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.2.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.2.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.2.0 3 | 4 | ### Features 5 | * Remove plugin check logic and support offline working of importing custom geoJSON([#10](https://github.com/opensearch-project/dashboards-maps/pull/10)) 6 | * Add error modal and lint fixes([#13](https://github.com/opensearch-project/dashboards-maps/pull/13)) 7 | * Introduce refresh option to update the custom vector map list ([#14](https://github.com/opensearch-project/dashboards-maps/pull/14)) 8 | 9 | ### Infrastructure 10 | * Add basic template files related to OpenSearch guidelines([#2](https://github.com/opensearch-project/dashboards-maps/pull/2)) 11 | * Add GitHub workflow for running unit tests([#6](https://github.com/opensearch-project/dashboards-maps/pull/6)) 12 | * Add badges to README([#9](https://github.com/opensearch-project/dashboards-maps/pull/9)) 13 | * Add developer guide and easy setup([#12](https://github.com/opensearch-project/dashboards-maps/pull/12)) 14 | * Add changes for releasing plugin with OSD as part of 2.2 release([#20](https://github.com/opensearch-project/dashboards-maps/pull/20)) 15 | 16 | ### Maintenance 17 | * Add version changes required for 2.2.0 release([#18](https://github.com/opensearch-project/dashboards-maps/pull/18)) 18 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.3.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.3.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.3.0 3 | 4 | ### Enhancements 5 | * Adds integration tests in the repo for customImportMap plugin ([#30](https://github.com/opensearch-project/dashboards-maps/pull/30)) 6 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.4.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.4.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.4.0 3 | 4 | ### Infrastructure 5 | * Add windows and mac platform to run unit test ([#74](https://github.com/opensearch-project/dashboards-maps/pull/74)) 6 | 7 | ### Maintenance 8 | * Bump version to 2.4.0.0 ([#70](https://github.com/opensearch-project/dashboards-maps/pull/70)) -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.5.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.5.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.5.0 3 | 4 | ### Features 5 | * Add tooltip for hover ([#132](https://github.com/opensearch-project/dashboards-maps/pull/132)) 6 | * Introduce tooltip fields to document layer specification ([#124](https://github.com/opensearch-project/dashboards-maps/pull/124)) 7 | * Add support for WKT format ([#165](https://github.com/opensearch-project/dashboards-maps/pull/165)) 8 | * Add global time filter bar to maps ([#131](https://github.com/opensearch-project/dashboards-maps/pull/131)) 9 | * Support custom layer to Maps ([#150](https://github.com/opensearch-project/dashboards-maps/pull/150)) 10 | * Support geo_shape visualization in documents layer ([#111](https://github.com/opensearch-project/dashboards-maps/pull/111)) 11 | * Introduce saved object plugin into maps plugin ([#67](https://github.com/opensearch-project/dashboards-maps/pull/67)) 12 | * Add new layer functions for opensearch map layer ([#66](https://github.com/opensearch-project/dashboards-maps/pull/66)) 13 | * Add map page and add basic layers panel ([#40](https://github.com/opensearch-project/dashboards-maps/pull/40)) 14 | * Add layer panel component ([#51](https://github.com/opensearch-project/dashboards-maps/pull/51)) 15 | * Add base map layer functions ([#62](https://github.com/opensearch-project/dashboards-maps/pull/62)) 16 | * Query with geo bounding box ([#148](https://github.com/opensearch-project/dashboards-maps/pull/148)) 17 | * Display search filters in map layer config panel ([#130](https://github.com/opensearch-project/dashboards-maps/pull/130)) 18 | 19 | ### Enhancements 20 | * Support other geopoint formats ([#144](https://github.com/opensearch-project/dashboards-maps/pull/144)) 21 | * Add reorder handle inside layer panel ([#116](https://github.com/opensearch-project/dashboards-maps/pull/116)) 22 | * Update show/hide icon ([#114](https://github.com/opensearch-project/dashboards-maps/pull/114)) 23 | * Update basic layer settings ([#107](https://github.com/opensearch-project/dashboards-maps/pull/107)) 24 | * Delete layer modal ([#139](https://github.com/opensearch-project/dashboards-maps/pull/139)) 25 | * Add multi-layer support to map popup ([#140](https://github.com/opensearch-project/dashboards-maps/pull/140)) 26 | * Support overriding maps config from OSD config yml file ([#202](https://github.com/opensearch-project/dashboards-maps/pull/202)) 27 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.6.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.6.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.6.0 3 | 4 | ### Features 5 | * Add map as embeddable to dashboard ([#231](https://github.com/opensearch-project/dashboards-maps/pull/231)) 6 | * Add maps saved object for sample datasets ([#240](https://github.com/opensearch-project/dashboards-maps/pull/240)) 7 | 8 | ### Infrastructure 9 | * [Cypress fix] Wait map saved before open maps listing ([#218](https://github.com/opensearch-project/dashboards-maps/pull/218)) 10 | 11 | ### Refactoring 12 | * Refactor add layer operations ([#222](https://github.com/opensearch-project/dashboards-maps/pull/222)) 13 | * Refactor layer operations ([#224](https://github.com/opensearch-project/dashboards-maps/pull/224)) 14 | * Refactor layer properties as own interface ([#225](https://github.com/opensearch-project/dashboards-maps/pull/225)) 15 | 16 | ### Enhancements 17 | * Fix popup display while zoomed out ([#226](https://github.com/opensearch-project/dashboards-maps/pull/226)) 18 | * Limit max number of layers ([#216](https://github.com/opensearch-project/dashboards-maps/pull/216)) 19 | * Add close button to tooltip hover ([#263](https://github.com/opensearch-project/dashboards-maps/pull/263)) 20 | * Add scroll bar when more layers added ([#254](https://github.com/opensearch-project/dashboards-maps/pull/254)) 21 | * Align items in add new layer modal ([#256](https://github.com/opensearch-project/dashboards-maps/pull/256)) 22 | * Add indexPatterns to map embeddable output for dashboard filters ([#272](https://github.com/opensearch-project/dashboards-maps/pull/272)) 23 | 24 | ### Bug Fixes 25 | * Fix custom layer render opacity config ([#289](https://github.com/opensearch-project/dashboards-maps/pull/289)) 26 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.8.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.8.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.8.0 3 | 4 | ### Maintenance 5 | * Remove package-lock.json ([#400](https://github.com/opensearch-project/dashboards-maps/pull/400)) 6 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-2.9.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 2.9.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 2.9.0 3 | 4 | ### Maintenance 5 | Increment version to 2.9.0.0 ([#426](https://github.com/opensearch-project/dashboards-maps/pull/426)) 6 | Bump semver from 7.3.8 to 7.5.3 ([#431](https://github.com/opensearch-project/dashboards-maps/pull/431)) 7 | Bump tough cookie to ^4.1.3 ([#439](https://github.com/opensearch-project/dashboards-maps/pull/439)) 8 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-3.0.0.0-alpha1.md: -------------------------------------------------------------------------------- 1 | ## Version 3.0.0.0-alpha1 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 3.0.0.0-alpha1 3 | 4 | ### Features 5 | - Introduce cluster layer in maps-dashboards ([#703](https://github.com/opensearch-project/dashboards-maps/pull/703)) 6 | ### Bug Fixes 7 | * Fix layer config panel background color inconsistency ([#704](https://github.com/opensearch-project/dashboards-maps/pull/704)) 8 | ### Infrastructure 9 | * Increment version to 3.0.0-alpha1 ([#708](https://github.com/opensearch-project/dashboards-maps/pull/708)) 10 | -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-3.0.0.0-beta1.md: -------------------------------------------------------------------------------- 1 | ## Version 3.0.0.0-beta1 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 3.0.0.0-beta1 3 | 4 | ### Maintenance 5 | * Bump up version to 3.0.0-beta1 [#716](https://github.com/opensearch-project/dashboards-maps/pull/716) -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-3.0.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 3.0.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 3.0.0.0 3 | 4 | ### Features 5 | - Introduce cluster layer in maps-dashboards ([#703](https://github.com/opensearch-project/dashboards-maps/pull/703)) 6 | ### Bug Fixes 7 | * Fix layer config panel background color inconsistency ([#704](https://github.com/opensearch-project/dashboards-maps/pull/704)) 8 | * Fix overlapping data labels on map layer ([#718](https://github.com/opensearch-project/dashboards-maps/pull/718)) 9 | ### Infrastructure 10 | * Increment version to 3.0.0-alpha1 ([#708](https://github.com/opensearch-project/dashboards-maps/pull/708)) 11 | * Bump up version to 3.0.0-beta1 [#716](https://github.com/opensearch-project/dashboards-maps/pull/716) -------------------------------------------------------------------------------- /release-notes/opensearch-dashboards-maps.release-notes-3.1.0.0.md: -------------------------------------------------------------------------------- 1 | ## Version 3.1.0.0 Release Notes 2 | Compatible with OpenSearch and OpenSearch Dashboards Version 3.1.0.0 3 | 4 | ### Maintenance 5 | * Increment version to 3.1.0.0 [#735](https://github.com/opensearch-project/dashboards-maps/pull/735) -------------------------------------------------------------------------------- /server/clusters/geospatial_cluster.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import GeospatialPlugin from './geospatial_plugin'; 7 | import { CLUSTER, DEFAULT_HEADERS } from '../services/utils/constants'; 8 | 9 | export default function createGeospatialCluster(core, globalConfig) { 10 | const { customHeaders, ...rest } = globalConfig.opensearch; 11 | return core.opensearch.legacy.createClient(CLUSTER.GEOSPATIAL, { 12 | plugins: [GeospatialPlugin], 13 | // Currently we are overriding any headers with our own since we explicitly required User-Agent to be OpenSearch Dashboards 14 | // for integration with our backend plugin. 15 | customHeaders: { ...customHeaders, ...DEFAULT_HEADERS }, 16 | ...rest, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /server/clusters/geospatial_plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { UPLOAD_GEOJSON_API_PATH } from '../../common/constants/shared'; 7 | 8 | // eslint-disable-next-line import/no-default-export 9 | export default function GeospatialPlugin(Client, config, components) { 10 | const ca = components.clientAction.factory; 11 | Client.prototype.geospatial = components.clientAction.namespaceFactory(); 12 | const geospatial = Client.prototype.geospatial.prototype; 13 | 14 | geospatial.geospatialQuery = ca({ 15 | url: { 16 | fmt: `${UPLOAD_GEOJSON_API_PATH}`, 17 | }, 18 | method: 'POST', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /server/clusters/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import createGeospatialCluster from './geospatial_cluster'; 7 | 8 | export { createGeospatialCluster }; 9 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { PluginConfigDescriptor, PluginInitializerContext } from '../../../src/core/server'; 7 | import { CustomImportMapPlugin } from './plugin'; 8 | import { ConfigSchema, configSchema } from '../common/config'; 9 | // This exports static code and TypeScript types, 10 | // as well as, OpenSearch Dashboards Platform `plugin()` initializer. 11 | 12 | export function plugin(initializerContext: PluginInitializerContext) { 13 | return new CustomImportMapPlugin(initializerContext); 14 | } 15 | 16 | export { CustomImportMapPluginSetup, CustomImportMapPluginStart } from './types'; 17 | 18 | export const config: PluginConfigDescriptor = { 19 | exposeToBrowser: { 20 | opensearchVectorTileDataUrl: true, 21 | opensearchVectorTileStyleUrl: true, 22 | opensearchVectorTileGlyphsUrl: true, 23 | }, 24 | schema: configSchema, 25 | }; 26 | -------------------------------------------------------------------------------- /server/plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { first } from 'rxjs/operators'; 7 | import { 8 | PluginInitializerContext, 9 | CoreSetup, 10 | CoreStart, 11 | Plugin, 12 | Logger, 13 | } from '../../../src/core/server'; 14 | 15 | import { HomeServerPluginSetup } from '../../../src/plugins/home/server'; 16 | import { getFlightsSavedObjects } from './services/sample_data/flights_saved_objects'; 17 | 18 | import { 19 | AppPluginSetupDependencies, 20 | CustomImportMapPluginSetup, 21 | CustomImportMapPluginStart, 22 | } from './types'; 23 | import { createGeospatialCluster } from './clusters'; 24 | import { GeospatialService, OpensearchService } from './services'; 25 | import { geospatial, opensearch, statsRoute } from '../server/routes'; 26 | import { mapSavedObjectsType } from './saved_objects'; 27 | import { capabilitiesProvider } from './saved_objects/capabilities_provider'; 28 | import { ConfigSchema } from '../common/config'; 29 | import GeospatialPlugin from './clusters/geospatial_plugin'; 30 | 31 | export class CustomImportMapPlugin 32 | implements Plugin { 33 | private readonly logger: Logger; 34 | private readonly globalConfig$; 35 | private readonly config$; 36 | 37 | constructor(initializerContext: PluginInitializerContext) { 38 | this.logger = initializerContext.logger.get(); 39 | this.globalConfig$ = initializerContext.config.legacy.globalConfig$; 40 | this.config$ = initializerContext.config.create(); 41 | } 42 | 43 | // Adds dashboards-maps saved objects to existing sample datasets using home plugin 44 | private addMapsSavedObjects(home: HomeServerPluginSetup, config: ConfigSchema) { 45 | home.sampleData.addSavedObjectsToSampleDataset('flights', getFlightsSavedObjects(config)); 46 | } 47 | 48 | public async setup(core: CoreSetup, plugins: AppPluginSetupDependencies) { 49 | this.logger.debug('customImportMap: Setup'); 50 | // @ts-ignore 51 | const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); 52 | // @ts-ignore 53 | const config = (await this.config$.pipe(first()).toPromise()) as ConfigSchema; 54 | 55 | const geospatialClient = createGeospatialCluster(core, globalConfig); 56 | // Initialize services 57 | const geospatialService = new GeospatialService(geospatialClient); 58 | const opensearchService = new OpensearchService(geospatialClient); 59 | 60 | const router = core.http.createRouter(); 61 | const { home, dataSource } = plugins; 62 | 63 | if (dataSource) { 64 | dataSource.registerCustomApiSchema(GeospatialPlugin); 65 | } 66 | 67 | // Register server side APIs 68 | geospatial(geospatialService, router); 69 | opensearch(opensearchService, router); 70 | statsRoute(router, this.logger); 71 | 72 | // Register saved object types 73 | core.savedObjects.registerType(mapSavedObjectsType); 74 | 75 | // Register capabilities 76 | core.capabilities.registerProvider(capabilitiesProvider); 77 | 78 | if (home) this.addMapsSavedObjects(home, config); 79 | 80 | return {}; 81 | } 82 | 83 | public start(core: CoreStart) { 84 | this.logger.debug('customImportMap: Started'); 85 | return {}; 86 | } 87 | 88 | public stop() {} 89 | } 90 | -------------------------------------------------------------------------------- /server/routes/geospatial.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { schema } from '@osd/config-schema'; 7 | import { MAX_FILE_PAYLOAD_SIZE } from '../../common'; 8 | 9 | // eslint-disable-next-line import/no-default-export 10 | export default function (services, router) { 11 | router.post( 12 | { 13 | path: '/api/custom_import_map/_upload', 14 | validate: { 15 | body: schema.any(), 16 | query: schema.maybe(schema.object({}, { unknowns: 'allow' })), 17 | }, 18 | options: { 19 | body: { 20 | accepts: 'application/json', 21 | maxBytes: MAX_FILE_PAYLOAD_SIZE, // 25 MB payload limit for custom geoJSON feature 22 | }, 23 | }, 24 | }, 25 | services.uploadGeojson 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /server/routes/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import geospatial from './geospatial'; 7 | import opensearch from './opensearch'; 8 | import { statsRoute } from './stats_router'; 9 | 10 | export { geospatial, opensearch, statsRoute }; 11 | -------------------------------------------------------------------------------- /server/routes/opensearch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { schema } from '@osd/config-schema'; 7 | 8 | // eslint-disable-next-line import/no-default-export 9 | export default function (services, router) { 10 | router.post( 11 | { 12 | path: '/api/custom_import_map/_indices', 13 | validate: { 14 | body: schema.object({ 15 | index: schema.string(), 16 | }), 17 | query: schema.maybe(schema.object({}, { unknowns: 'allow' })), 18 | }, 19 | }, 20 | services.getIndex 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /server/routes/stats_router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; 7 | import { Logger } from '@osd/logging'; 8 | import { 9 | IOpenSearchDashboardsResponse, 10 | IRouter, 11 | SavedObjectsFindResponse, 12 | } from '../../../../src/core/server'; 13 | import { APP_API, APP_PATH, PER_PAGE_REQUEST_NUMBER } from '../../common'; 14 | import { getMapSavedObjects, getStats } from '../common/stats/stats_helper'; 15 | import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; 16 | 17 | export function statsRoute(router: IRouter, logger: Logger) { 18 | router.get( 19 | { 20 | path: `${APP_API}${APP_PATH.STATS}`, 21 | validate: {}, 22 | }, 23 | async ( 24 | context, 25 | request, 26 | response 27 | ): Promise> => { 28 | try { 29 | const savedObjectsClient = context.core.savedObjects.client; 30 | const mapsSavedObjects: SavedObjectsFindResponse = 31 | await getMapSavedObjects(savedObjectsClient, PER_PAGE_REQUEST_NUMBER); 32 | const stats = getStats(mapsSavedObjects); 33 | return response.ok({ 34 | body: stats, 35 | }); 36 | } catch (error) { 37 | logger.error(error); 38 | return response.custom({ 39 | statusCode: error.statusCode || 500, 40 | body: error.message, 41 | }); 42 | } 43 | } 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /server/saved_objects/capabilities_provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export const capabilitiesProvider = () => ({ 7 | map: { 8 | // TODO: investigate which capabilities we need to provide 9 | // createNew: true, 10 | // createShortUrl: true, 11 | // delete: true, 12 | show: true, 13 | // showWriteControls: true, 14 | // save: true, 15 | // saveQuery: true, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /server/saved_objects/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export { mapSavedObjectsType } from './map_saved_object'; 7 | -------------------------------------------------------------------------------- /server/saved_objects/map_saved_object.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { SavedObjectsType } from 'opensearch-dashboards/server'; 7 | 8 | export const mapSavedObjectsType: SavedObjectsType = { 9 | name: 'map', 10 | hidden: false, 11 | namespaceType: 'agnostic', 12 | management: { 13 | defaultSearchField: 'title', 14 | importableAndExportable: true, 15 | getTitle(obj) { 16 | return obj.attributes.title; 17 | }, 18 | getInAppUrl(obj) { 19 | return { 20 | path: `/app/maps-dashboards#/${encodeURIComponent(obj.id)}`, 21 | uiCapabilitiesPath: 'map.show', 22 | }; 23 | }, 24 | getEditUrl(obj) { 25 | return `/management/opensearch-dashboards/objects/map/${encodeURIComponent(obj.id)}`; 26 | }, 27 | }, 28 | mappings: { 29 | properties: { 30 | title: { type: 'text' }, 31 | description: { type: 'text' }, 32 | layerList: { type: 'text', index: false }, 33 | uiState: { type: 'text', index: false }, 34 | mapState: { type: 'text', index: false }, 35 | version: { type: 'integer' }, 36 | // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow 37 | // When we save a saved object, the saved object plugin will extract the search source into two parts 38 | // Some information will be put into kibanaSavedObjectMeta while others will be created as a reference object and pushed to the reference array 39 | kibanaSavedObjectMeta: { 40 | properties: { searchSourceJSON: { type: 'text', index: false } }, 41 | }, 42 | }, 43 | }, 44 | migrations: {}, 45 | }; 46 | -------------------------------------------------------------------------------- /server/services/geospatial_service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export default class GeospatialService { 7 | constructor(driver) { 8 | this.driver = driver; 9 | } 10 | 11 | uploadGeojson = async (context, req, res) => { 12 | const dataSourceRefId = req.query.dataSourceId; 13 | let uploadResponse; 14 | try { 15 | if (dataSourceRefId) { 16 | const remoteDataSourceClient = context.dataSource.opensearch.legacy.getClient( 17 | dataSourceRefId 18 | ).callAPI; 19 | uploadResponse = await remoteDataSourceClient('geospatial.geospatialQuery', { 20 | body: req.body, 21 | }); 22 | } else { 23 | const { callAsCurrentUser } = await this.driver.asScoped(req); 24 | uploadResponse = await callAsCurrentUser('geospatial.geospatialQuery', { 25 | body: req.body, 26 | }); 27 | } 28 | 29 | return res.ok({ 30 | body: { 31 | ok: true, 32 | resp: uploadResponse, 33 | }, 34 | }); 35 | } catch (err) { 36 | return res.ok({ 37 | body: { 38 | ok: false, 39 | resp: err.message, 40 | }, 41 | }); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /server/services/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import GeospatialService from './geospatial_service'; 7 | import OpensearchService from './opensearch_service'; 8 | 9 | export { GeospatialService, OpensearchService }; 10 | -------------------------------------------------------------------------------- /server/services/opensearch_service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export default class OpensearchService { 7 | constructor(driver) { 8 | this.driver = driver; 9 | } 10 | 11 | getIndex = async (context, req, res) => { 12 | const dataSourceRefId = req.query.dataSourceId; 13 | try { 14 | if (dataSourceRefId) { 15 | const remoteDataSourceClient = context.dataSource.opensearch.legacy.getClient( 16 | dataSourceRefId 17 | ).callAPI; 18 | const { index } = req.body; 19 | const indices = await remoteDataSourceClient('cat.indices', { 20 | index, 21 | format: 'json', 22 | h: 'health,index,status', 23 | }); 24 | } else { 25 | const { callAsCurrentUser } = this.driver.asScoped(req); 26 | const { index } = req.body; 27 | const indices = await callAsCurrentUser; 28 | } 29 | return res.ok({ 30 | body: { 31 | ok: true, 32 | resp: indices, 33 | }, 34 | }); 35 | } catch (err) { 36 | // Opensearch throws an index_not_found_exception which we'll treat as a success 37 | if (err.statusCode === 404) { 38 | return res.ok({ 39 | body: { 40 | ok: false, 41 | resp: [], 42 | }, 43 | }); 44 | } else { 45 | return res.ok({ 46 | body: { 47 | ok: false, 48 | resp: err.message, 49 | }, 50 | }); 51 | } 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /server/services/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export const DEFAULT_HEADERS = { 7 | 'Content-Type': 'application/json', 8 | Accept: 'application/json', 9 | 'User-Agent': 'OpenSearch-Dashboards', 10 | 'osd-xsrf': true, 11 | }; 12 | export const CLUSTER = { 13 | GEOSPATIAL: 'opensearch_geospatial', 14 | }; 15 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { HomeServerPluginSetup } from '../../../src/plugins/home/server'; 6 | import { DataSourcePluginSetup } from '../../../src/plugins/data_source/server'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 9 | export interface CustomImportMapPluginSetup {} 10 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 11 | export interface CustomImportMapPluginStart {} 12 | 13 | export interface AppPluginSetupDependencies { 14 | home?: HomeServerPluginSetup; 15 | dataSource: DataSourcePluginSetup; 16 | } 17 | -------------------------------------------------------------------------------- /test/enzyme.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { configure } from 'enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | 9 | configure({ adapter: new Adapter() }); 10 | -------------------------------------------------------------------------------- /test/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | process.env.TZ = 'UTC'; 6 | module.exports = { 7 | rootDir: '../', 8 | setupFiles: [ 9 | '/test/polyfills.js', 10 | '/test/setupTests.js', 11 | '/test/enzyme.js', 12 | ], 13 | setupFilesAfterEnv: ['/test/setup.jest.js'], 14 | modulePaths: ['node_modules', `../../node_modules`], 15 | coverageDirectory: './coverage', 16 | moduleNameMapper: { 17 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 18 | '/test/mocks/styleMock.js', 19 | '\\.(css|less|scss)$': '/test/mocks/styleMock.js', 20 | '^ui/(.*)': '/../../src/legacy/ui/public/$1/', 21 | }, 22 | snapshotSerializers: ['../../node_modules/enzyme-to-json/serializer'], 23 | coverageReporters: ['lcov', 'text', 'cobertura'], 24 | testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'], 25 | coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/test/'], 26 | clearMocks: true, 27 | testPathIgnorePatterns: ['/build/', '/node_modules/'], 28 | modulePathIgnorePatterns: [], 29 | testEnvironment: 'jest-environment-jsdom', 30 | }; 31 | -------------------------------------------------------------------------------- /test/mocks/styleMock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | export default {}; 7 | -------------------------------------------------------------------------------- /test/polyfills.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { MutationObserver } from './polyfills/mutationObserver'; 7 | 8 | Object.defineProperty(window, 'MutationObserver', { value: MutationObserver }); 9 | 10 | document.createRange = () => ({ 11 | setStart: () => {}, 12 | setEnd: () => {}, 13 | commonAncestorContainer: { 14 | nodeName: 'BODY', 15 | ownerDocument: document, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /test/setup.jest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | 8 | // https://github.com/elastic/eui/issues/2530 9 | jest.mock('@elastic/eui/lib/components/icon', () => ({ 10 | EuiIcon: () =>
EuiIconMock
, 11 | __esModule: true, 12 | IconPropType: require('@elastic/eui/lib/components/icon/icon').IconPropType, 13 | ICON_TYPES: require('@elastic/eui/lib/components/icon/icon').TYPES, 14 | ICON_SIZES: require('@elastic/eui/lib/components/icon/icon').SIZES, 15 | ICON_COLORS: require('@elastic/eui/lib/components/icon/icon').COLORS, 16 | })); 17 | 18 | jest.mock('@elastic/eui/lib/components/form/form_row/make_id', () => () => 'some_make_id'); 19 | 20 | jest.mock('@elastic/eui/lib/services/accessibility', () => ({ 21 | htmlIdGenerator: () => () => 'generated-id', 22 | cascadingMenuKeys: require('@elastic/eui/lib/services/accessibility/cascading_menu_keys'), 23 | comboBoxKeys: require('@elastic/eui/lib/services/accessibility/combo_box_keys'), 24 | accessibleClickKeys: require('@elastic/eui/lib/services/accessibility/accessible_click_keys'), 25 | })); 26 | 27 | // https://github.com/facebook/jest/issues/5785 28 | // https://github.com/facebook/jest/pull/5267#issuecomment-356605468 29 | beforeEach(() => { 30 | jest.spyOn(console, 'error'); 31 | console.error.mockImplementation(() => {}); 32 | }); 33 | 34 | afterEach(() => { 35 | console.error.mockRestore(); 36 | }); 37 | window.URL.createObjectURL = function () {}; 38 | -------------------------------------------------------------------------------- /test/setupTests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright OpenSearch Contributors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | require('babel-polyfill'); 7 | -------------------------------------------------------------------------------- /translations/ja-JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "formats": { 3 | "number": { 4 | "currency": { 5 | "style": "currency" 6 | }, 7 | "percent": { 8 | "style": "percent" 9 | } 10 | }, 11 | "date": { 12 | "short": { 13 | "month": "numeric", 14 | "day": "numeric", 15 | "year": "2-digit" 16 | }, 17 | "medium": { 18 | "month": "short", 19 | "day": "numeric", 20 | "year": "numeric" 21 | }, 22 | "long": { 23 | "month": "long", 24 | "day": "numeric", 25 | "year": "numeric" 26 | }, 27 | "full": { 28 | "weekday": "long", 29 | "month": "long", 30 | "day": "numeric", 31 | "year": "numeric" 32 | } 33 | }, 34 | "time": { 35 | "short": { 36 | "hour": "numeric", 37 | "minute": "numeric" 38 | }, 39 | "medium": { 40 | "hour": "numeric", 41 | "minute": "numeric", 42 | "second": "numeric" 43 | }, 44 | "long": { 45 | "hour": "numeric", 46 | "minute": "numeric", 47 | "second": "numeric", 48 | "timeZoneName": "short" 49 | }, 50 | "full": { 51 | "hour": "numeric", 52 | "minute": "numeric", 53 | "second": "numeric", 54 | "timeZoneName": "short" 55 | } 56 | }, 57 | "relative": { 58 | "years": { 59 | "units": "year" 60 | }, 61 | "months": { 62 | "units": "month" 63 | }, 64 | "days": { 65 | "units": "day" 66 | }, 67 | "hours": { 68 | "units": "hour" 69 | }, 70 | "minutes": { 71 | "units": "minute" 72 | }, 73 | "seconds": { 74 | "units": "second" 75 | } 76 | } 77 | }, 78 | "messages": { 79 | "mapsDashboards.buttonText": "Translate me to Japanese" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "baseUrl": ".", 5 | "paths": { 6 | // Allows for importing from `opensearch-dashboards` package for the exported types. 7 | "opensearch-dashboards": ["./opensearch_dashboards"], 8 | "ui/*": ["src/legacy/ui/public/*"], 9 | "test_utils/*": ["src/test_utils/public/*"] 10 | }, 11 | // Support .tsx files and transform JSX into calls to React.createElement 12 | "jsx": "react", 13 | // Enables all strict type checking options. 14 | "strict": true, 15 | // enables "core language features" 16 | "lib": [ 17 | // ESNext auto includes previous versions all the way back to es5 18 | "esnext", 19 | // includes support for browser APIs 20 | "dom" 21 | ], 22 | // Node 8 should support everything output by esnext, we override this 23 | // in webpack with loader-level compiler options 24 | "target": "esnext", 25 | // Use commonjs for node, overridden in webpack to keep import statements 26 | // to maintain support for things like `await import()` 27 | "module": "commonjs", 28 | // Allows default imports from modules with no default export. This does not affect code emit, just type checking. 29 | // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or 30 | // ESNext module format is used. 31 | "allowSyntheticDefaultImports": true, 32 | // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. 33 | "esModuleInterop": true, 34 | // Resolve modules in the same way as Node.js. Aka make `require` works the 35 | // same in TypeScript as it does in Node.js. 36 | "moduleResolution": "node", 37 | // Disallow inconsistently-cased references to the same file. 38 | "forceConsistentCasingInFileNames": true, 39 | // Disable the breaking keyof behaviour introduced in TS 2.9.2 until EUI is updated to support that too 40 | "keyofStringsOnly": true, 41 | // Forbid unused local variables as the rule was deprecated by ts-lint 42 | "noUnusedLocals": true, 43 | // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. 44 | "downlevelIteration": true, 45 | // import tslib helpers rather than inlining helpers for iteration or spreading, for instance 46 | "importHelpers": true, 47 | // adding global typings 48 | "types": ["node", "jest", "react"] 49 | }, 50 | "include": [ 51 | "common/**/*", 52 | "server/**/*", 53 | "public/**/*", 54 | "utils/**/*", 55 | "models/**/*", 56 | "test/**/*" 57 | ], 58 | "exclude": ["node_modules", "*/node_modules/"] 59 | } 60 | --------------------------------------------------------------------------------