From 0cf87c8b211dd3530057f914718691043397d69d Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Mon, 9 Jan 2023 16:30:16 -0800 Subject: [PATCH] [2.x]Manually backport to 2.x from main (#180) Signed-off-by: Junqiu Lei --- .github/workflows/cypress-workflow.yml | 57 +- ...mImportMap.yml => unit-tests-workflow.yml} | 37 +- DEVELOPER_GUIDE.md | 7 +- MAINTAINERS.md | 145 +- README.md | 1 - .../babel.config.js => babel.config.js | 0 .../common => common}/constants/shared.ts | 2 + common/index.ts | 121 ++ common/map_saved_object_attributes.ts | 25 + {custom_import_map/common => common}/util.ts | 0 custom_import_map/.babelrc | 19 - custom_import_map/.eslintrc | 4 - custom_import_map/.gitignore | 4 - custom_import_map/.lintstagedrc | 3 - ....opensearch_dashboards-plugin-helpers.json | 10 - custom_import_map/.prettierignore | 8 - custom_import_map/.prettierrc | 5 - custom_import_map/build.sh | 87 - custom_import_map/common/index.ts | 22 - custom_import_map/cypress/support/commands.js | 71 - custom_import_map/public/plugin.tsx | 49 - custom_import_map/public/types.ts | 21 - custom_import_map/tsconfig.json | 59 - .../cypress.json => cypress.json | 0 .../fixtures/sample_geojson.json | 0 cypress/integration/documentsLayer.spec.js | 56 + .../integration/geojson_file_upload.spec.js | 25 +- .../integration/import_vector_map_tab.spec.js | 20 +- .../integration/opensearchMapLayer.spec.js | 39 + .../cypress => cypress}/plugins/index.js | 13 +- cypress/support/commands.js | 74 + cypress/support/constants.js | 12 + .../cypress => cypress}/support/index.js | 8 +- cypress/utils/constants.js | 9 + ...hboards.json => opensearch_dashboards.json | 6 +- .../package.json => package.json | 24 +- .../constants.js => public/_variables.scss | 2 +- public/application.tsx | 22 + .../show_error_modal.test.tsx.snap | 0 .../vector_upload_options.test.tsx.snap | 26 +- .../add_layer_panel/add_layer_panel.scss | 12 + .../add_layer_panel/add_layer_panel.tsx | 154 ++ .../components/add_layer_panel/index.ts | 5 +- public/components/app.tsx | 32 + .../base_map_layer_config_panel.tsx | 32 + .../custom_map_config_panel.tsx | 47 + .../custom_map_config/custom_map_source.tsx | 297 ++++ .../document_layer_config_panel.tsx | 84 + .../document_layer_source.tsx | 357 ++++ .../documents_config/document_layer_style.tsx | 237 +++ public/components/layer_config/index.ts | 7 + .../layer_config/layer_basic_settings.tsx | 153 ++ .../layer_config/layer_config_panel.tsx | 184 +++ .../components/layer_control_panel/index.ts | 6 + .../layer_control_panel.scss | 36 + .../layer_control_panel.tsx | 524 ++++++ public/components/map_container/index.ts | 6 + .../map_container/map_container.scss | 32 + .../map_container/map_container.tsx | 189 +++ public/components/map_page/index.ts | 6 + public/components/map_page/map_page.tsx | 86 + .../map_top_nav/get_top_nav_config.tsx | 133 ++ public/components/map_top_nav/index.ts | 6 + .../components/map_top_nav/top_nav_menu.tsx | 139 ++ public/components/maps_list/index.ts | 6 + public/components/maps_list/maps_list.tsx | 151 ++ .../components/show_error_modal.test.tsx | 0 .../components/show_error_modal.tsx | 0 public/components/tooltip/create_tooltip.tsx | 86 + .../components/tooltip/tooltipContainer.tsx | 96 ++ .../tooltip/tooltipHeaderContent.tsx | 44 + public/components/tooltip/tooltipTable.tsx | 200 +++ .../components/vector_upload_options.scss | 0 .../components/vector_upload_options.test.tsx | 0 .../components/vector_upload_options.tsx | 0 public/index.scss | 4 + {custom_import_map/public => public}/index.ts | 2 + public/model/OSMLayerFunctions.ts | 123 ++ public/model/customLayerFunctions.ts | 122 ++ public/model/documentLayerFunctions.ts | 401 +++++ public/model/layerRenderController.ts | 153 ++ public/model/layersFunctions.ts | 107 ++ public/model/mapLayerType.ts | 93 ++ public/model/mapState.ts | 10 + public/plugin.tsx | 90 ++ .../public => public}/services.ts | 0 public/types.ts | 47 + public/utils/breadcrumbs.ts | 38 + public/utils/geo_formater.ts | 55 + public/utils/getIntialConfig.ts | 117 ++ ...h-dashboards-maps.release-notes-2.4.0.0.md | 1 - .../clusters/geospatial_cluster.js | 0 .../clusters/geospatial_plugin.ts | 0 .../server => server}/clusters/index.ts | 0 {custom_import_map/server => server}/index.ts | 0 .../server => server}/plugin.ts | 9 + .../server => server}/routes/geospatial.ts | 0 .../server => server}/routes/index.ts | 0 .../server => server}/routes/opensearch.ts | 0 server/saved_objects/capabilities_provider.ts | 17 + server/saved_objects/index.ts | 6 + server/saved_objects/map_saved_object.ts | 45 + .../services/geospatial_service.js | 0 .../index.js => server/services/index.ts | 0 .../services/opensearch_service.js | 0 .../services/utils/constants.ts | 0 {custom_import_map/server => server}/types.ts | 0 {custom_import_map/test => test}/enzyme.js | 0 .../test => test}/jest.config.js | 4 +- .../test => test}/mocks/styleMock.js | 0 {custom_import_map/test => test}/polyfills.js | 0 .../polyfills/mutationObserver.js | 0 .../test => test}/setup.jest.js | 0 .../test => test}/setupTests.js | 0 translations/ja-JP.json | 81 + tsconfig.json | 58 + yarn.lock | 1433 +++++++++++++++++ 117 files changed, 6842 insertions(+), 614 deletions(-) rename .github/workflows/{unit-tests-workflow-for-customImportMap.yml => unit-tests-workflow.yml} (68%) rename custom_import_map/babel.config.js => babel.config.js (100%) rename {custom_import_map/common => common}/constants/shared.ts (83%) create mode 100644 common/index.ts create mode 100644 common/map_saved_object_attributes.ts rename {custom_import_map/common => common}/util.ts (100%) delete mode 100644 custom_import_map/.babelrc delete mode 100644 custom_import_map/.eslintrc delete mode 100644 custom_import_map/.gitignore delete mode 100644 custom_import_map/.lintstagedrc delete mode 100644 custom_import_map/.opensearch_dashboards-plugin-helpers.json delete mode 100644 custom_import_map/.prettierignore delete mode 100644 custom_import_map/.prettierrc delete mode 100644 custom_import_map/build.sh delete mode 100644 custom_import_map/common/index.ts delete mode 100644 custom_import_map/cypress/support/commands.js delete mode 100644 custom_import_map/public/plugin.tsx delete mode 100644 custom_import_map/public/types.ts delete mode 100644 custom_import_map/tsconfig.json rename custom_import_map/cypress.json => cypress.json (100%) rename {custom_import_map/cypress => cypress}/fixtures/sample_geojson.json (100%) create mode 100644 cypress/integration/documentsLayer.spec.js rename {custom_import_map/cypress => cypress}/integration/geojson_file_upload.spec.js (72%) rename {custom_import_map/cypress => cypress}/integration/import_vector_map_tab.spec.js (74%) create mode 100644 cypress/integration/opensearchMapLayer.spec.js rename {custom_import_map/cypress => cypress}/plugins/index.js (67%) create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/constants.js rename {custom_import_map/cypress => cypress}/support/index.js (89%) create mode 100644 cypress/utils/constants.js rename custom_import_map/opensearch_dashboards.json => opensearch_dashboards.json (60%) rename custom_import_map/package.json => package.json (69%) rename custom_import_map/cypress/utils/constants.js => public/_variables.scss (63%) create mode 100644 public/application.tsx rename {custom_import_map/public => public}/components/__snapshots__/show_error_modal.test.tsx.snap (100%) rename {custom_import_map/public => public}/components/__snapshots__/vector_upload_options.test.tsx.snap (94%) create mode 100644 public/components/add_layer_panel/add_layer_panel.scss create mode 100644 public/components/add_layer_panel/add_layer_panel.tsx rename custom_import_map/cypress/support/constants.js => public/components/add_layer_panel/index.ts (53%) create mode 100644 public/components/app.tsx create mode 100644 public/components/layer_config/base_map_layer_config_panel.tsx create mode 100644 public/components/layer_config/custom_map_config/custom_map_config_panel.tsx create mode 100644 public/components/layer_config/custom_map_config/custom_map_source.tsx create mode 100644 public/components/layer_config/documents_config/document_layer_config_panel.tsx create mode 100644 public/components/layer_config/documents_config/document_layer_source.tsx create mode 100644 public/components/layer_config/documents_config/document_layer_style.tsx create mode 100644 public/components/layer_config/index.ts create mode 100644 public/components/layer_config/layer_basic_settings.tsx create mode 100644 public/components/layer_config/layer_config_panel.tsx create mode 100644 public/components/layer_control_panel/index.ts create mode 100644 public/components/layer_control_panel/layer_control_panel.scss create mode 100644 public/components/layer_control_panel/layer_control_panel.tsx create mode 100644 public/components/map_container/index.ts create mode 100644 public/components/map_container/map_container.scss create mode 100644 public/components/map_container/map_container.tsx create mode 100644 public/components/map_page/index.ts create mode 100644 public/components/map_page/map_page.tsx create mode 100644 public/components/map_top_nav/get_top_nav_config.tsx create mode 100644 public/components/map_top_nav/index.ts create mode 100644 public/components/map_top_nav/top_nav_menu.tsx create mode 100644 public/components/maps_list/index.ts create mode 100644 public/components/maps_list/maps_list.tsx rename {custom_import_map/public => public}/components/show_error_modal.test.tsx (100%) rename {custom_import_map/public => public}/components/show_error_modal.tsx (100%) create mode 100644 public/components/tooltip/create_tooltip.tsx create mode 100644 public/components/tooltip/tooltipContainer.tsx create mode 100644 public/components/tooltip/tooltipHeaderContent.tsx create mode 100644 public/components/tooltip/tooltipTable.tsx rename {custom_import_map/public => public}/components/vector_upload_options.scss (100%) rename {custom_import_map/public => public}/components/vector_upload_options.test.tsx (100%) rename {custom_import_map/public => public}/components/vector_upload_options.tsx (100%) create mode 100644 public/index.scss rename {custom_import_map/public => public}/index.ts (94%) create mode 100644 public/model/OSMLayerFunctions.ts create mode 100644 public/model/customLayerFunctions.ts create mode 100644 public/model/documentLayerFunctions.ts create mode 100644 public/model/layerRenderController.ts create mode 100644 public/model/layersFunctions.ts create mode 100644 public/model/mapLayerType.ts create mode 100644 public/model/mapState.ts create mode 100644 public/plugin.tsx rename {custom_import_map/public => public}/services.ts (100%) create mode 100644 public/types.ts create mode 100644 public/utils/breadcrumbs.ts create mode 100644 public/utils/geo_formater.ts create mode 100644 public/utils/getIntialConfig.ts rename {custom_import_map/server => server}/clusters/geospatial_cluster.js (100%) rename {custom_import_map/server => server}/clusters/geospatial_plugin.ts (100%) rename {custom_import_map/server => server}/clusters/index.ts (100%) rename {custom_import_map/server => server}/index.ts (100%) rename {custom_import_map/server => server}/plugin.ts (82%) rename {custom_import_map/server => server}/routes/geospatial.ts (100%) rename {custom_import_map/server => server}/routes/index.ts (100%) rename {custom_import_map/server => server}/routes/opensearch.ts (100%) create mode 100644 server/saved_objects/capabilities_provider.ts create mode 100644 server/saved_objects/index.ts create mode 100644 server/saved_objects/map_saved_object.ts rename {custom_import_map/server => server}/services/geospatial_service.js (100%) rename custom_import_map/server/services/index.js => server/services/index.ts (100%) rename {custom_import_map/server => server}/services/opensearch_service.js (100%) rename {custom_import_map/server => server}/services/utils/constants.ts (100%) rename {custom_import_map/server => server}/types.ts (100%) rename {custom_import_map/test => test}/enzyme.js (100%) rename {custom_import_map/test => test}/jest.config.js (92%) rename {custom_import_map/test => test}/mocks/styleMock.js (100%) rename {custom_import_map/test => test}/polyfills.js (100%) rename {custom_import_map/test => test}/polyfills/mutationObserver.js (100%) rename {custom_import_map/test => test}/setup.jest.js (100%) rename {custom_import_map/test => test}/setupTests.js (100%) create mode 100644 translations/ja-JP.json create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 23005716..ea0866b8 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -4,13 +4,14 @@ on: branches: - main - 2.* + - feature/** push: branches: - main - 2.* env: - OPENSEARCH_DASHBOARDS_VERSION: '2.4' - OPENSEARCH_VERSION: '2.4.0-SNAPSHOT' + OPENSEARCH_DASHBOARDS_BRANCH: '2.5' + GEOSPATIAL_PLUGIN_BRANCH: '2.5' jobs: tests: env: @@ -23,7 +24,7 @@ jobs: # This setting says that all jobs should finish, even if one fails fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] + os: [ ubuntu-latest, windows-latest ] include: - os: windows-latest working_directory: X:\ @@ -31,8 +32,7 @@ jobs: cypress_cache_folder: ~/AppData/Local/Cypress/Cache - os: ubuntu-latest cypress_cache_folder: ~/.cache/Cypress - - os: macos-latest - cypress_cache_folder: ~/Library/Caches/Cypress + name: Test and Build Dashboards Maps on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: @@ -48,35 +48,32 @@ jobs: run: subst 'X:' . - name: Checkout geospatial plugin - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: geospatial repository: opensearch-project/geospatial - ref: '2.4' + ref: ${{ env.GEOSPATIAL_PLUGIN_BRANCH }} - name: Run Opensearch with plugin working-directory: ${{ env.WORKING_DIR }} run: | - # Install coreutils for macOS since timeout doesn't seem to available on that OS even when forcing bash shell - if [ "$RUNNER_OS" == "macOS" ]; then - brew install coreutils - fi cd geospatial ./gradlew run & timeout 600 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9200)" != "200" ]]; do sleep 5; done' shell: bash env: _JAVA_OPTIONS: ${{ matrix.os_java_options }} + - name: Checkout Plugin - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: dashboards-maps - name: Checkout OpenSearch Dashboards - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: opensearch-project/OpenSearch-Dashboards - ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + ref: ${{ env.OPENSEARCH_DASHBOARDS_BRANCH }} path: OpenSearch-Dashboards - name: Get node and yarn versions @@ -84,7 +81,7 @@ jobs: run: | echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" - + - name: Setup node uses: actions/setup-node@v1 with: @@ -103,36 +100,36 @@ jobs: # Sets Windows to use bash for npm shell so the script commands work as intended npm config set script-shell "C:\\Program Files\\git\\bin\\bash.exe" - - name: Move custom_import_map to Plugins Dir + - name: Move plugin to OpenSearch-Dashboards Plugins Directory run: | - mv dashboards-maps/custom_import_map OpenSearch-Dashboards/plugins/custom_import_map - - - name: Bootstrap plugin/opensearch-dashboards + mv dashboards-maps OpenSearch-Dashboards/plugins/dashboards-maps + + - name: Bootstrap plugin run: | - cd OpenSearch-Dashboards/plugins/custom_import_map + cd OpenSearch-Dashboards/plugins/dashboards-maps yarn osd bootstrap - + - name: Run OpenSearch Dashboards server run: | cd OpenSearch-Dashboards yarn start --no-base-path --no-watch & shell: bash + # Window is slow so wait longer - name: Sleep until OSD server starts - windows if: ${{ matrix.os == 'windows-latest' }} - # Window is slow so wait longer run: Start-Sleep -s 400 shell: powershell - name: Sleep until OSD server starts - non-windows if: ${{ matrix.os != 'windows-latest' }} - run: sleep 300 + run: sleep 400 shell: bash - name: Install Cypress run: | - cd OpenSearch-Dashboards/plugins/custom_import_map - # This will install Cypress in case the binary is missing which can happen on Windows and Mac + cd OpenSearch-Dashboards/plugins/dashboards-maps + # This will install Cypress in case the binary is missing which can happen on Windows # If the binary exists, this will exit quickly so it should not be an expensive operation npx cypress install shell: bash @@ -140,7 +137,7 @@ jobs: - name: Get Cypress version id: cypress_version run: | - cd OpenSearch-Dashboards/plugins/custom_import_map + cd OpenSearch-Dashboards/plugins/dashboards-maps echo "::set-output name=cypress_version::$(cat ./package.json | jq '.dependencies.cypress' | tr -d '"')" - name: Cache Cypress @@ -157,9 +154,9 @@ jobs: npm config delete script-shell - name: Run Cypress tests - uses: cypress-io/github-action@v2 + uses: cypress-io/github-action@v5 with: - working-directory: OpenSearch-Dashboards/plugins/custom_import_map + working-directory: OpenSearch-Dashboards/plugins/dashboards-maps command: yarn run cypress run --browser chrome wait-on: 'http://localhost:5601' env: @@ -170,11 +167,11 @@ jobs: if: failure() with: name: cypress-screenshots-${{ matrix.os }} - path: OpenSearch-Dashboards/plugins/custom_import_map/cypress/screenshots + path: OpenSearch-Dashboards/plugins/dashboards-maps/cypress/screenshots # Test run video was always captured, so this action uses "always()" condition - uses: actions/upload-artifact@v1 if: always() with: name: cypress-videos-${{ matrix.os }} - path: OpenSearch-Dashboards/plugins/custom_import_map/cypress/videos + path: OpenSearch-Dashboards/plugins/dashboards-maps/cypress/videos diff --git a/.github/workflows/unit-tests-workflow-for-customImportMap.yml b/.github/workflows/unit-tests-workflow.yml similarity index 68% rename from .github/workflows/unit-tests-workflow-for-customImportMap.yml rename to .github/workflows/unit-tests-workflow.yml index 79bc8fdd..4928a66a 100644 --- a/.github/workflows/unit-tests-workflow-for-customImportMap.yml +++ b/.github/workflows/unit-tests-workflow.yml @@ -2,21 +2,16 @@ name: Unit tests workflow on: push: branches: - - "*" - - "feature/**" - paths: - - src/plugins/custom_import_map/** - - .github/workflows/unit-tests-workflow-for-customImportMap.yml + - main + - 2.* pull_request: branches: - - "*" - - "feature/**" - paths: - - src/plugins/custom_import_map/** - - .github/workflows/unit-tests-workflow-for-customImportMap.yml + - main + - 2.* + - feature/** env: - OPENSEARCH_DASHBOARDS_VERSION: '2.0' + OPENSEARCH_DASHBOARDS_BRANCH: '2.5' jobs: tests: strategy: @@ -27,7 +22,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Plugin - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + path: dashboards-maps # Enable longer filenames for windows - name: Enable longer filenames @@ -35,10 +32,10 @@ jobs: run: git config --system core.longpaths true - name: Checkout OpenSearch Dashboards - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: opensearch-project/OpenSearch-Dashboards - ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }} + ref: ${{ env.OPENSEARCH_DASHBOARDS_BRANCH }} path: OpenSearch-Dashboards - name: Get node and yarn versions @@ -46,7 +43,6 @@ jobs: run: | echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")" echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")" - - name: Setup node uses: actions/setup-node@v1 with: @@ -58,17 +54,16 @@ jobs: npm uninstall -g yarn echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} + - name: Move plugin to OpenSearch-Dashboard Plugins Directory + run: mv dashboards-maps OpenSearch-Dashboards/plugins/dashboards-maps - - name: Move custom_import_map to Plugins Dir - run: mv custom_import_map OpenSearch-Dashboards/plugins/custom_import_map - - - name: Bootstrap plugin/opensearch-dashboards + - name: Bootstrap plugin run: | - cd OpenSearch-Dashboards/plugins/custom_import_map + cd OpenSearch-Dashboards/plugins/dashboards-maps yarn osd bootstrap - name: Run tests with coverage run: | - cd OpenSearch-Dashboards/plugins/custom_import_map + cd OpenSearch-Dashboards/plugins/dashboards-maps yarn run test:jest --coverage - name: Uploads coverage uses: codecov/codecov-action@v1 diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index cf53ffff..4e1ef1ac 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -33,8 +33,7 @@ echo 'src/plugins/custom_import_map/*' >> .git/info/sparse-checkout git config core.sparseCheckout true git checkout main ``` -6. Retain only `custom_import_map` directory within plugins and remove `src/plugins` from the path. -6. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/custom_import_map`. +6. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/src/plugins/custom_import_map`. Ultimately, your directory structure should look like this: @@ -42,12 +41,12 @@ Ultimately, your directory structure should look like this: . ├── OpenSearch-Dashboards │ └── plugins -│ └── custom_import_map +│ └── src/plugins/custom_import_map ``` ### Run -From OpenSearch-Dashboards repo (root folder), run the following command - +From OpenSearch-Dashbaords repo (root folder), run the following command - - `yarn start` Starts OpenSearch Dashboards and includes this plugin. OpenSearch Dashboards will be available on `localhost:5601`. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 9d67652b..95c18283 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,144 +1,11 @@ -- [Overview](#overview) -- [Current Maintainers](#current-maintainers) -- [Maintainer Responsibilities](#maintainer-responsibilities) - - [Uphold Code of Conduct](#uphold-code-of-conduct) - - [Prioritize Security](#prioritize-security) - - [Review Pull Requests](#review-pull-requests) - - [Triage Open Issues](#triage-open-issues) - - [Be Responsive](#be-responsive) - - [Maintain Overall Health of the Repo](#maintain-overall-health-of-the-repo) - - [Manage Roadmap](#manage-roadmap) - - [Add Continuous Integration Checks](#add-continuous-integration-checks) - - [Developer Certificate of Origin Workflow](#developer-certificate-of-origin-workflow) - - [Use Semver](#use-semver) - - [Release Frequently](#release-frequently) - - [Promote Other Maintainers](#promote-other-maintainers) - - [Describe the Repo](#describe-the-repo) -- [Becoming a Maintainer](#becoming-a-maintainer) - - [Nomination](#nomination) - - [Interest](#interest) - - [Addition](#addition) -- [Removing a Maintainer](#removing-a-maintainer) - - [Moving On](#moving-on) - - [Inactivity](#inactivity) - - [Negative Impact on the Project](#negative-impact-on-the-project) - ## Overview -This document explains who the maintainers are (see below), what they do in this repo, and how they should be doing it. If you're interested in contributing, see [CONTRIBUTING](CONTRIBUTING.md). +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). ## Current Maintainers -| Maintainer | GitHub ID | Affiliation | -| ------------------------ | --------------------------------------- | ----------- | -| Shivam Dhar | [Shivamdhar](https://github.com/Shivamdhar) | Amazon | -| Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | -| Vamshi Vijay Nakkirtha | [vamshin](https://github.com/vamshin) | Amazon | - -## Maintainer Responsibilities - -Maintainers are active and visible members of the community, and have [maintain-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 evolve code as follows. - -### Uphold Code of Conduct - -Model the behavior set forward by the [Code of Conduct](CODE_OF_CONDUCT.md) and raise any violations to other maintainers and admins. - -### Prioritize Security - -Security is your number one priority. Maintainer's Github keys must be password protected securely and any reported security vulnerabilities are addressed before features or bugs. - -Note that this repository is monitored and supported 24/7 by Amazon Security, see [Reporting a Vulnerability](SECURITY.md) for details. - -### Review Pull Requests - -Review pull requests regularly, comment, suggest, reject, merge and close. Accept only high quality pull-requests. Provide code reviews and guidance on incoming pull requests. Don't let PRs be stale and do your best to be helpful to contributors. - -### Triage Open Issues - -Manage labels, review issues regularly, and triage by labelling them. - -All repositories in this organization have a standard set of labels, including `bug`, `documentation`, `duplicate`, `enhancement`, `good first issue`, `help wanted`, `blocker`, `invalid`, `question`, `wontfix`, and `untriaged`, along with release labels, such as `v1.0.0`, `v1.1.0`, `v2.0.0`, `patch`, and `backport`. - -Use labels to target an issue or a PR for a given release, add `help wanted` to good issues for new community members, and `blocker` for issues that scare you or need immediate attention. Request for more information from a submitter if an issue is not clear. Create new labels as needed by the project. - -### Be Responsive - -Respond to enhancement requests, and forum posts. Allocate time to reviewing and commenting on issues and conversations as they come in. - -### Maintain Overall Health of the Repo - -Keep the `main` branch at production quality at all times. Backport features as needed. Cut release branches and tags to enable future patches. - -### Manage Roadmap - -Ensure the repo highlights features that should be elevated to the project roadmap. Be clear about the feature’s status, priority, target version, and whether or not it should be elevated to the roadmap. Any feature that you want highlighted on the OpenSearch Roadmap should be tagged with "roadmap". The OpenSearch [project-meta maintainers](https://github.com/opensearch-project/project-meta/blob/main/MAINTAINERS.md) will highlight features tagged "roadmap" on the project wide [OpenSearch Roadmap](https://github.com/orgs/opensearch-project/projects/1). - -### Add Continuous Integration Checks - -Add integration checks that validate pull requests and pushes to ease the burden on Pull Request reviewers. - -#### Developer Certificate of Origin Workflow - -Validates pull requests commits are all signed with DCO, [dco.yml](./workflow/dco.yml). Example [pull request](https://github.com/opensearch-project/opensearch-ci/pull/16). - -### Use Semver - -Use and enforce [semantic versioning](https://semver.org/) and do not let breaking changes be made outside of major releases. - -### Release Frequently - -Make frequent project releases to the community. - -### Promote Other Maintainers - -Assist, add, and remove [MAINTAINERS](MAINTAINERS.md). Exercise good judgement, and propose high quality contributors to become co-maintainers. See [Becoming a Maintainer](#becoming-a-maintainer) for more information. - -### Describe the Repo - -Make sure the repo has a well-written, accurate, and complete description. See [opensearch-project/.github#38](https://github.com/opensearch-project/.github/issues/38) for some helpful tips to describe your repo. - -## Becoming a Maintainer - -You can become a maintainer by actively [contributing](CONTRIBUTING.md) to any project, and being nominated by an existing maintainer. - -### Nomination - -Any current maintainer starts a private e-mail thread (until we have a better mechanism, e-mail addresses can usually be found via MAINTAINERS.md + DCO) with all other maintainers on that repository to discuss nomination using the template below. In order to be approved, at least three positive (+1) maintainer votes are necessary, and no vetoes (-1). In rare cases when there are fewer than three maintainers, the positive (+1) votes from all maintainers are required. Any disagreements can be escalated to the repo admin. - -The nomination should clearly identify the person with their real name and a link to their GitHub profile, and the rationale for the nomination, with concrete example contributions. - -### Interest - -Upon receiving at least three positive (+1) maintainer votes, and no vetoes (-1), from existing maintainers after a one week period, the nominating maintainer asks the nominee whether they might be interested in becoming a maintainer on the repository via private e-mail message. - -> This is great work! Based on your valuable contribution and ongoing engagement with the project, the current maintainers invite you to become a co-maintainer for this project. Please respond and let us know if you accept the invitation to become maintainer. - -Individuals accept the nomination by replying, or commenting, for example _"Thank you! I would love to."_ - -### Addition - -Upon receiving three positive (+1) maintainer votes, and no vetoes (-1), from other maintainers, and after having privately confirmed interest with the nominee, the maintainer opens a pull request adding the proposed co-maintainer to MAINTAINERS.md. The pull request is approved and merged. - -> _Content from the above nomination._ -> -> The maintainers have voted and agreed to this nomination. - -The repo admin adjusts the new maintainer’s permissions accordingly, and merges the pull request. - -## Removing a Maintainer - -Removing a maintainer is a disruptive action that the community of maintainers should not undertake lightly. There are several reasons a maintainer will be removed from the project, such as violating the [code of conduct](https://github.com/opensearch-project/.github/blob/main/CODE_OF_CONDUCT.md), or taking other actions that negatively impact the project. - -### Moving On - -There are plenty of reasons that might cause someone to want to take a step back or even a hiatus from a project. Existing maintainers can choose to leave the project at any time, with or without reason, by making a pull request to move themselves to the "Emeritus" section of MAINTAINERS.md, and asking an admin to remove their permissions. - -### Inactivity - -Maintainer status never expires. If a maintainer becomes inactive for a time (usually several months), the repo admin may revoke maintainer level access to the repository for security reasons. A maintainer can reach out to the repo admin to get their permissions reinstated. - -If the repo is left without any maintainers, either by maintainer inactivity or moving on, the repo is considered unmaintained. The repo admin will seek out new maintainers and note the maintenance status in the repo README file. - -### Negative Impact on the Project - -Actions that negatively impact the project will be handled by the admins, in coordination with other maintainers, in balance with the urgency of the issue. Examples would be [Code of Conduct](CODE_OF_CONDUCT.md) violations, deliberate harmful or malicious actions, and security risks. +| Maintainer | GitHub ID | Affiliation | +| ----------------------- | ------------------------------------------- | ----------- | +| Shivam Dhar | [Shivamdhar](https://github.com/Shivamdhar) | Amazon | +| Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon | +| Vamshi Vijay Nakkirtha | [vamshin](https://github.com/vamshin) | Amazon | diff --git a/README.md b/README.md index ef070e8f..04b58295 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![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) -[![Integration tests](https://github.com/opensearch-project/dashboards-maps/workflows/E2E%20Cypress%20tests/badge.svg)](https://github.com/opensearch-project/dashboards-maps/actions?query=workflow%3A%22E2E+Cypress+tests%22) [![codecov](https://codecov.io/gh/opensearch-project/dashboards-maps/branch/main/graph/badge.svg)](https://codecov.io/gh/opensearch-project/dashboards-maps) [![Forum](https://img.shields.io/badge/chat-on%20forums-blue)](https://forum.opensearch.org/) ![PRs welcome!](https://img.shields.io/badge/PRs-welcome!-success) diff --git a/custom_import_map/babel.config.js b/babel.config.js similarity index 100% rename from custom_import_map/babel.config.js rename to babel.config.js diff --git a/custom_import_map/common/constants/shared.ts b/common/constants/shared.ts similarity index 83% rename from custom_import_map/common/constants/shared.ts rename to common/constants/shared.ts index 3290bd78..7d1ed801 100644 --- a/custom_import_map/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -12,3 +12,5 @@ export const MAX_FILE_PAYLOAD_SIZE_IN_MB = 25; export const MAX_FILE_PAYLOAD_SIZE = fromMBtoBytes(MAX_FILE_PAYLOAD_SIZE_IN_MB); export const PLUGIN_ID = 'customImportMap'; export const PLUGIN_NAME = 'customImportMap'; +export const PLUGIN_NAVIGATION_BAR_TILE = 'Maps'; +export const PLUGIN_NAVIGATION_BAR_ID = 'maps-dashboards'; diff --git a/common/index.ts b/common/index.ts new file mode 100644 index 00000000..94b49746 --- /dev/null +++ b/common/index.ts @@ -0,0 +1,121 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fromMBtoBytes } from './util'; +import { + ALLOWED_FILE_EXTENSIONS, + MAX_FILE_PAYLOAD_SIZE, + MAX_FILE_PAYLOAD_SIZE_IN_MB, + PLUGIN_ID, + PLUGIN_NAVIGATION_BAR_ID, + PLUGIN_NAME, +} from './constants/shared'; + +export { + fromMBtoBytes, + ALLOWED_FILE_EXTENSIONS, + MAX_FILE_PAYLOAD_SIZE, + MAX_FILE_PAYLOAD_SIZE_IN_MB, + PLUGIN_ID, + PLUGIN_NAVIGATION_BAR_ID, + PLUGIN_NAME, +}; + +export const MAP_VECTOR_TILE_BASIC_STYLE = 'https://tiles.maps.opensearch.org/styles/basic.json'; +export const MAP_GLYPHS = 'https://tiles.maps.opensearch.org/fonts/{fontstack}/{range}.pbf'; +export const MAP_VECTOR_TILE_DATA_SOURCE = 'https://tiles.maps.opensearch.org/data/v1.json'; +export const MAP_DEFAULT_MIN_ZOOM = 0; +export const MAP_DEFAULT_MAX_ZOOM = 22; +export const MAP_REFERENCE_LAYER_DEFAULT_OPACITY = 100; +export const MAP_DATA_LAYER_DEFAULT_OPACITY = 70; +export const MAP_LAYER_DEFAULT_MIN_OPACITY = 0; +export const MAP_LAYER_DEFAULT_MAX_OPACITY = 100; +export const MAP_LAYER_DEFAULT_OPACITY_STEP = 1; +export const MAP_LAYER_DEFAULT_BORDER_THICKNESS = 1; +export const DOCUMENTS_DEFAULT_REQUEST_NUMBER = 1000; +export const DOCUMENTS_DEFAULT_SHOW_TOOLTIPS: boolean = false; +export const DOCUMENTS_DEFAULT_TOOLTIPS: string[] = []; +export const DOCUMENTS_DEFAULT_MARKER_SIZE = 5; +export const LAYER_PANEL_SHOW_LAYER_ICON = 'eye'; +export const LAYER_PANEL_HIDE_LAYER_ICON = 'eyeClosed'; +export const MAX_LAYER_NAME_LIMIT = 35; +export const MAP_LAYER_DEFAULT_NAME = 'Default map'; +export const NEW_MAP_LAYER_DEFAULT_PREFIX = 'New layer'; + +// Starting position [lng, lat] and zoom +export const MAP_INITIAL_STATE = { + lng: 0, + lat: 0, + zoom: 1, +}; + +export const APP_PATH = { + LANDING_PAGE_PATH: '/', + CREATE_MAP: '/create', + EDIT_MAP: '/:id', +}; + +export enum DASHBOARDS_MAPS_LAYER_NAME { + OPENSEARCH_MAP = 'OpenSearch map', + DOCUMENTS = 'Documents', + CUSTOM_MAP = 'Custom map', +} + +export enum DASHBOARDS_MAPS_LAYER_TYPE { + OPENSEARCH_MAP = 'opensearch_vector_tile_map', + DOCUMENTS = 'documents', + CUSTOM_MAP = 'custom_map', +} + +export enum DASHBOARDS_MAPS_LAYER_ICON { + OPENSEARCH_MAP = 'globe', + DOCUMENTS = 'document', + CUSTOM_MAP = 'globe', +} + +export enum DASHBOARDS_MAPS_LAYER_DESCRIPTION { + OPENSEARCH_MAP = 'Use default OpenSearch basemaps.', + DOCUMENTS = 'View points, lines, and polygons on the map.', + CUSTOM_MAP = 'Configure maps to use a custom map source.', +} + +export const DOCUMENTS = { + name: DASHBOARDS_MAPS_LAYER_NAME.DOCUMENTS, + type: DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS, + icon: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.DOCUMENTS, +}; + +export const OPENSEARCH_MAP_LAYER = { + name: DASHBOARDS_MAPS_LAYER_NAME.OPENSEARCH_MAP, + type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP, + icon: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.OPENSEARCH_MAP, +}; + +export const CUSTOM_MAP = { + name: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, + type: DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP, + icon: DASHBOARDS_MAPS_LAYER_ICON.CUSTOM_MAP, + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.CUSTOM_MAP, +}; + +export interface Layer { + name: DASHBOARDS_MAPS_LAYER_NAME; + type: DASHBOARDS_MAPS_LAYER_TYPE; + icon: DASHBOARDS_MAPS_LAYER_ICON; + description: DASHBOARDS_MAPS_LAYER_DESCRIPTION; +} + +export const LAYER_VISIBILITY = { + NONE: 'none', + VISIBLE: 'visible', +}; + +export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: 'globe', + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: 'document', + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe', +}; diff --git a/common/map_saved_object_attributes.ts b/common/map_saved_object_attributes.ts new file mode 100644 index 00000000..3d071ebb --- /dev/null +++ b/common/map_saved_object_attributes.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from 'opensearch-dashboards/server'; + +export interface MapSavedObjectAttributes extends SavedObjectAttributes { + /** Title of the map */ + title: string; + /** Description of the map */ + description?: string; + /** State of the map, which could include current zoom level, lat, lng etc. */ + mapState?: string; + /** Maps-dashboards layers of the map */ + layerList?: string; + /** UI state of the map */ + uiState?: string; + /** Version is used to track version differences in saved object mapping */ + version: number; + /** SearchSourceFields is used to reference other saved objects */ + searchSourceFields?: { + index?: string; + }; +} diff --git a/custom_import_map/common/util.ts b/common/util.ts similarity index 100% rename from custom_import_map/common/util.ts rename to common/util.ts diff --git a/custom_import_map/.babelrc b/custom_import_map/.babelrc deleted file mode 100644 index a14d6b62..00000000 --- a/custom_import_map/.babelrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { "node": "10" } - } - ], - "@babel/preset-react", - "@babel/preset-typescript" - ], - "plugins": [ - "@babel/plugin-syntax-jsx", - "@babel/plugin-transform-modules-commonjs", - ["@babel/plugin-transform-runtime", { "regenerator": true }], - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-object-rest-spread" - ] - } diff --git a/custom_import_map/.eslintrc b/custom_import_map/.eslintrc deleted file mode 100644 index 70b6ed10..00000000 --- a/custom_import_map/.eslintrc +++ /dev/null @@ -1,4 +0,0 @@ ---- -extends: - - "@elastic/eslint-config-kibana" - - "plugin:@elastic/eui/recommended" diff --git a/custom_import_map/.gitignore b/custom_import_map/.gitignore deleted file mode 100644 index a4b274f0..00000000 --- a/custom_import_map/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -target/ -build/ -coverage/ diff --git a/custom_import_map/.lintstagedrc b/custom_import_map/.lintstagedrc deleted file mode 100644 index 6b707f91..00000000 --- a/custom_import_map/.lintstagedrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "*.{js,jsx,json,css,md}": ["prettier --write", "git add"] -} diff --git a/custom_import_map/.opensearch_dashboards-plugin-helpers.json b/custom_import_map/.opensearch_dashboards-plugin-helpers.json deleted file mode 100644 index 47278c3b..00000000 --- a/custom_import_map/.opensearch_dashboards-plugin-helpers.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "serverSourcePatterns": [ - "package.json", - "tsconfig.json", - "yarn.lock", - ".yarnrc", - "{lib,public,server,webpackShims,translations,utils,models,test,common}/**/*", - "!__tests__" - ] - } \ No newline at end of file diff --git a/custom_import_map/.prettierignore b/custom_import_map/.prettierignore deleted file mode 100644 index 3d2f91d2..00000000 --- a/custom_import_map/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -.vscode -build -coverage -node_modules -npm-debug.log -yarn.lock -*.md -*.lock diff --git a/custom_import_map/.prettierrc b/custom_import_map/.prettierrc deleted file mode 100644 index d4401713..00000000 --- a/custom_import_map/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "es5", - "printWidth": 100 -} diff --git a/custom_import_map/build.sh b/custom_import_map/build.sh deleted file mode 100644 index 8da0f918..00000000 --- a/custom_import_map/build.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -# Copyright OpenSearch Contributors -# SPDX-License-Identifier: Apache-2.0 -# -# The OpenSearch Contributors require contributions made to -# this file be licensed under the Apache-2.0 license or a -# compatible open source license. - -set -ex - -function usage() { - echo "Usage: $0 [args]" - echo "" - echo "Arguments:" - echo -e "-v VERSION\t[Required] OpenSearch version." - echo -e "-q QUALIFIER\t[Optional] Version qualifier." - echo -e "-s SNAPSHOT\t[Optional] Build a snapshot, default is 'false'." - echo -e "-p PLATFORM\t[Optional] Platform, ignored." - echo -e "-a ARCHITECTURE\t[Optional] Build architecture, ignored." - echo -e "-o OUTPUT\t[Optional] Output path, default is 'artifacts'." - echo -e "-h help" -} - -while getopts ":h:v:q:s:o:p:a:" arg; do - case $arg in - h) - usage - exit 1 - ;; - v) - VERSION=$OPTARG - ;; - q) - QUALIFIER=$OPTARG - ;; - s) - SNAPSHOT=$OPTARG - ;; - o) - OUTPUT=$OPTARG - ;; - p) - PLATFORM=$OPTARG - ;; - a) - ARCHITECTURE=$OPTARG - ;; - :) - echo "Error: -${OPTARG} requires an argument" - usage - exit 1 - ;; - ?) - echo "Invalid option: -${arg}" - exit 1 - ;; - esac -done - -if [ -z "$VERSION" ]; then - echo "Error: You must specify the OpenSearch Dashboards version" - usage - exit 1 -fi - -[ -z "$OUTPUT" ] && OUTPUT=artifacts -[ ! -z "$QUALIFIER" ] && QUALIFIER_IDENTIFIER="-$QUALIFIER" - -NVM_CMD="source $NVM_DIR/nvm.sh && nvm use" -if [ "$PLATFORM" = "windows" ]; then - NVM_CMD="volta install node@`cat ../../OpenSearch-Dashboards/.nvmrc` && volta install yarn@`jq -r '.engines.yarn' ../../OpenSearch-Dashboards/package.json`" -fi - -mkdir -p $OUTPUT/plugins -PLUGIN_FOLDER=$(basename "$PWD") -PLUGIN_NAME=customImportMapDashboards -# TODO: [CLEANUP] Needed OpenSearch Dashboards git repo to build the required modules for plugins -# This makes it so there is a dependency on having Dashboards pulled already. -cp -r ../$PLUGIN_FOLDER/ ../../OpenSearch-Dashboards/plugins -echo "BUILD MODULES FOR $PLUGIN_NAME" -(cd ../../OpenSearch-Dashboards && eval $NVM_CMD && yarn osd bootstrap) -echo "BUILD RELEASE ZIP FOR $PLUGIN_NAME" -(cd ../../OpenSearch-Dashboards && eval $NVM_CMD && cd plugins/$PLUGIN_FOLDER && yarn plugin-helpers build --opensearch-dashboards-version=$VERSION$QUALIFIER_IDENTIFIER) -echo "COPY $PLUGIN_NAME.zip" -cp -r ../../OpenSearch-Dashboards/plugins/$PLUGIN_FOLDER/build/$PLUGIN_NAME-$VERSION$QUALIFIER_IDENTIFIER.zip $OUTPUT/plugins/ -rm -rf ../../OpenSearch-Dashboards/plugins/$PLUGIN_FOLDER diff --git a/custom_import_map/common/index.ts b/custom_import_map/common/index.ts deleted file mode 100644 index 10feca0e..00000000 --- a/custom_import_map/common/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { fromMBtoBytes } from './util'; -import { - ALLOWED_FILE_EXTENSIONS, - MAX_FILE_PAYLOAD_SIZE, - MAX_FILE_PAYLOAD_SIZE_IN_MB, - PLUGIN_ID, - PLUGIN_NAME, -} from './constants/shared'; - -export { - fromMBtoBytes, - ALLOWED_FILE_EXTENSIONS, - MAX_FILE_PAYLOAD_SIZE, - MAX_FILE_PAYLOAD_SIZE_IN_MB, - PLUGIN_ID, - PLUGIN_NAME, -}; diff --git a/custom_import_map/cypress/support/commands.js b/custom_import_map/cypress/support/commands.js deleted file mode 100644 index 2f2587e5..00000000 --- a/custom_import_map/cypress/support/commands.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const { ADMIN_AUTH } = require('./constants'); - -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) - -Cypress.Commands.overwrite('visit', (originalFn, url, options) => { - // Add the basic auth header when security enabled in the Opensearch cluster - // https://github.com/cypress-io/cypress/issues/1288 - if (Cypress.env('security_enabled')) { - if (options) { - options.auth = ADMIN_AUTH; - } else { - options = { auth: ADMIN_AUTH }; - } - // Add query parameters - select the default OpenSearch Dashboards tenant - options.qs = { security_tenant: 'private' }; - return originalFn(url, options); - } else { - return originalFn(url, options); - } -}); - -// Be able to add default options to cy.request(), https://github.com/cypress-io/cypress/issues/726 -Cypress.Commands.overwrite('request', (originalFn, ...args) => { - let defaults = {}; - // Add the basic authentication header when security enabled in the Opensearch cluster - if (Cypress.env('security_enabled')) { - defaults.auth = ADMIN_AUTH; - } - - let options = {}; - if (typeof args[0] === 'object' && args[0] !== null) { - options = Object.assign({}, args[0]); - } else if (args.length === 1) { - [options.url] = args; - } else if (args.length === 2) { - [options.method, options.url] = args; - } else if (args.length === 3) { - [options.method, options.url, options.body] = args; - } - - return originalFn(Object.assign({}, defaults, options)); -}); diff --git a/custom_import_map/public/plugin.tsx b/custom_import_map/public/plugin.tsx deleted file mode 100644 index f65d838d..00000000 --- a/custom_import_map/public/plugin.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { i18n } from '@osd/i18n'; -import { CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; -import { - CustomImportMapPluginSetup, - CustomImportMapPluginStart, - AppPluginSetupDependencies, -} from './types'; -import { RegionMapVisualizationDependencies } from '../../../src/plugins/region_map/public'; -import { VectorUploadOptions } from './components/vector_upload_options'; -import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; - -export class CustomImportMapPlugin - implements Plugin { - public setup( - core: CoreSetup, - { regionMap }: AppPluginSetupDependencies - ): CustomImportMapPluginSetup { - const customSetup = async () => { - const [coreStart] = await core.getStartServices(); - regionMap.addOptionTab({ - name: 'controls', - title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.controlTabs.controlsTitle', { - defaultMessage: 'Import Vector Map', - }), - editor: (props: RegionMapVisualizationDependencies) => ( - - - - ), - }); - }; - customSetup(); - - // Return methods that should be available to other plugins - return {}; - } - - public start(core: CoreStart): CustomImportMapPluginStart { - return {}; - } - - public stop() {} -} diff --git a/custom_import_map/public/types.ts b/custom_import_map/public/types.ts deleted file mode 100644 index 791b872f..00000000 --- a/custom_import_map/public/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; -import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomImportMapPluginSetup {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomImportMapPluginStart {} - -export interface AppPluginStartDependencies { - navigation: NavigationPublicPluginStart; -} - -export interface AppPluginSetupDependencies { - regionMap: RegionMapPluginSetup; -} diff --git a/custom_import_map/tsconfig.json b/custom_import_map/tsconfig.json deleted file mode 100644 index e662ef11..00000000 --- a/custom_import_map/tsconfig.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "compilerOptions": { - "skipLibCheck": true, - "baseUrl": ".", - "paths": { - // Allows for importing from `opensearch-dashboards` package for the exported types. - "opensearch-dashboards": ["./opensearch_dashboards"], - "ui/*": ["src/legacy/ui/public/*"], - "test_utils/*": ["src/test_utils/public/*"] - }, - // Support .tsx files and transform JSX into calls to React.createElement - "jsx": "react", - // Enables all strict type checking options. - "strict": true, - // enables "core language features" - "lib": [ - // ESNext auto includes previous versions all the way back to es5 - "esnext", - // includes support for browser APIs - "dom" - ], - // Node 8 should support everything output by esnext, we override this - // in webpack with loader-level compiler options - "target": "esnext", - // Use commonjs for node, overridden in webpack to keep import statements - // to maintain support for things like `await import()` - "module": "commonjs", - // Allows default imports from modules with no default export. This does not affect code emit, just type checking. - // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or - // ESNext module format is used. - "allowSyntheticDefaultImports": true, - // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. - "esModuleInterop": true, - // Resolve modules in the same way as Node.js. Aka make `require` works the - // same in TypeScript as it does in Node.js. - "moduleResolution": "node", - // Disallow inconsistently-cased references to the same file. - "forceConsistentCasingInFileNames": true, - // Disable the breaking keyof behaviour introduced in TS 2.9.2 until EUI is updated to support that too - "keyofStringsOnly": true, - // Forbid unused local variables as the rule was deprecated by ts-lint - "noUnusedLocals": true, - // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. - "downlevelIteration": true, - // import tslib helpers rather than inlining helpers for iteration or spreading, for instance - "importHelpers": true, - // adding global typings - "types": ["node", "jest", "react"] - }, - "include": [ - "common/**/*", - "server/**/*", - "public/**/*", - "utils/**/*", - "models/**/*", - "test/**/*" - ], - "exclude": ["node_modules", "*/node_modules/"] - } diff --git a/custom_import_map/cypress.json b/cypress.json similarity index 100% rename from custom_import_map/cypress.json rename to cypress.json diff --git a/custom_import_map/cypress/fixtures/sample_geojson.json b/cypress/fixtures/sample_geojson.json similarity index 100% rename from custom_import_map/cypress/fixtures/sample_geojson.json rename to cypress/fixtures/sample_geojson.json diff --git a/cypress/integration/documentsLayer.spec.js b/cypress/integration/documentsLayer.spec.js new file mode 100644 index 00000000..58df4fc3 --- /dev/null +++ b/cypress/integration/documentsLayer.spec.js @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../utils/constants'; + +describe('Documents layer', () => { + before(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { + retryOnStatusCodeFailure: true, + timeout: 60000, + }); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/(Add|View) data/) + .click(); + cy.wait(60000); + }); + + const uniqueName = 'saved-map-' + Date.now().toString(); + + it('Add new documents layer with configuration', () => { + cy.visit(`${BASE_PATH}/app/maps-dashboards`); + cy.contains('Create map').click(); + cy.get("button[data-test-subj='addLayerButton']").click(); + cy.contains('Documents').click(); + cy.contains('Select data source', { timeout: 60000 }).click({ force: true }); + cy.contains('opensearch_dashboards_sample_data_flights').click(); + cy.contains('Select data field', { timeout: 60000 }).click({ force: true }); + cy.contains('DestLocation').click(); + cy.get('[data-test-subj="indexPatternSelect"]').should( + 'contain', + 'opensearch_dashboards_sample_data_flights' + ); + cy.get('[data-test-subj="geoFieldSelect"]').should('contain', 'DestLocation'); + cy.get(`button[testSubj="styleTab"]`).click(); + cy.contains('Fill color').click(); + cy.get(`button[aria-label="Select #E7664C as the color"]`).click(); + cy.contains('Border color').click(); + cy.get(`button[aria-label="Select #DA8B45 as the color"]`).click(); + cy.get(`button[testSubj="settingsTab"]`).click(); + cy.get('[name="layerName"]').clear().type('Documents layer 1'); + cy.get(`button[data-test-subj="updateButton"]`).click(); + cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Documents layer 1'); + cy.wait(5000).get('[data-test-subj="top-nav"]').click(); + cy.wait(5000).get('[data-test-subj="savedObjectTitle"]').type(uniqueName); + cy.wait(5000).get('[data-test-subj="confirmSaveSavedObjectButton"]').click(); + }); + + it('Open saved map with documents layer', () => { + cy.visit(`${BASE_PATH}/app/maps-dashboards`); + cy.get('[data-test-subj="mapListingPage"]').should('contain', uniqueName); + cy.contains(uniqueName).click(); + cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Documents layer 1'); + }); +}); diff --git a/custom_import_map/cypress/integration/geojson_file_upload.spec.js b/cypress/integration/geojson_file_upload.spec.js similarity index 72% rename from custom_import_map/cypress/integration/geojson_file_upload.spec.js rename to cypress/integration/geojson_file_upload.spec.js index cc406d2d..854e4ec9 100644 --- a/custom_import_map/cypress/integration/geojson_file_upload.spec.js +++ b/cypress/integration/geojson_file_upload.spec.js @@ -10,16 +10,14 @@ import 'cypress-file-upload'; describe('Verify successful custom geojson file upload', () => { before(() => { - cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`); - // Click on "Sample data" tab - cy.contains('Sample data').click({ force: true }); - // Load sample flights data - cy.get(`button[data-test-subj="addSampleDataSetflights"]`).click({ - force: true, + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { + retryOnStatusCodeFailure: true, + timeout: 60000, }); - - // Verify that sample data is add by checking toast notification - cy.contains('Sample flight data installed', { timeout: 240000 }); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/(Add|View) data/) + .click(); + cy.wait(60000); cy.visit(`${BASE_PATH}/app/visualize#/`); @@ -40,11 +38,14 @@ describe('Verify successful custom geojson file upload', () => { cy.get('[data-testId="filePicker"]').attachFile('sample_geojson.json'); cy.get('[data-testId="customIndex"]').type('sample'); cy.contains('Import file').click({ force: true }); - cy.contains('Successfully added 2 features to sample-map. Refresh to visualize the uploaded map.', { timeout: 240000 }); - }) + cy.contains( + 'Successfully added 2 features to sample-map. Refresh to visualize the uploaded map.', + { timeout: 240000 } + ); + }); after(() => { cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); - }) + }); }); diff --git a/custom_import_map/cypress/integration/import_vector_map_tab.spec.js b/cypress/integration/import_vector_map_tab.spec.js similarity index 74% rename from custom_import_map/cypress/integration/import_vector_map_tab.spec.js rename to cypress/integration/import_vector_map_tab.spec.js index 86ebef79..ca8b0544 100644 --- a/custom_import_map/cypress/integration/import_vector_map_tab.spec.js +++ b/cypress/integration/import_vector_map_tab.spec.js @@ -9,16 +9,14 @@ import { BASE_PATH } from '../utils/constants'; describe('Verify the presence of import custom map tab in region map plugin', () => { before(() => { - cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`); - // Click on "Sample data" tab - cy.contains('Sample data').click({ force: true }); - // Load sample flights data - cy.get(`button[data-test-subj="addSampleDataSetflights"]`).click({ - force: true, + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { + retryOnStatusCodeFailure: true, + timeout: 60000, }); - - // Verify that sample data is add by checking toast notification - cy.contains('Sample flight data installed', { timeout: 240000 }); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/(Add|View) data/) + .click(); + cy.wait(60000); cy.visit(`${BASE_PATH}/app/visualize#/`); @@ -35,10 +33,10 @@ describe('Verify the presence of import custom map tab in region map plugin', () it('checks import custom map tab is present', () => { // Click on "Import Vector Map" tab, which is part of customImportMap plugin cy.contains('Import Vector Map').click({ force: true }); - }) + }); after(() => { cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); - }) + }); }); diff --git a/cypress/integration/opensearchMapLayer.spec.js b/cypress/integration/opensearchMapLayer.spec.js new file mode 100644 index 00000000..75642344 --- /dev/null +++ b/cypress/integration/opensearchMapLayer.spec.js @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../utils/constants'; + +describe('Default OpenSearch base map layer', () => { + before(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { + retryOnStatusCodeFailure: true, + timeout: 60000, + }); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/(Add|View) data/) + .click(); + cy.wait(60000); + }); + + it('check if default OpenSearch map layer can be open', () => { + cy.visit(`${BASE_PATH}/app/maps-dashboards`); + cy.contains('Create map').click(); + cy.get('[data-test-subj="layerControlPanel"]').should('contain', 'Default map'); + cy.get('canvas.maplibregl-canvas').trigger('mousemove', { + x: 100, + y: 100, + force: true, + }); + cy.get('canvas.maplibregl-canvas').trigger('mousemove', { + x: 200, + y: 200, + force: true, + }); + for (let i = 0; i < 21; i++) { + cy.wait(1000).get('canvas.maplibregl-canvas').trigger('dblclick', { force: true }); + } + cy.get('[data-test-subj="mapStatusBar"]').should('contain', 'zoom: 22'); + }); +}); diff --git a/custom_import_map/cypress/plugins/index.js b/cypress/plugins/index.js similarity index 67% rename from custom_import_map/cypress/plugins/index.js rename to cypress/plugins/index.js index 4e2fbcc4..f58b65e0 100644 --- a/custom_import_map/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,6 +1,9 @@ /* - * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. */ /// @@ -20,7 +23,7 @@ /** * @type {Cypress.PluginConfig} */ - module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - }; \ No newline at end of file +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000..899ff71d --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +const { ADMIN_AUTH } = require('./constants'); + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +Cypress.Commands.overwrite('visit', (originalFn, url, options) => { + // Add the basic auth header when security enabled in the Opensearch cluster + // https://github.com/cypress-io/cypress/issues/1288 + if (Cypress.env('security_enabled')) { + if (options) { + options.auth = ADMIN_AUTH; + } else { + options = { auth: ADMIN_AUTH }; + } + // Add query parameters - select the default OpenSearch Dashboards tenant + options.qs = { security_tenant: 'private' }; + return originalFn(url, options); + } else { + return originalFn(url, options); + } +}); + +// Be able to add default options to cy.request(), https://github.com/cypress-io/cypress/issues/726 +Cypress.Commands.overwrite('request', (originalFn, ...args) => { + let defaults = {}; + // Add the basic authentication header when security enabled in the Opensearch cluster + if (Cypress.env('security_enabled')) { + defaults.auth = ADMIN_AUTH; + } + + let options = {}; + if (typeof args[0] === 'object' && args[0] !== null) { + options = Object.assign({}, args[0]); + } else if (args.length === 1) { + [options.url] = args; + } else if (args.length === 2) { + [options.method, options.url] = args; + } else if (args.length === 3) { + [options.method, options.url, options.body] = args; + } + + return originalFn(Object.assign({}, defaults, options)); +}); \ No newline at end of file diff --git a/cypress/support/constants.js b/cypress/support/constants.js new file mode 100644 index 00000000..84aa933f --- /dev/null +++ b/cypress/support/constants.js @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +export const ADMIN_AUTH = { + username: 'admin', + password: 'admin', +}; \ No newline at end of file diff --git a/custom_import_map/cypress/support/index.js b/cypress/support/index.js similarity index 89% rename from custom_import_map/cypress/support/index.js rename to cypress/support/index.js index 08d440df..19655d13 100644 --- a/custom_import_map/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,6 +1,9 @@ /* - * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. */ // *********************************************************** @@ -39,4 +42,5 @@ if (Cypress.env('security_enabled')) { Cypress.env('opensearch', `https://${Cypress.env('opensearch_url')}`); } else { Cypress.env('opensearch', `http://${Cypress.env('opensearch_url')}`); -} \ No newline at end of file +} + diff --git a/cypress/utils/constants.js b/cypress/utils/constants.js new file mode 100644 index 00000000..6997c8e7 --- /dev/null +++ b/cypress/utils/constants.js @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +export const BASE_PATH = Cypress.env('base_url'); \ No newline at end of file diff --git a/custom_import_map/opensearch_dashboards.json b/opensearch_dashboards.json similarity index 60% rename from custom_import_map/opensearch_dashboards.json rename to opensearch_dashboards.json index 3ed9e323..9d7e117c 100644 --- a/custom_import_map/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -1,9 +1,9 @@ { "id": "customImportMapDashboards", - "version": "2.4.1.0", - "opensearchDashboardsVersion": "2.4.1", + "version": "2.5.0.0", + "opensearchDashboardsVersion": "2.5.0", "server": true, "ui": true, - "requiredPlugins": ["regionMap", "opensearchDashboardsReact"], + "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "navigation", "savedObjects", "data"], "optionalPlugins": [] } diff --git a/custom_import_map/package.json b/package.json similarity index 69% rename from custom_import_map/package.json rename to package.json index 334498ee..c96b52e1 100644 --- a/custom_import_map/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "customImportMap", - "version": "2.4.1.0", + "version": "2.5.0.0", "license": "Apache-2.0", "config": { "id": "customImportMapDashboards" @@ -12,22 +12,26 @@ "lint": "yarn run lint:es && yarn run lint:style", "lint:es": "node ../../scripts/eslint", "lint:style": "node ../../scripts/stylelint", - "test:jest": "../../node_modules/.bin/jest --config ./test/jest.config.js", - "postbuild": "echo Renaming build artifact to [$npm_package_config_id-$npm_package_version.zip] && mv build/$npm_package_config_id*.zip build/$npm_package_config_id-$npm_package_version.zip", - "opensearch": "node ../../scripts/opensearch" + "test:jest": "../../node_modules/.bin/jest --config ./test/jest.config.js" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, - "devDependencies": { - "cypress": "9.5.4", - "cypress-multi-reporters": "^1.5.0" - }, "dependencies": { - "@opensearch-dashboards-test/opensearch-dashboards-test-library": "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main", "@cypress/skip-test": "^2.6.1", - "cypress-file-upload": "^5.0.8" + "@opensearch-dashboards-test/opensearch-dashboards-test-library": "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main", + "@types/wellknown": "^0.5.4", + "cypress-file-upload": "^5.0.8", + "maplibre-gl": "^2.4.0", + "prettier": "^2.1.1", + "uuid": "3.3.2", + "wellknown": "^0.5.0" + }, + "devDependencies": { + "cypress": "9.5.4", + "cypress-multi-reporters": "^1.5.0", + "prettier": "^2.1.1" } } diff --git a/custom_import_map/cypress/utils/constants.js b/public/_variables.scss similarity index 63% rename from custom_import_map/cypress/utils/constants.js rename to public/_variables.scss index cdace4f0..98f04c0a 100644 --- a/custom_import_map/cypress/utils/constants.js +++ b/public/_variables.scss @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export const BASE_PATH = Cypress.env('base_url'); \ No newline at end of file +$mapHeaderOffset: 154px; diff --git a/public/application.tsx b/public/application.tsx new file mode 100644 index 00000000..aab8c3ea --- /dev/null +++ b/public/application.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters } from '../../../src/core/public'; +import { MapServices } from './types'; +import { MapsDashboardsApp } from './components/app'; +import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; + +export const renderApp = ({ element }: AppMountParameters, services: MapServices) => { + ReactDOM.render( + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/custom_import_map/public/components/__snapshots__/show_error_modal.test.tsx.snap b/public/components/__snapshots__/show_error_modal.test.tsx.snap similarity index 100% rename from custom_import_map/public/components/__snapshots__/show_error_modal.test.tsx.snap rename to public/components/__snapshots__/show_error_modal.test.tsx.snap diff --git a/custom_import_map/public/components/__snapshots__/vector_upload_options.test.tsx.snap b/public/components/__snapshots__/vector_upload_options.test.tsx.snap similarity index 94% rename from custom_import_map/public/components/__snapshots__/vector_upload_options.test.tsx.snap rename to public/components/__snapshots__/vector_upload_options.test.tsx.snap index eb4e289f..0c36eed6 100644 --- a/custom_import_map/public/components/__snapshots__/vector_upload_options.test.tsx.snap +++ b/public/components/__snapshots__/vector_upload_options.test.tsx.snap @@ -66,9 +66,16 @@ Object { class="euiFilePicker__prompt" id="generated-id-filePicker__prompt" > -
- EuiIconMock -
+
@@ -278,9 +285,16 @@ Object { class="euiFilePicker__prompt" id="generated-id-filePicker__prompt" > -
- EuiIconMock -
+
diff --git a/public/components/add_layer_panel/add_layer_panel.scss b/public/components/add_layer_panel/add_layer_panel.scss new file mode 100644 index 00000000..0209fa59 --- /dev/null +++ b/public/components/add_layer_panel/add_layer_panel.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.addLayer__button { + padding: $euiSizeM $euiSizeM; +} + +.addLayerDialog__description { + width: 272px; +} diff --git a/public/components/add_layer_panel/add_layer_panel.tsx b/public/components/add_layer_panel/add_layer_panel.tsx new file mode 100644 index 00000000..b6b81b00 --- /dev/null +++ b/public/components/add_layer_panel/add_layer_panel.tsx @@ -0,0 +1,154 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiHorizontalRule, + EuiTitle, + EuiButton, + EuiIcon, + EuiKeyPadMenuItem, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import './add_layer_panel.scss'; +import { + DOCUMENTS, + OPENSEARCH_MAP_LAYER, + CUSTOM_MAP, + Layer, + NEW_MAP_LAYER_DEFAULT_PREFIX, +} from '../../../common'; +import { getLayerConfigMap } from '../../utils/getIntialConfig'; + +interface Props { + setIsLayerConfigVisible: Function; + setSelectedLayerConfig: Function; + IsLayerConfigVisible: boolean; + addLayer: Function; + setIsNewLayer: Function; + newLayerIndex: number; +} + +export const AddLayerPanel = ({ + setIsLayerConfigVisible, + setSelectedLayerConfig, + IsLayerConfigVisible, + addLayer, + setIsNewLayer, + newLayerIndex, +}: Props) => { + const [isAddNewLayerModalVisible, setIsAddNewLayerModalVisible] = useState(false); + const [highlightItem, setHighlightItem] = useState(null); + + function onClickAddNewLayer(layerType: string) { + const initLayerConfig = getLayerConfigMap()[layerType]; + initLayerConfig.name = NEW_MAP_LAYER_DEFAULT_PREFIX + ' ' + newLayerIndex; + setSelectedLayerConfig(initLayerConfig); + setIsAddNewLayerModalVisible(false); + setIsLayerConfigVisible(true); + setIsNewLayer(true); + addLayer(initLayerConfig); + } + + const dataLayers = [DOCUMENTS]; + const dataLayerItems = Object.values(dataLayers).map((layerItem, index) => { + return ( + onClickAddNewLayer(layerItem.type)} + onFocus={() => setHighlightItem(layerItem)} + onMouseEnter={() => setHighlightItem(layerItem)} + onMouseLeave={() => setHighlightItem(null)} + onBlur={() => setHighlightItem(null)} + > + + + ); + }); + + const baseLayers = [OPENSEARCH_MAP_LAYER, CUSTOM_MAP]; + const baseLayersItems = Object.values(baseLayers).map((layerItem, index) => { + return ( + onClickAddNewLayer(layerItem.type)} + onFocus={() => setHighlightItem(layerItem)} + onMouseEnter={() => setHighlightItem(layerItem)} + onMouseLeave={() => setHighlightItem(null)} + onBlur={() => setHighlightItem(null)} + > + + + ); + }); + + const closeModal = () => setIsAddNewLayerModalVisible(false); + const showModal = () => setIsAddNewLayerModalVisible(true); + + return ( +
+ + + Add layer + + + {isAddNewLayerModalVisible && ( + + + +

Add layer

+
+
+ + + + +
Data layer
+
+ + {dataLayerItems} + + + +
Base layer
+
+ + {baseLayersItems} +
+ + +
{highlightItem?.name ? highlightItem.name : 'Select a layer type'}
+
+ + + {highlightItem?.description + ? highlightItem.description + : 'Start creating your map by selecting a layer type.'} + +
+
+
+
+ )} +
+ ); +}; diff --git a/custom_import_map/cypress/support/constants.js b/public/components/add_layer_panel/index.ts similarity index 53% rename from custom_import_map/cypress/support/constants.js rename to public/components/add_layer_panel/index.ts index 4f2416b5..d892657c 100644 --- a/custom_import_map/cypress/support/constants.js +++ b/public/components/add_layer_panel/index.ts @@ -3,7 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export const ADMIN_AUTH = { - username: 'admin', - password: 'admin', -}; \ No newline at end of file +export { AddLayerPanel } from './add_layer_panel'; diff --git a/public/components/app.tsx b/public/components/app.tsx new file mode 100644 index 00000000..207a6906 --- /dev/null +++ b/public/components/app.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Router, Route, Switch } from 'react-router-dom'; +import { I18nProvider } from '@osd/i18n/react'; +import { MapsList } from './maps_list'; +import { MapPage } from './map_page'; +import { APP_PATH } from '../../common'; +import { useOpenSearchDashboards } from '../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../types'; + +export const MapsDashboardsApp = () => { + const { + services: { appBasePath }, + } = useOpenSearchDashboards(); + // Render the application DOM. + return ( + + +
+ + } /> + } /> + +
+
+
+ ); +}; diff --git a/public/components/layer_config/base_map_layer_config_panel.tsx b/public/components/layer_config/base_map_layer_config_panel.tsx new file mode 100644 index 00000000..8b983623 --- /dev/null +++ b/public/components/layer_config/base_map_layer_config_panel.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { LayerBasicSettings } from './layer_basic_settings'; + +interface Props { + selectedLayerConfig: MapLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const BaseMapLayerConfigPanel = (props: Props) => { + const tabs = [ + { + id: 'settings--id', + name: 'Settings', + content: ( + + + + + ), + }, + ]; + return ; +}; diff --git a/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx b/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx new file mode 100644 index 00000000..5c6d1673 --- /dev/null +++ b/public/components/layer_config/custom_map_config/custom_map_config_panel.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { CustomLayerSpecification } from '../../../model/mapLayerType'; +import { LayerBasicSettings } from '../layer_basic_settings'; +import { CustomMapSource } from './custom_map_source'; + +interface Props { + selectedLayerConfig: CustomLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const CustomMapConfigPanel = (props: Props) => { + const newProps = { + ...props, + }; + + const tabs = [ + { + id: 'custom-map-source--id', + name: 'Data', + content: ( + + + + + ), + }, + { + id: 'settings--id', + name: 'Settings', + content: ( + + + + + ), + }, + ]; + return ; +}; diff --git a/public/components/layer_config/custom_map_config/custom_map_source.tsx b/public/components/layer_config/custom_map_config/custom_map_source.tsx new file mode 100644 index 00000000..5c0b5f3b --- /dev/null +++ b/public/components/layer_config/custom_map_config/custom_map_source.tsx @@ -0,0 +1,297 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { EuiSpacer, EuiPanel, EuiForm, EuiFieldText, EuiSelect, EuiFormRow } from '@elastic/eui'; +import { CustomLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + selectedLayerConfig: CustomLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; +} + +export const CustomMapSource = ({ + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdateDisabled, +}: Props) => { + const customMapTypeOptions = [ + { value: 'tms', text: 'Tile Map Service (TMS)' }, + { value: 'wms', text: 'Web Map Service (WMS)' }, + ]; + + const [customMapURL, setCustomMapURL] = useState(''); + const [customMapAttribution, setCustomMapAttribution] = useState(''); + const [customType, setCustomType] = useState(customMapTypeOptions[1].value); + const [WMSLayers, setWMSLayers] = useState(''); + const [WMSVersion, setWMSVersion] = useState(''); + const [WMSFormat, setWMSFormat] = useState(''); + const [WMSStyles, setWMSStyles] = useState(''); + // CRS: Coordinate reference systems in WMS + const [WMSCoordinateSystem, setWMSCoordinateSystem] = useState(''); + const [WMSBbox, setWMSBbox] = useState(''); + + const onChangeCustomMapURL = (e: any) => { + setCustomMapURL(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + url: e.target.value, + }, + }); + }; + + const onChangeCustomMapAttribution = (e: any) => { + setCustomMapAttribution(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + attribution: e.target.value, + }, + }); + }; + + const onChangeCustomType = (e: any) => { + setCustomType(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + customType: e.target.value, + }, + }); + }; + + const onChangeWMSLayers = (e: any) => { + setWMSLayers(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + layers: e.target.value, + }, + }); + }; + + const onChangeWMSVersion = (e: any) => { + setWMSVersion(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + version: e.target.value, + }, + }); + }; + + const onChangeWMSFormat = (e: any) => { + setWMSFormat(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + format: e.target.value, + }, + }); + }; + + const onChangeWMSStyles = (e: any) => { + setWMSStyles(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + styles: e.target.value, + }, + }); + }; + + const onChangeWMSCoordinateSystem = (e: any) => { + setWMSCoordinateSystem(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + crs: e.target.value, + }, + }); + }; + + const onChangeWMSBbox = (e: any) => { + setWMSBbox(e.target.value); + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { + ...selectedLayerConfig?.source, + bbox: e.target.value, + }, + }); + }; + + const isInvalidURL = (url: string): boolean => { + if (url === '') return false; + try { + new URL(url); + return false; + } catch (e) { + return true; + } + }; + + useEffect(() => { + setCustomMapURL(selectedLayerConfig.source.url); + setCustomType(selectedLayerConfig.source.customType); + setCustomMapAttribution(selectedLayerConfig.source.attribution); + if (selectedLayerConfig.source.customType === 'wms') { + setWMSLayers(selectedLayerConfig.source.layers); + setWMSVersion(selectedLayerConfig.source.version); + setWMSFormat(selectedLayerConfig.source.format); + setWMSStyles(selectedLayerConfig.source.styles); + setWMSCoordinateSystem(selectedLayerConfig.source.crs); + setWMSBbox(selectedLayerConfig.source.bbox); + } + }, [selectedLayerConfig]); + + useEffect(() => { + setCustomMapAttribution(selectedLayerConfig.source.attribution); + }, [selectedLayerConfig.source.attribution]); + + useEffect(() => { + if (customType === 'wms') { + setIsUpdateDisabled( + customMapURL === '' || + WMSLayers === '' || + WMSVersion === '' || + WMSFormat === '' || + isInvalidURL(customMapURL) + ); + } else { + setIsUpdateDisabled(customMapURL === '' || isInvalidURL(customMapURL)); + } + }, [WMSFormat, WMSLayers, WMSVersion, customMapURL, customType, setIsUpdateDisabled]); + + return ( +
+ + + + + + + + {selectedLayerConfig.source.customType === 'tms' && ( + + + + + + + + + + )} + {selectedLayerConfig.source.customType === 'wms' && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + +
+ ); +}; diff --git a/public/components/layer_config/documents_config/document_layer_config_panel.tsx b/public/components/layer_config/documents_config/document_layer_config_panel.tsx new file mode 100644 index 00000000..2a99abac --- /dev/null +++ b/public/components/layer_config/documents_config/document_layer_config_panel.tsx @@ -0,0 +1,84 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { DocumentLayerSpecification } from '../../../model/mapLayerType'; +import { LayerBasicSettings } from '../layer_basic_settings'; +import { DocumentLayerSource } from './document_layer_source'; +import { DocumentLayerStyle } from './document_layer_style'; + +interface Props { + selectedLayerConfig: DocumentLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const DocumentLayerConfigPanel = (props: Props) => { + const { selectedLayerConfig } = props; + + const checkKeys = [ + 'name', + { + key: 'source', + children: ['indexPatternId', 'geoFieldName'], + }, + ]; + const setIsUpdateDisabled = (isUpdateDisabled: boolean) => { + const check = (obj: any, keys: any) => { + return keys.some((key: any) => { + if (typeof key === 'string') { + return !obj[key]; + } else { + return !obj[key.key] || check(obj[key.key], key.children); + } + }); + }; + props.setIsUpdateDisabled(check(selectedLayerConfig, checkKeys) || isUpdateDisabled); + }; + + const newProps = { + ...props, + setIsUpdateDisabled, + }; + + const tabs = [ + { + id: 'data-source--id', + name: 'Data', + content: ( + + + + + ), + testSubj: 'dataTab', + }, + { + id: 'style--id', + name: 'Style', + content: ( + + + + + ), + testSubj: 'styleTab', + }, + { + id: 'settings--id', + name: 'Settings', + content: ( + + + + + ), + testSubj: 'settingsTab', + }, + ]; + return ; +}; diff --git a/public/components/layer_config/documents_config/document_layer_source.tsx b/public/components/layer_config/documents_config/document_layer_source.tsx new file mode 100644 index 00000000..ce0c40f1 --- /dev/null +++ b/public/components/layer_config/documents_config/document_layer_source.tsx @@ -0,0 +1,357 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiComboBox, + EuiFlexItem, + EuiFormLabel, + EuiFlexGrid, + EuiFieldNumber, + EuiFormErrorText, + EuiCollapsibleNavGroup, + EuiSpacer, + EuiPanel, + EuiForm, + EuiCheckbox, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import _, { Dictionary } from 'lodash'; +import { Filter, IndexPattern, IndexPatternField } from '../../../../../../src/plugins/data/public'; +import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../../types'; +import { DocumentLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + setSelectedLayerConfig: Function; + selectedLayerConfig: DocumentLayerSpecification; + setIsUpdateDisabled: Function; +} + +export const DocumentLayerSource = ({ + setSelectedLayerConfig, + selectedLayerConfig, + setIsUpdateDisabled, +}: Props) => { + const { + services: { + savedObjects: { client: savedObjectsClient }, + data: { + ui: { IndexPatternSelect, SearchBar }, + indexPatterns, + }, + }, + } = useOpenSearchDashboards(); + const [indexPattern, setIndexPattern] = useState(); + const [geoFields, setGeoFields] = useState(); + const [selectedField, setSelectedField] = useState(); + const [hasInvalidRequestNumber, setHasInvalidRequestNumber] = useState(false); + const [showTooltips, setShowTooltips] = useState( + selectedLayerConfig.source.showTooltips + ); + + const errorsMap = { + datasource: ['Required'], + geoFields: ['Required'], + }; + + useEffect(() => { + const disableUpdate = !indexPattern || !selectedField || hasInvalidRequestNumber; + setIsUpdateDisabled(disableUpdate); + }, [setIsUpdateDisabled, indexPattern, selectedField, hasInvalidRequestNumber]); + + const formatFieldToComboBox = (field?: IndexPatternField | null) => { + if (!field) return []; + return formatFieldsToComboBox([field]); + }; + + const formatFieldsToComboBox = (fields?: IndexPatternField[]) => { + if (!fields) return []; + + return fields?.map((field) => { + return { + label: field.displayName || field.name, + }; + }); + }; + + const tooltipFieldsOptions = () => { + const fieldList = indexPattern?.fields; + if (!fieldList) return []; + const fieldTypeMap: Dictionary = _.groupBy( + fieldList, + (field) => field.type + ); + + const fieldOptions: Array<{ label: string; options: Array<{ label: string }> }> = []; + let fieldsOfSameType: Array<{ label: string }> = []; + + Object.entries(fieldTypeMap).forEach(([fieldType, fieldEntries]) => { + for (const field of fieldEntries) { + fieldsOfSameType.push({ label: `${field.displayName || field.name}` }); + } + fieldOptions.push({ + label: `${fieldType}`, + options: fieldsOfSameType, + }); + fieldsOfSameType = []; + }); + return fieldOptions; + }; + + const formatTooltipFieldsToComboBox = (fields: string[]) => { + if (!fields) return []; + + return fields?.map((field) => { + return { + label: field, + }; + }); + }; + + const onDocumentRequestNumberChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const selectedNumber = parseInt(value, 10); + const source = { ...selectedLayerConfig.source, documentRequestNumber: selectedNumber }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const onTooltipSelectionChange = (options: any[]) => { + const tooltipSelection: string[] = []; + for (const option of options) { + tooltipSelection.push(option.label); + } + const source = { ...selectedLayerConfig.source, tooltipFields: tooltipSelection }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const onFiltersUpdated = useCallback( + (filters: Filter[]) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + source: { ...selectedLayerConfig.source, filters }, + }); + }, + [selectedLayerConfig] + ); + + useEffect(() => { + const selectIndexPattern = async () => { + if (selectedLayerConfig.source.indexPatternId) { + const selectedIndexPattern = await indexPatterns.get( + selectedLayerConfig.source.indexPatternId + ); + setIndexPattern(selectedIndexPattern); + } + }; + selectIndexPattern(); + }, [indexPatterns, selectedLayerConfig.source.indexPatternId]); + + // Update the fields list every time the index pattern is modified. + useEffect(() => { + const acceptedFieldTypes = ['geo_point', 'geo_shape']; + const fields = indexPattern?.fields.filter( + (field) => acceptedFieldTypes.indexOf(field.type) !== -1 + ); + setGeoFields(fields); + fields?.filter((field) => field.displayName === selectedLayerConfig.source.geoFieldName); + const savedField = fields?.find( + (field) => field.name === selectedLayerConfig.source.geoFieldName + ); + setSelectedField(savedField); + }, [indexPattern]); + + useEffect(() => { + const setLayerSource = () => { + if (!indexPattern || !selectedField) return; + const source = { + ...selectedLayerConfig.source, + indexPatternRefName: indexPattern?.title, + indexPatternId: indexPattern?.id, + geoFieldName: selectedField?.displayName, + geoFieldType: selectedField?.type, + }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + setLayerSource(); + }, [selectedField]); + + useEffect(() => { + setHasInvalidRequestNumber( + selectedLayerConfig.source.documentRequestNumber < 1 || + selectedLayerConfig.source.documentRequestNumber > 10000 + ); + }, [selectedLayerConfig.source.documentRequestNumber]); + + const onShowTooltipsChange = (event: { target: { checked: React.SetStateAction } }) => { + setShowTooltips(event.target.checked); + const source = { ...selectedLayerConfig.source, showTooltips: event.target.checked }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const onToggleGeoBoundingBox = (e: React.ChangeEvent) => { + const source = { ...selectedLayerConfig.source, useGeoBoundingBoxFilter: e.target.checked }; + setSelectedLayerConfig({ ...selectedLayerConfig, source }); + }; + + const shouldTooltipSectionOpen = () => { + return ( + selectedLayerConfig.source.showTooltips && + selectedLayerConfig.source.tooltipFields?.length > 0 + ); + }; + + const filterPanelInitialIsOpen = + selectedLayerConfig.source.filters?.length > 0 || + selectedLayerConfig.source.useGeoBoundingBoxFilter; + + return ( +
+ + + + + + + { + const newIndexPattern = await indexPatterns.get(newIndexPatternId); + setIndexPattern(newIndexPattern); + }} + isClearable={false} + data-test-subj={'indexPatternSelect'} + /> + + + + + { + const field = indexPattern?.getFieldByName(option[0].label); + setSelectedField(field || null); + }} + sortMatchesBy="startsWith" + placeholder={i18n.translate('documentLayer.selectDataFieldPlaceholder', { + defaultMessage: 'Select data field', + })} + data-test-subj={'geoFieldSelect'} + /> + + + + Number of documents + + + {hasInvalidRequestNumber && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + Tooltip fields + + + + + + +
+ ); +}; diff --git a/public/components/layer_config/documents_config/document_layer_style.tsx b/public/components/layer_config/documents_config/document_layer_style.tsx new file mode 100644 index 00000000..53882f76 --- /dev/null +++ b/public/components/layer_config/documents_config/document_layer_style.tsx @@ -0,0 +1,237 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiColorPicker, + useColorPickerState, + EuiFieldNumber, + EuiFormLabel, + EuiFormErrorText, + EuiFlexItem, + EuiSpacer, + EuiButtonGroup, + EuiPanel, + EuiTitle, + EuiFormRow, + EuiForm, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { DocumentLayerSpecification } from '../../../model/mapLayerType'; + +interface Props { + selectedLayerConfig: DocumentLayerSpecification; + setSelectedLayerConfig: Function; +} + +export const DocumentLayerStyle = ({ setSelectedLayerConfig, selectedLayerConfig }: Props) => { + const [fillColor, setFillColor] = useState(selectedLayerConfig?.style?.fillColor); + const [borderColor, setBorderColor] = useState(selectedLayerConfig?.style?.borderColor); + const [hasInvalidThickness, setHasInvalidThickness] = useState(false); + const [hasInvalidSize, setHasInvalidSize] = useState(false); + const geoTypeToggleButtonGroupPrefix = 'geoTypeToggleButtonGroup'; + const [toggleGeoTypeIdSelected, setToggleGeoTypeIdSelected] = useState( + `${geoTypeToggleButtonGroupPrefix}__Point` + ); + + useEffect(() => { + setFillColor(selectedLayerConfig?.style?.fillColor); + setBorderColor(selectedLayerConfig?.style?.borderColor); + }, [selectedLayerConfig]); + + useEffect(() => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + fillColor, + }, + }); + }, [fillColor]); + + useEffect(() => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + borderColor, + }, + }); + }, [borderColor]); + + const onBorderThicknessChange = (e: any) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + borderThickness: Number(e.target.value), + }, + }); + }; + + const onMarkerSizeChange = (e: any) => { + setSelectedLayerConfig({ + ...selectedLayerConfig, + style: { + ...selectedLayerConfig?.style, + markerSize: Number(e.target.value), + }, + }); + }; + + useEffect(() => { + if ( + selectedLayerConfig?.style?.borderThickness < 0 || + selectedLayerConfig?.style?.borderThickness > 100 + ) { + setHasInvalidThickness(true); + } else { + setHasInvalidThickness(false); + } + }, [selectedLayerConfig?.style?.borderThickness]); + + useEffect(() => { + if ( + selectedLayerConfig?.style?.markerSize < 0 || + selectedLayerConfig?.style?.markerSize > 100 + ) { + setHasInvalidSize(true); + } else { + setHasInvalidSize(false); + } + }, [selectedLayerConfig?.style?.markerSize]); + + const toggleButtonsGeoType = [ + { + id: `${geoTypeToggleButtonGroupPrefix}__Point`, + label: 'Points', + }, + { + id: `${geoTypeToggleButtonGroupPrefix}__Line`, + label: 'Lines', + }, + { + id: `${geoTypeToggleButtonGroupPrefix}__Polygon`, + label: 'Polygons', + }, + ]; + + const onChangeGeoTypeSelected = (optionId: string) => { + setToggleGeoTypeIdSelected(optionId); + }; + + interface ColorPickerProps { + color: string; + setColor: Function; + label: string; + } + + const ColorPicker = ({ color, setColor, label }: ColorPickerProps) => { + return ( + + {}} + fullWidth={true} + /> + + ); + }; + + interface WidthSelectorProps { + size: number; + onWidthChange: Function; + label: string; + hasInvalid: boolean; + } + + const WidthSelector = ({ label, onWidthChange, size, hasInvalid }: WidthSelectorProps) => { + return ( + + + px} + fullWidth={true} + /> + {hasInvalid && ( + + + + )} + + + ); + }; + + return ( + + +

Layer style

+
+ + onChangeGeoTypeSelected(id)} + buttonSize="compressed" + /> + + + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Point` && ( + + + + + + + )} + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Line` && ( + + + + + )} + {toggleGeoTypeIdSelected === `${geoTypeToggleButtonGroupPrefix}__Polygon` && ( + + + + + + )} + +
+ ); +}; diff --git a/public/components/layer_config/index.ts b/public/components/layer_config/index.ts new file mode 100644 index 00000000..4d043e76 --- /dev/null +++ b/public/components/layer_config/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { LayerConfigPanel } from './layer_config_panel'; +export { BaseMapLayerConfigPanel } from './base_map_layer_config_panel'; diff --git a/public/components/layer_config/layer_basic_settings.tsx b/public/components/layer_config/layer_basic_settings.tsx new file mode 100644 index 00000000..67fbfb56 --- /dev/null +++ b/public/components/layer_config/layer_basic_settings.tsx @@ -0,0 +1,153 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiDualRange, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiRange, + EuiPanel, + EuiFormLabel, + EuiTextArea, +} from '@elastic/eui'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { + MAP_DEFAULT_MIN_ZOOM, + MAP_DEFAULT_MAX_ZOOM, + MAP_LAYER_DEFAULT_MIN_OPACITY, + MAP_LAYER_DEFAULT_MAX_OPACITY, + MAP_LAYER_DEFAULT_OPACITY_STEP, + MAX_LAYER_NAME_LIMIT, +} from '../../../common'; +import { layersTypeNameMap } from '../../model/layersFunctions'; + +interface Props { + selectedLayerConfig: MapLayerSpecification; + setSelectedLayerConfig: Function; + setIsUpdateDisabled: Function; + isLayerExists: Function; +} + +export const LayerBasicSettings = ({ + selectedLayerConfig, + setSelectedLayerConfig, + setIsUpdateDisabled, + isLayerExists, +}: Props) => { + const [invalid, setInvalid] = useState(selectedLayerConfig.name.length === 0); + const [errors, setErrors] = useState([]); + + const validateName = (name: string) => { + if (name?.length === 0) { + setInvalid(true); + setErrors(['Name cannot be empty']); + return; + } + if (MAX_LAYER_NAME_LIMIT < name?.length) { + setInvalid(true); + setErrors(['Name should be less than ' + MAX_LAYER_NAME_LIMIT + ' characters']); + return; + } + if (isLayerExists(name)) { + setInvalid(true); + setErrors(['Name already exists']); + return; + } + setInvalid(false); + return; + }; + + const { name } = selectedLayerConfig; + + useEffect(() => { + const disableUpdate = !name || invalid; + setIsUpdateDisabled(disableUpdate); + }, [setIsUpdateDisabled, name, invalid]); + + const commonUpdate = (key: string, value: any) => { + const newLayerConfig = { ...selectedLayerConfig, [key]: value }; + setSelectedLayerConfig(newLayerConfig); + }; + const onZoomChange = (value: number[]) => { + commonUpdate('zoomRange', value); + }; + + const onOpacityChange = (e: any) => { + commonUpdate('opacity', Number(e.target.value)); + }; + + const onNameChange = (e: any) => { + const layerName = String(e.target.value); + validateName(layerName); + commonUpdate('name', layerName); + }; + + const onDescriptionChange = (e: any) => { + commonUpdate('description', String(e.target.value)); + }; + + return ( + + +

Layer settings

+
+ + + + + + + + + + + + + + + + + + %} + /> + + +
+ ); +}; diff --git a/public/components/layer_config/layer_config_panel.tsx b/public/components/layer_config/layer_config_panel.tsx new file mode 100644 index 00000000..400795dd --- /dev/null +++ b/public/components/layer_config/layer_config_panel.tsx @@ -0,0 +1,184 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { cloneDeep, isEqual } from 'lodash'; + +import { + EuiButton, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlexItem, + EuiButtonEmpty, + EuiFlexGroup, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiIcon, +} from '@elastic/eui'; + +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { BaseMapLayerConfigPanel } from './index'; +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; +import { DocumentLayerConfigPanel } from './documents_config/document_layer_config_panel'; +import { layersTypeIconMap } from '../../model/layersFunctions'; +import { CustomMapConfigPanel } from './custom_map_config/custom_map_config_panel'; + +interface Props { + closeLayerConfigPanel: Function; + selectedLayerConfig: MapLayerSpecification; + setSelectedLayerConfig: Function; + updateLayer: Function; + removeLayer: Function; + isNewLayer: boolean; + setIsNewLayer: Function; + isLayerExists: Function; + originLayerConfig: MapLayerSpecification | null; + setOriginLayerConfig: Function; +} + +export const LayerConfigPanel = ({ + closeLayerConfigPanel, + selectedLayerConfig, + setSelectedLayerConfig, + updateLayer, + removeLayer, + isNewLayer, + setIsNewLayer, + isLayerExists, + originLayerConfig, + setOriginLayerConfig, +}: Props) => { + const [isUpdateDisabled, setIsUpdateDisabled] = useState(false); + const [unsavedModalVisible, setUnsavedModalVisible] = useState(false); + + useEffect(() => { + if (originLayerConfig === null || originLayerConfig.id !== selectedLayerConfig.id) { + setOriginLayerConfig(cloneDeep(selectedLayerConfig)); + } + }, [originLayerConfig, selectedLayerConfig]); + + const discardChanges = () => { + closeLayerConfigPanel(false); + setSelectedLayerConfig(undefined); + setOriginLayerConfig(null); + setUnsavedModalVisible(false); + }; + + const onClose = () => { + if (isEqual(originLayerConfig, selectedLayerConfig)) { + discardChanges(); + } else { + setUnsavedModalVisible(true); + } + if (isNewLayer) { + removeLayer(selectedLayerConfig.id); + setIsNewLayer(false); + } + }; + const onUpdate = () => { + updateLayer(); + closeLayerConfigPanel(false); + setOriginLayerConfig(null); + if (isNewLayer) { + setIsNewLayer(false); + } + }; + + const closeModal = () => { + setUnsavedModalVisible(false); + }; + + return ( + + + + + + + + {selectedLayerConfig.name} + + + + + + + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP && ( + + )} + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS && ( + + )} + {selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP && ( + + )} + + + + + + + + Discard + + + + + Update + + + + + {unsavedModalVisible && ( + + + Unsaved changes + + +

Do you want to discard the changes?

+
+ + Cancel + + Discard + + +
+ )} +
+ ); +}; diff --git a/public/components/layer_control_panel/index.ts b/public/components/layer_control_panel/index.ts new file mode 100644 index 00000000..30cd7379 --- /dev/null +++ b/public/components/layer_control_panel/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { LayerControlPanel } from './layer_control_panel'; diff --git a/public/components/layer_control_panel/layer_control_panel.scss b/public/components/layer_control_panel/layer_control_panel.scss new file mode 100644 index 00000000..27d19370 --- /dev/null +++ b/public/components/layer_control_panel/layer_control_panel.scss @@ -0,0 +1,36 @@ +.layerControlPanel--show { + pointer-events: auto; + width: $euiSizeL * 11; + + .layerControlPanel__title { + padding: $euiSizeM $euiSizeM + } + + .layerControlPanel__selected { + background-color: $euiColorLightShade; + } + + .layerControlPanel__layerFunctionButton { + height: $euiSizeL; + width: $euiSizeL; + } + + .layerControlPanel__layerTypeIcon { + padding-left: $euiSizeM; + } + + .euiListGroupItem__label { + width: $euiSizeL * 6; + } +} + +.layerControlPanel--hide { + pointer-events: auto; + + .layerControlPanel__visButton { + background-color: $euiColorEmptyShade; + color: $euiTextColor; + border-color: $euiColorLightShade; + } +} + diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx new file mode 100644 index 00000000..d436439e --- /dev/null +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -0,0 +1,524 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo, useEffect, useState } from 'react'; +import { + DropResult, + EuiButtonEmpty, + EuiButtonIcon, + EuiConfirmModal, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiListGroupItem, + EuiPanel, + EuiTitle, + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; +import { I18nProvider } from '@osd/i18n/react'; +import { Map as Maplibre } from 'maplibre-gl'; +import './layer_control_panel.scss'; +import { isEqual } from 'lodash'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { AddLayerPanel } from '../add_layer_panel'; +import { LayerConfigPanel } from '../layer_config'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { + LAYER_ICON_TYPE_MAP, + LAYER_PANEL_HIDE_LAYER_ICON, + LAYER_PANEL_SHOW_LAYER_ICON, + LAYER_VISIBILITY, +} from '../../../common'; +import { + LayerActions, + layersFunctionMap, + referenceLayerTypeLookup, +} from '../../model/layersFunctions'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; +import { + handleDataLayerRender, + handleReferenceLayerRender, +} from '../../model/layerRenderController'; +import { MapState } from '../../model/mapState'; + +interface MaplibreRef { + current: Maplibre | null; +} + +interface Props { + maplibreRef: MaplibreRef; + setLayers: (layers: MapLayerSpecification[]) => void; + layers: MapLayerSpecification[]; + layersIndexPatterns: IndexPattern[]; + setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; + mapState: MapState; + zoom: number; +} + +export const LayerControlPanel = memo( + ({ + maplibreRef, + setLayers, + layers, + layersIndexPatterns, + setLayersIndexPatterns, + mapState, + zoom, + }: Props) => { + const { services } = useOpenSearchDashboards(); + const { + data: { indexPatterns }, + notifications, + } = services; + + const [isLayerConfigVisible, setIsLayerConfigVisible] = useState(false); + const [isLayerControlVisible, setIsLayerControlVisible] = useState(true); + const [selectedLayerConfig, setSelectedLayerConfig] = useState< + MapLayerSpecification | undefined + >(); + const [initialLayersLoaded, setInitialLayersLoaded] = useState(false); + const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(false); + const [isNewLayer, setIsNewLayer] = useState(false); + const [isDeleteLayerModalVisible, setIsDeleteLayerModalVisible] = useState(false); + const [originLayerConfig, setOriginLayerConfig] = useState(null); + const [selectedDeleteLayer, setSelectedDeleteLayer] = useState< + MapLayerSpecification | undefined + >(); + const [visibleLayers, setVisibleLayers] = useState([]); + + useEffect(() => { + if (!isUpdatingLayerRender && initialLayersLoaded) { + return; + } + if (layers.length <= 0) { + return; + } + + if (initialLayersLoaded) { + if (!selectedLayerConfig) { + return; + } + if (referenceLayerTypeLookup[selectedLayerConfig.type]) { + handleReferenceLayerRender(selectedLayerConfig, maplibreRef, undefined); + } else { + updateIndexPatterns(); + handleDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef, undefined); + } + setSelectedLayerConfig(undefined); + } else { + layers.forEach((layer) => { + const beforeLayerId = getMapBeforeLayerId(layer); + if (referenceLayerTypeLookup[layer.type]) { + handleReferenceLayerRender(layer, maplibreRef, beforeLayerId); + } else { + handleDataLayerRender(layer, mapState, services, maplibreRef, beforeLayerId); + } + }); + setInitialLayersLoaded(true); + } + setIsUpdatingLayerRender(false); + }, [layers]); + + useEffect(() => { + const getCurrentVisibleLayers = () => { + return layers.filter( + (layer: { visibility: string; zoomRange: number[] }) => + zoom >= layer.zoomRange[0] && zoom <= layer.zoomRange[1] + ); + }; + setVisibleLayers(getCurrentVisibleLayers()); + }, [layers, zoom]); + + // Get layer id from layers that is above the selected layer + function getMapBeforeLayerId(selectedLayer: MapLayerSpecification): string | undefined { + const selectedLayerIndex = layers.findIndex((layer) => layer.id === selectedLayer.id); + const beforeLayers = layers.slice(selectedLayerIndex + 1); + if (beforeLayers.length === 0) { + return undefined; + } + return beforeLayers[0]?.id; + } + + const closeLayerConfigPanel = () => { + setIsLayerConfigVisible(false); + setTimeout(() => { + maplibreRef.current?.resize(); + }, 0); + }; + + const newLayerIndex = () => { + return layers?.length + 1; + }; + + const addLayer = (layer: MapLayerSpecification) => { + setLayers([...layers, layer]); + }; + + const updateLayer = () => { + if (!selectedLayerConfig) { + return; + } + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === selectedLayerConfig.id); + if (index <= -1) { + layersClone.push(selectedLayerConfig); + } else { + layersClone[index] = { + ...layersClone[index], + ...selectedLayerConfig, + }; + } + setLayers(layersClone); + setIsUpdatingLayerRender(true); + }; + + const removeLayer = (layerId: string) => { + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === layerId); + if (index > -1) { + layersClone.splice(index, 1); + setLayers(layersClone); + } + }; + + const hasUnsavedChanges = () => { + if (!selectedLayerConfig || !originLayerConfig) { + return false; + } + return !isEqual(originLayerConfig, selectedLayerConfig); + }; + + const onClickLayerName = (layer: MapLayerSpecification) => { + if (hasUnsavedChanges()) { + notifications.toasts.addWarning( + `You have unsaved changes for ${selectedLayerConfig?.name}` + ); + } else { + setSelectedLayerConfig(layer); + setIsLayerConfigVisible(true); + } + }; + + const isLayerExists = (name: string) => { + return layers.findIndex((layer) => layer.name === name) > -1; + }; + + const [layerVisibility, setLayerVisibility] = useState(new Map([])); + layers.forEach((layer) => { + layerVisibility.set(layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE); + }); + + const beforeMaplibreLayerID = (source: number, destination: number) => { + if (source > destination) { + // if layer is moved below, move current layer below given destination + return layers[destination].id; + } + const beforeIndex = destination + 1; // if layer is moved up, move current layer above destination + if (beforeIndex < layers.length) { + return layers[beforeIndex].id; + } + return undefined; + }; + + const onDragEnd = (dropResult: DropResult) => { + if (!dropResult) { + return; + } + if (dropResult.source && dropResult.destination) { + // we display list in reverse order + const prevIndex = getLayerIndex(dropResult.source.index); + const newIndex = getLayerIndex(dropResult.destination.index); + + const currentMaplibreLayerId = layers[prevIndex].id; + const beforeMaplibreLayerId = beforeMaplibreLayerID(prevIndex, newIndex); + LayerActions.move(maplibreRef, currentMaplibreLayerId, beforeMaplibreLayerId); + + // update map layers + const layersClone = [...layers]; + const oldLayer = layersClone[prevIndex]; + layersClone.splice(prevIndex, 1); + layersClone.splice(newIndex, 0, oldLayer); + setLayers(layersClone); + } + }; + + const getLayerIndex = (reversedIndex: number) => { + return layers.length - reversedIndex - 1; + }; + + const getReverseLayers = () => { + const layersClone = [...layers]; + return layersClone.reverse(); + }; + + const updateIndexPatterns = async () => { + if (!selectedLayerConfig) { + return; + } + if (referenceLayerTypeLookup[selectedLayerConfig.type]) { + return; + } + const findIndexPattern = layersIndexPatterns.find( + // @ts-ignore + (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId + ); + if (!findIndexPattern) { + // @ts-ignore + const newIndexPattern = await indexPatterns.get(selectedLayerConfig.source.indexPatternId); + const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; + setLayersIndexPatterns(cloneLayersIndexPatterns); + } + }; + + const onLayerVisibilityChange = (layer: MapLayerSpecification) => { + if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { + layer.visibility = LAYER_VISIBILITY.NONE; + setLayerVisibility(new Map(layerVisibility.set(layer.id, false))); + } else { + layer.visibility = LAYER_VISIBILITY.VISIBLE; + setLayerVisibility(new Map(layerVisibility.set(layer.id, true))); + } + layersFunctionMap[layer.type]?.hide(maplibreRef, layer); + }; + + const onDeleteLayerIconClick = (layer: MapLayerSpecification) => { + setSelectedDeleteLayer(layer); + setIsDeleteLayerModalVisible(true); + }; + + const onDeleteLayerConfirm = () => { + if (selectedDeleteLayer) { + layersFunctionMap[selectedDeleteLayer.type]?.remove(maplibreRef, selectedDeleteLayer); + removeLayer(selectedDeleteLayer.id); + setIsDeleteLayerModalVisible(false); + setSelectedDeleteLayer(undefined); + } + }; + + const onCancelDeleteLayer = () => { + setIsDeleteLayerModalVisible(false); + setSelectedDeleteLayer(undefined); + }; + + let deleteLayerModal; + if (isDeleteLayerModalVisible) { + deleteLayerModal = ( + +

+ Do you want to delete layer {selectedDeleteLayer?.name}? +

+
+ ); + } + + const getLayerTooltipContent = (layer: MapLayerSpecification) => { + if (zoom < layer.zoomRange[0] || zoom > layer.zoomRange[1]) { + return `Layer is not visible outside of zoom range ${layer.zoomRange[0]} - ${layer.zoomRange[1]}`; + } else { + return `Layer is visible within zoom range ${layer.zoomRange[0]} - ${layer.zoomRange[1]}`; + } + }; + + const layerIsVisible = (layer: MapLayerSpecification) => { + return visibleLayers.includes(layer); + }; + + if (isLayerControlVisible) { + return ( + + + + + + +

Layers

+
+
+ + setIsLayerControlVisible((visible) => !visible)} + aria-label="Hide layer control" + color="text" + className="layerControlPanel__visButton" + title="Collapse layers panel" + /> + +
+ + + + {getReverseLayers().map((layer, index) => { + const isLayerSelected = + isLayerConfigVisible && + selectedLayerConfig && + selectedLayerConfig.id === layer.id; + return ( + + {(provided) => ( +
+ + + + + + + onClickLayerName(layer)} + showToolTip={false} + /> + + + + + onLayerVisibilityChange(layer)} + aria-label="Hide or show layer" + color="text" + title={ + layerVisibility.get(layer.id) ? 'Hide layer' : 'Show layer' + } + /> + + + onDeleteLayerIconClick(layer)} + aria-label="Delete layer" + color={layer.id === selectedLayerConfig?.id ? 'text' : 'danger'} + title="Delete layer" + disabled={layer.id === selectedLayerConfig?.id} + /> + + + + + + + +
+ )} +
+ ); + })} +
+
+ {isLayerConfigVisible && selectedLayerConfig && ( + + )} + + {deleteLayerModal} +
+
+
+ ); + } + + return ( + + setIsLayerControlVisible((visible) => !visible)} + aria-label="Show layer control" + title="Expand layers panel" + /> + + ); + } +); diff --git a/public/components/map_container/index.ts b/public/components/map_container/index.ts new file mode 100644 index 00000000..b0716344 --- /dev/null +++ b/public/components/map_container/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapContainer } from './map_container'; diff --git a/public/components/map_container/map_container.scss b/public/components/map_container/map_container.scss new file mode 100644 index 00000000..7453ca94 --- /dev/null +++ b/public/components/map_container/map_container.scss @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +@import "maplibre-gl/dist/maplibre-gl.css"; +@import "../../variables"; + +/* stylelint-disable no-empty-source */ +.map-container { + width: 100%; + min-height: calc(100vh - #{$mapHeaderOffset}); +} + +.maplibregl-ctrl-top-left { + left: $euiSizeS; + top: $euiSizeS; +} + +.layerControlPanel-container { + z-index: 1; + position: absolute; + margin-left: $euiSizeS; + margin-top: $euiSizeS; +} + +.zoombar { + z-index: 1; + position: absolute; + bottom: $euiSizeM; + right: $euiSizeS; +} diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx new file mode 100644 index 00000000..4e3c38a6 --- /dev/null +++ b/public/components/map_container/map_container.tsx @@ -0,0 +1,189 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { LngLat, Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; +import { debounce } from 'lodash'; +import { LayerControlPanel } from '../layer_control_panel'; +import './map_container.scss'; +import { MAP_INITIAL_STATE, MAP_GLYPHS, DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { MapState } from '../../model/mapState'; +import { createPopup, getPopupLngLat, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; +import { handleDataLayerRender } from '../../model/layerRenderController'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; + +interface MapContainerProps { + setLayers: (layers: MapLayerSpecification[]) => void; + layers: MapLayerSpecification[]; + layersIndexPatterns: IndexPattern[]; + setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; + maplibreRef: React.MutableRefObject; + mapState: MapState; +} + +export const MapContainer = ({ + setLayers, + layers, + layersIndexPatterns, + setLayersIndexPatterns, + maplibreRef, + mapState, +}: MapContainerProps) => { + const { services } = useOpenSearchDashboards(); + const mapContainer = useRef(null); + const [mounted, setMounted] = useState(false); + const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); + const [coordinates, setCoordinates] = useState(); + + useEffect(() => { + if (!mapContainer.current) return; + const mbStyle = { + version: 8 as 8, + sources: {}, + layers: [], + glyphs: MAP_GLYPHS, + }; + + maplibreRef.current = new Maplibre({ + container: mapContainer.current!, + center: [MAP_INITIAL_STATE.lng, MAP_INITIAL_STATE.lat], + zoom, + style: mbStyle, + }); + + const maplibreInstance = maplibreRef.current!; + maplibreInstance.addControl(new NavigationControl({ showCompass: true }), 'top-right'); + maplibreInstance.on('style.load', function () { + setMounted(true); + }); + maplibreInstance.on('move', () => { + return setZoom(Number(maplibreInstance.getZoom().toFixed(2))); + }); + }, []); + + // Create onClick tooltip for each layer features that has tooltip enabled + useEffect(() => { + let clickPopup: Popup | null = null; + let hoverPopup: Popup | null = null; + + // We don't want to show layer information in the popup for the map tile layer + const tooltipEnabledLayers = layers.filter(isTooltipEnabledLayer); + + function onClickMap(e: MapEventType['click']) { + // remove previous popup + clickPopup?.remove(); + + const features = maplibreRef.current?.queryRenderedFeatures(e.point); + if (features && maplibreRef.current) { + clickPopup = createPopup({ features, layers: tooltipEnabledLayers }); + clickPopup + ?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat) + .addTo(maplibreRef.current); + } + } + + function onMouseMoveMap(e: MapEventType['mousemove']) { + setCoordinates(e.lngLat.wrap()); + + // remove previous popup + hoverPopup?.remove(); + + const features = maplibreRef.current?.queryRenderedFeatures(e.point); + if (features && maplibreRef.current) { + hoverPopup = createPopup({ + features, + layers: tooltipEnabledLayers, + showCloseButton: false, + showPagination: false, + showLayerSelection: false, + }); + hoverPopup + ?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat) + .addTo(maplibreRef.current); + } + } + + if (maplibreRef.current) { + const map = maplibreRef.current; + map.on('click', onClickMap); + // reset cursor to default when user is no longer hovering over a clickable feature + map.on('mouseleave', () => { + map.getCanvas().style.cursor = ''; + hoverPopup?.remove(); + }); + map.on('mouseenter', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + // add tooltip when users mouse move over a point + map.on('mousemove', onMouseMoveMap); + } + + return () => { + if (maplibreRef.current) { + maplibreRef.current.off('click', onClickMap); + maplibreRef.current.off('mousemove', onMouseMoveMap); + } + }; + }, [layers]); + + // Handle map bounding box change, it should update the search if "request data around map extent" was enabled + useEffect(() => { + function renderLayers() { + layers.forEach((layer: MapLayerSpecification) => { + // We don't send search query if the layer doesn't have "request data around map extent" enabled + if ( + layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS && + layer.source.useGeoBoundingBoxFilter + ) { + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); + } + }); + } + + // Rerender layers with 200ms debounce to avoid calling the search API too frequently, especially when + // resizing the window, the "moveend" event could be fired constantly + const debouncedRenderLayers = debounce(renderLayers, 200); + + if (maplibreRef.current) { + maplibreRef.current.on('moveend', debouncedRenderLayers); + } + + return () => { + if (maplibreRef.current) { + maplibreRef.current.off('moveend', debouncedRenderLayers); + } + }; + }, [layers, mapState, services]); + + return ( +
+ + + {coordinates && + `lat: ${coordinates.lat.toFixed(4)}, lon: ${coordinates.lng.toFixed(4)}, `} + zoom: {zoom} + + +
+ {mounted && ( + + )} +
+
+
+ ); +}; diff --git a/public/components/map_page/index.ts b/public/components/map_page/index.ts new file mode 100644 index 00000000..a79e0689 --- /dev/null +++ b/public/components/map_page/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapPage } from './map_page'; diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx new file mode 100644 index 00000000..d8dc8d3d --- /dev/null +++ b/public/components/map_page/map_page.tsx @@ -0,0 +1,86 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import { Map as Maplibre } from 'maplibre-gl'; +import { MapContainer } from '../map_container'; +import { MapTopNavMenu } from '../map_top_nav'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { MapServices } from '../../types'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { + DASHBOARDS_MAPS_LAYER_TYPE, + MAP_LAYER_DEFAULT_NAME, + OPENSEARCH_MAP_LAYER, +} from '../../../common'; +import { getLayerConfigMap, getInitialMapState } from '../../utils/getIntialConfig'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { MapState } from '../../model/mapState'; + +export const MapPage = () => { + const { services } = useOpenSearchDashboards(); + const { + savedObjects: { client: savedObjectsClient }, + } = services; + const [layers, setLayers] = useState([]); + const { id: mapIdFromUrl } = useParams<{ id: string }>(); + const [savedMapObject, setSavedMapObject] = + useState | null>(); + const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); + const maplibreRef = useRef(null); + const [mapState, setMapState] = useState(getInitialMapState()); + + useEffect(() => { + if (mapIdFromUrl) { + savedObjectsClient.get('map', mapIdFromUrl).then((res) => { + setSavedMapObject(res); + const layerList: MapLayerSpecification[] = JSON.parse(res.attributes.layerList as string); + const savedMapState: MapState = JSON.parse(res.attributes.mapState as string); + setMapState(savedMapState); + setLayers(layerList); + const savedIndexPatterns: IndexPattern[] = []; + layerList.forEach(async (layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const indexPatternId = layer.source.indexPatternId; + const indexPattern = await services.data.indexPatterns.get(indexPatternId); + savedIndexPatterns.push(indexPattern); + } + }); + setLayersIndexPatterns(savedIndexPatterns); + }); + } else { + const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap()[ + OPENSEARCH_MAP_LAYER.type + ]; + initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; + setLayers([initialDefaultLayer]); + } + }, []); + + return ( +
+ + +
+ ); +}; diff --git a/public/components/map_top_nav/get_top_nav_config.tsx b/public/components/map_top_nav/get_top_nav_config.tsx new file mode 100644 index 00000000..4269c356 --- /dev/null +++ b/public/components/map_top_nav/get_top_nav_config.tsx @@ -0,0 +1,133 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; +import { + OnSaveProps, + SavedObjectSaveModalOrigin, + showSaveModal, + checkForDuplicateTitle, +} from '../../../../../src/plugins/saved_objects/public'; +import { MapServices } from '../../types'; +import { MapState } from '../../model/mapState'; + +const SAVED_OBJECT_TYPE = 'map'; + +interface GetTopNavConfigParams { + mapIdFromUrl: string; + layers: any; + title: string; + description: string; + setTitle: (title: string) => void; + setDescription: (description: string) => void; + mapState: MapState; +} + +export const getTopNavConfig = ( + { + notifications: { toasts }, + i18n: { Context: I18nContext }, + savedObjects: { client: savedObjectsClient }, + history, + overlays, + }: MapServices, + { + mapIdFromUrl, + layers, + title, + description, + setTitle, + setDescription, + mapState, + }: GetTopNavConfigParams +) => { + const topNavConfig: TopNavMenuData[] = [ + { + iconType: 'save', + emphasize: true, + id: 'save', + label: i18n.translate('maps.topNav.saveMapButtonLabel', { + defaultMessage: `Save`, + }), + run: (_anchorElement) => { + const onModalSave = async ({ newTitle, newDescription, onTitleDuplicate }: OnSaveProps) => { + let newlySavedMap; + const saveAttributes = { + title: newTitle, + description: newDescription, + layerList: JSON.stringify(layers), + mapState: JSON.stringify(mapState), + }; + try { + await checkForDuplicateTitle( + { + title: newTitle, + lastSavedTitle: title, + copyOnSave: false, + getDisplayName: () => SAVED_OBJECT_TYPE, + getOpenSearchType: () => SAVED_OBJECT_TYPE, + }, + false, + onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } catch (_error) { + return {}; + } + if (mapIdFromUrl) { + // edit existing map + newlySavedMap = await savedObjectsClient.update( + SAVED_OBJECT_TYPE, + mapIdFromUrl, + saveAttributes + ); + } else { + // save new map + newlySavedMap = await savedObjectsClient.create(SAVED_OBJECT_TYPE, saveAttributes); + } + const id = newlySavedMap.id; + if (id) { + history.push({ + ...history.location, + pathname: `${id}`, + }); + setTitle(newTitle); + setDescription(newDescription); + toasts.addSuccess({ + title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { + defaultMessage: `Saved ${newTitle}`, + values: { + visTitle: newTitle, + }, + }), + }); + } + return { id }; + }; + + const documentInfo = { + title, + description, + }; + + const saveModal = ( + {}} + /> + ); + showSaveModal(saveModal, I18nContext); + }, + }, + ]; + return topNavConfig; +}; diff --git a/public/components/map_top_nav/index.ts b/public/components/map_top_nav/index.ts new file mode 100644 index 00000000..c732aa00 --- /dev/null +++ b/public/components/map_top_nav/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapTopNavMenu } from './top_nav_menu'; diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx new file mode 100644 index 00000000..15b2fbe3 --- /dev/null +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -0,0 +1,139 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import { IndexPattern, Query, TimeRange } from '../../../../../src/plugins/data/public'; +import { DASHBOARDS_MAPS_LAYER_TYPE, PLUGIN_NAVIGATION_BAR_ID } from '../../../common'; +import { getTopNavConfig } from './get_top_nav_config'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { getSavedMapBreadcrumbs } from '../../utils/breadcrumbs'; +import { handleDataLayerRender } from '../../model/layerRenderController'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { MapState } from '../../model/mapState'; + +interface MapTopNavMenuProps { + mapIdFromUrl: string; + layers: MapLayerSpecification[]; + savedMapObject: SimpleSavedObject | null | undefined; + layersIndexPatterns: IndexPattern[]; + maplibreRef: any; + mapState: MapState; + setMapState: (mapState: MapState) => void; +} + +export const MapTopNavMenu = ({ + mapIdFromUrl, + savedMapObject, + layers, + layersIndexPatterns, + maplibreRef, + mapState, + setMapState, +}: MapTopNavMenuProps) => { + const { services } = useOpenSearchDashboards(); + const { + setHeaderActionMenu, + navigation: { + ui: { TopNavMenu }, + }, + chrome, + application: { navigateToApp }, + } = services; + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [queryConfig, setQueryConfig] = useState({ query: '', language: 'kuery' }); + const [refreshIntervalValue, setRefreshIntervalValue] = useState(60000); + const [isRefreshPaused, setIsRefreshPaused] = useState(false); + const changeTitle = useCallback( + (newTitle: string) => { + chrome.setBreadcrumbs(getSavedMapBreadcrumbs(newTitle, navigateToApp)); + chrome.docTitle.change(newTitle); + }, + [chrome, navigateToApp] + ); + + useEffect(() => { + if (savedMapObject) { + setTitle(savedMapObject.attributes.title); + setDescription(savedMapObject.attributes.description!); + } + }, [savedMapObject, mapIdFromUrl]); + + useEffect(() => { + changeTitle(title || 'Create'); + }, [title, changeTitle]); + + const refreshDataLayerRender = () => { + layers.forEach((layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + return; + } + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); + }); + }; + + const handleQuerySubmit = ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => { + if (query) { + setMapState({ ...mapState, query }); + } + if (dateRange) { + setMapState({ ...mapState, timeRange: dateRange }); + } + }; + + useEffect(() => { + setDateFrom(mapState.timeRange.from); + setDateTo(mapState.timeRange.to); + setQueryConfig(mapState.query); + setIsRefreshPaused(mapState.refreshInterval.pause); + setRefreshIntervalValue(mapState.refreshInterval.value); + refreshDataLayerRender(); + }, [mapState]); + + const onRefreshChange = useCallback( + ({ isPaused, refreshInterval }: { isPaused: boolean; refreshInterval: number }) => { + setIsRefreshPaused(isPaused); + setRefreshIntervalValue(refreshInterval); + }, + [] + ); + + return ( + + ); +}; diff --git a/public/components/maps_list/index.ts b/public/components/maps_list/index.ts new file mode 100644 index 00000000..11ecaf3e --- /dev/null +++ b/public/components/maps_list/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapsList } from './maps_list'; diff --git a/public/components/maps_list/maps_list.tsx b/public/components/maps_list/maps_list.tsx new file mode 100644 index 00000000..d03d7e7a --- /dev/null +++ b/public/components/maps_list/maps_list.tsx @@ -0,0 +1,151 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React, { useCallback, useEffect } from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiLink, + EuiButton, + EuiPageHeader, +} from '@elastic/eui'; +import { + TableListView, + useOpenSearchDashboards, +} from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { MapServices } from '../../types'; +import { getMapsLandingBreadcrumbs } from '../../utils/breadcrumbs'; +import { APP_PATH, PLUGIN_NAVIGATION_BAR_ID } from '../../../common'; + +export const MapsList = () => { + const { + services: { + notifications: { toasts }, + savedObjects: { client: savedObjectsClient }, + application: { navigateToApp }, + chrome: { docTitle, setBreadcrumbs }, + }, + } = useOpenSearchDashboards(); + + useEffect(() => { + setBreadcrumbs(getMapsLandingBreadcrumbs(navigateToApp)); + docTitle.change(i18n.translate('maps.listing.pageTitle', { defaultMessage: 'Maps' })); + }, [docTitle, navigateToApp, setBreadcrumbs]); + + const navigateToSavedMapPage = (id: string) => { + navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: `/${id}` }); + }; + + const tableColumns = [ + { + field: 'attributes.title', + name: i18n.translate('maps.listing.table.titleColumnName', { + defaultMessage: 'Title', + }), + sortable: true, + render: (title: string, record: any) => ( + navigateToSavedMapPage(record.id)}>{title} + ), + }, + { + field: 'attributes.description', + name: i18n.translate('maps.listing.table.descriptionColumnName', { + defaultMessage: 'Description', + }), + sortable: true, + }, + { + field: 'updated_at', + name: i18n.translate('maps.listing.table.updatedTimeColumnName', { + defaultMessage: 'Last updated', + }), + sortable: true, + }, + ]; + + const navigateToCreateMapPage = () => { + navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: APP_PATH.CREATE_MAP }); + }; + + const fetchMaps = useCallback(async (): Promise<{ + total: number; + hits: object[]; + }> => { + const res = await savedObjectsClient.find({ + type: 'map', + fields: ['description', 'title'], + }); + return { + total: res.total, + hits: res.savedObjects, + }; + }, [savedObjectsClient]); + + const deleteMaps = useCallback( + async (selectedItems: object[]) => { + await Promise.all( + selectedItems.map((item: any) => savedObjectsClient.delete(item.type, item.id)) + ).catch((error) => { + toasts.addError(error, { + title: i18n.translate('map.mapListingDeleteErrorTitle', { + defaultMessage: 'Error deleting map', + }), + }); + }); + }, + [savedObjectsClient, toasts] + ); + + const noMapItem = ( + + Create map + , + ]} + /> + ); + + // Render the map list DOM. + return ( + + <> + + + + + + + + + + ); +}; diff --git a/custom_import_map/public/components/show_error_modal.test.tsx b/public/components/show_error_modal.test.tsx similarity index 100% rename from custom_import_map/public/components/show_error_modal.test.tsx rename to public/components/show_error_modal.test.tsx diff --git a/custom_import_map/public/components/show_error_modal.tsx b/public/components/show_error_modal.tsx similarity index 100% rename from custom_import_map/public/components/show_error_modal.tsx rename to public/components/show_error_modal.tsx diff --git a/public/components/tooltip/create_tooltip.tsx b/public/components/tooltip/create_tooltip.tsx new file mode 100644 index 00000000..7d86fc66 --- /dev/null +++ b/public/components/tooltip/create_tooltip.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Popup, MapGeoJSONFeature } from 'maplibre-gl'; + +import { MapLayerSpecification, DocumentLayerSpecification } from '../../model/mapLayerType'; +import { FeatureGroupItem, TooltipContainer } from './tooltipContainer'; + +type Options = { + features: MapGeoJSONFeature[]; + layers: DocumentLayerSpecification[]; + showCloseButton?: boolean; + showPagination?: boolean; + showLayerSelection?: boolean; +}; + +export function isTooltipEnabledLayer( + layer: MapLayerSpecification +): layer is DocumentLayerSpecification { + return ( + layer.type !== 'opensearch_vector_tile_map' && + layer.type !== 'custom_map' && + layer.source.showTooltips === true + ); +} + +export function groupFeaturesByLayers( + features: MapGeoJSONFeature[], + layers: DocumentLayerSpecification[] +) { + const featureGroups: FeatureGroupItem[] = []; + if (layers.length > 0) { + layers.forEach((layer) => { + const layerFeatures = features.filter((f) => f.layer.source === layer.id); + if (layerFeatures.length > 0) { + featureGroups.push({ features: layerFeatures, layer }); + } + }); + } + return featureGroups; +} + +export function getPopupLngLat(geometry: GeoJSON.Geometry) { + // geometry.coordinates is different for different geometry.type, here we use the geometry.coordinates + // of a Point as the position of the popup. For other types, such as Polygon, MultiPolygon, etc, + // use mouse position should be better + if (geometry.type === 'Point') { + return [geometry.coordinates[0], geometry.coordinates[1]] as [number, number]; + } else { + return null; + } +} + +export function createPopup({ + features, + layers, + showCloseButton = true, + showPagination = true, + showLayerSelection = true, +}: Options) { + const popup = new Popup({ + closeButton: false, + closeOnClick: false, + maxWidth: 'max-content', + }); + + const featureGroup = groupFeaturesByLayers(features, layers); + + // Don't show popup if no feature + if (featureGroup.length === 0) { + return null; + } + + const div = document.createElement('div'); + ReactDOM.render( + , + div + ); + + return popup.setDOMContent(div); +} diff --git a/public/components/tooltip/tooltipContainer.tsx b/public/components/tooltip/tooltipContainer.tsx new file mode 100644 index 00000000..236546ba --- /dev/null +++ b/public/components/tooltip/tooltipContainer.tsx @@ -0,0 +1,96 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useState } from 'react'; + +import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiText, EuiHorizontalRule } from '@elastic/eui'; +import { MapGeoJSONFeature } from 'maplibre-gl'; +import { TooltipHeaderContent } from './tooltipHeaderContent'; +import { ALL_LAYERS, PageData, TableData, TooltipTable } from './tooltipTable'; +import { DocumentLayerSpecification } from '../../model/mapLayerType'; + +export type FeatureGroupItem = { + features: MapGeoJSONFeature[]; + layer: DocumentLayerSpecification; +}; + +interface TooltipProps { + featureGroup: FeatureGroupItem[]; + onClose: () => void; + showCloseButton?: boolean; + showPagination?: boolean; + showLayerSelection?: boolean; +} + +function featureToTableRow(properties: Record) { + const row: PageData = []; + for (const [k, v] of Object.entries(properties)) { + row.push({ + key: k, + value: `${v}`, + }); + } + return row; +} + +function toTable(featureGroupItem: FeatureGroupItem) { + const table: TableData = []; + for (const feature of featureGroupItem.features) { + if (feature?.properties) { + table.push(featureToTableRow(feature.properties)); + } + } + return { table, layer: featureGroupItem.layer.name }; +} + +function createTableData(featureGroups: FeatureGroupItem[]) { + return featureGroups.map(toTable); +} + +export function TooltipContainer({ + featureGroup, + onClose, + showCloseButton = true, + showPagination = true, + showLayerSelection = true, +}: TooltipProps) { + const [selectedLayerIndexes, setSelectedLayerIndexes] = useState([0]); + const tables = useMemo(() => createTableData(featureGroup), [featureGroup]); + + const title = useMemo(() => { + if (selectedLayerIndexes.includes(ALL_LAYERS)) { + return 'All layers'; + } + if (selectedLayerIndexes.length === 1) { + return tables[selectedLayerIndexes[0]].layer; + } + return ''; + }, [selectedLayerIndexes, tables]); + + return ( + + + + + + + + + + + + + + ); +} diff --git a/public/components/tooltip/tooltipHeaderContent.tsx b/public/components/tooltip/tooltipHeaderContent.tsx new file mode 100644 index 00000000..7373b38f --- /dev/null +++ b/public/components/tooltip/tooltipHeaderContent.tsx @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; + +interface Props { + title: string; + showCloseButton: boolean; + onClose: Function; +} + +const TooltipHeaderContent = (props: Props) => { + return ( + + + +

+ {props.title} +

+
+
+ {props.showCloseButton && ( + + { + return props.onClose(); + }} + iconType="cross" + aria-label={i18n.translate('maps.tooltip.closeLabel', { + defaultMessage: 'Close tooltip', + })} + data-test-subj="featureTooltipCloseButton" + /> + + )} +
+ ); +}; + +export { TooltipHeaderContent }; diff --git a/public/components/tooltip/tooltipTable.tsx b/public/components/tooltip/tooltipTable.tsx new file mode 100644 index 00000000..4ca7a571 --- /dev/null +++ b/public/components/tooltip/tooltipTable.tsx @@ -0,0 +1,200 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiBasicTable, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, + EuiPagination, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useState, Fragment, useCallback, useEffect, useMemo } from 'react'; + +export type RowData = { + key: string; + value: string; +}; +export type PageData = RowData[]; +export type TableData = PageData[]; +type Table = { table: TableData; layer: string }; + +export const ALL_LAYERS = -1; + +interface Props { + tables: Table[]; + onLayerChange?: (layerIndexes: number[]) => void; + showPagination?: boolean; + showLayerSelection?: boolean; +} + +function mergeTables(tables: Table[], selectedIndex: number[]) { + const merged: TableData = []; + const allSelected = selectedIndex.includes(ALL_LAYERS); + + if (!allSelected) { + for (const index of selectedIndex) { + merged.push(...tables[index].table); + } + return merged; + } + for (let i = 0; i < tables.length; i++) { + const features: PageData[] = []; + tables[i].table.map((feature) => { + // Add layer name to every feature as first field + features.push( + [ + { + key: 'Layer name', + value: tables[i].layer, + }, + ].concat(feature.slice(0)) + ); + }); + merged.push(...features); + } + return merged; +} + +const TooltipTable = ({ + tables, + onLayerChange, + showPagination = true, + showLayerSelection = true, +}: Props) => { + const [selectedLayers, setSelectedLayers] = useState[]>([ + { + label: tables[0]?.layer ?? '', + value: 0, + key: '0', + }, + ]); + const [activePage, setActivePage] = useState(0); + const columns = [ + { + field: 'key', + name: 'Field Name', + width: '25%', + truncateText: false, + }, + { + field: 'value', + name: 'Field Value', + width: '75%', + truncateText: true, + }, + ]; + + useEffect(() => { + // When selected layer changed, reset the active page to the first page + setActivePage(0); + }, [selectedLayers]); + + const getRowProps = (item) => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + className: 'customRowClass', + }; + }; + + const handleLayerChange = useCallback( + (layerSelections: EuiComboBoxOptionOption[]) => { + if (tables.length === 0) { + return; + } + + const selections = layerSelections; + + setSelectedLayers(selections); + if (onLayerChange) { + onLayerChange(selections.map((s) => s.value ?? 0)); + } + }, + [tables] + ); + + const options = useMemo(() => { + const layerOptions = []; + if (tables.length > 1) { + layerOptions.push({ label: 'All layers', value: ALL_LAYERS, key: '-1' }); + } + tables.forEach(({ layer }, i) => { + layerOptions.push({ label: layer, value: i, key: `${i}` }); + }); + return layerOptions; + }, [tables]); + + const tableItems = useMemo( + () => + mergeTables( + tables, + selectedLayers.map((l) => l.value ?? 0) + ), + [tables, selectedLayers] + ); + const pageItems = tableItems[activePage]; + + const getCellProps = (item, column) => { + const { id } = item; + const { field } = column; + return { + 'data-test-subj': `cell-${id}-${field}`, + className: 'customCellClass', + textOnly: true, + }; + }; + + return ( + + + + + + + + + {showLayerSelection && options?.length > 1 && ( + + + placeholder="Select a layer" + selectedOptions={selectedLayers} + singleSelection={{ asPlainText: true }} + options={options} + onChange={handleLayerChange} + isClearable={false} + /> + + )} + + {showPagination ? ( + + ) : ( + + {1} of {tableItems.length} + + )} + + + + ); +}; + +export { TooltipTable }; diff --git a/custom_import_map/public/components/vector_upload_options.scss b/public/components/vector_upload_options.scss similarity index 100% rename from custom_import_map/public/components/vector_upload_options.scss rename to public/components/vector_upload_options.scss diff --git a/custom_import_map/public/components/vector_upload_options.test.tsx b/public/components/vector_upload_options.test.tsx similarity index 100% rename from custom_import_map/public/components/vector_upload_options.test.tsx rename to public/components/vector_upload_options.test.tsx diff --git a/custom_import_map/public/components/vector_upload_options.tsx b/public/components/vector_upload_options.tsx similarity index 100% rename from custom_import_map/public/components/vector_upload_options.tsx rename to public/components/vector_upload_options.tsx diff --git a/public/index.scss b/public/index.scss new file mode 100644 index 00000000..a850c169 --- /dev/null +++ b/public/index.scss @@ -0,0 +1,4 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/custom_import_map/public/index.ts b/public/index.ts similarity index 94% rename from custom_import_map/public/index.ts rename to public/index.ts index 6fe617d6..c0461f64 100644 --- a/custom_import_map/public/index.ts +++ b/public/index.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import './index.scss'; + import { CustomImportMapPlugin } from './plugin'; // This exports static code and TypeScript types, diff --git a/public/model/OSMLayerFunctions.ts b/public/model/OSMLayerFunctions.ts new file mode 100644 index 00000000..ca879121 --- /dev/null +++ b/public/model/OSMLayerFunctions.ts @@ -0,0 +1,123 @@ +import { Map as Maplibre, LayerSpecification } from 'maplibre-gl'; +import { OSMLayerSpecification } from './mapLayerType'; +import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} + +// Fetch style layers from OpenSearch vector tile service +const fetchStyleLayers = (url: string) => { + return fetch(url) + .then((res) => res.json()) + .then((json) => json.layers) + .catch((error) => { + // eslint-disable-next-line no-console + console.log('error', error); + }); +}; + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +const handleStyleLayers = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayerZoomRange( + mbLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + // TODO: figure out error reason + if (mbLayer.type === 'symbol') { + return; + } + maplibreRef.current?.setPaintProperty( + mbLayer.id, + `${mbLayer.type}-opacity`, + layerConfig.opacity / 100 + ); + } + }); +}; + +const updateLayerConfig = (layerConfig: OSMLayerSpecification, maplibreRef: MaplibreRef) => { + if (maplibreRef.current) { + handleStyleLayers(layerConfig, maplibreRef); + } +}; + +const addNewLayer = ( + layerConfig: OSMLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + if (maplibreRef.current) { + const layerSource = layerConfig?.source; + const layerStyle = layerConfig?.style; + maplibreRef.current.addSource(layerConfig.id, { + type: 'vector', + url: layerSource?.dataURL, + }); + fetchStyleLayers(layerStyle?.styleURL).then((styleLayers: LayerSpecification[]) => { + const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); + styleLayers.forEach((styleLayer) => { + styleLayer.id = styleLayer.id + '_' + layerConfig.id; + if (styleLayer.type !== 'background') { + styleLayer.source = layerConfig.id; + } + maplibreRef.current?.addLayer(styleLayer, beforeMbLayerId); + maplibreRef.current?.setLayoutProperty(styleLayer.id, 'visibility', layerConfig.visibility); + maplibreRef.current?.setLayerZoomRange( + styleLayer.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + // TODO: figure out error reason + if (styleLayer.type === 'symbol') { + return; + } + maplibreRef.current?.setPaintProperty( + styleLayer.id, + `${styleLayer.type}-opacity`, + layerConfig.opacity / 100 + ); + }); + }); + } +}; + +// Functions for OpenSearch maps vector tile layer +export const OSMLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: OSMLayerSpecification, + beforeLayerId: string | undefined + ) => { + // If layer already exist in maplibre source, update layer config + // else add new layer. + if (layerExistInMbSource(layerConfig.id, maplibreRef)) { + updateLayerConfig(layerConfig, maplibreRef); + } else { + addNewLayer(layerConfig, maplibreRef, beforeLayerId); + } + }, + remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.removeLayer(mbLayer.id); + } + }); + }, + hide: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayoutProperty(mbLayer.id, 'visibility', layerConfig.visibility); + } + }); + }, +}; diff --git a/public/model/customLayerFunctions.ts b/public/model/customLayerFunctions.ts new file mode 100644 index 00000000..b8f43bd9 --- /dev/null +++ b/public/model/customLayerFunctions.ts @@ -0,0 +1,122 @@ +import { Map as Maplibre, AttributionControl, RasterSourceSpecification } from 'maplibre-gl'; +import { CustomLayerSpecification, OSMLayerSpecification } from './mapLayerType'; +import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +const updateLayerConfig = (layerConfig: CustomLayerSpecification, maplibreRef: MaplibreRef) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const customLauer = maplibreInstance.getLayer(layerConfig.id); + if (customLauer) { + maplibreInstance.setPaintProperty( + layerConfig.id, + 'raster-opacity', + layerConfig.opacity / 100 + ); + maplibreInstance.setLayerZoomRange( + layerConfig.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + const rasterLayerSource = maplibreInstance.getSource( + layerConfig.id + )! as RasterSourceSpecification; + if (rasterLayerSource.attribution !== layerConfig.source?.attribution) { + rasterLayerSource.attribution = layerConfig?.source?.attribution; + maplibreInstance._controls.forEach((control) => { + if (control instanceof AttributionControl) { + control._updateAttributions(); + } + }); + } + const tilesURL = getCustomMapURL(layerConfig); + if (rasterLayerSource.tiles![0] !== tilesURL) { + rasterLayerSource.tiles = [layerConfig?.source?.url]; + maplibreInstance.style.sourceCaches[layerConfig.id].clearTiles(); + maplibreInstance.style.sourceCaches[layerConfig.id].update(maplibreInstance.transform); + maplibreInstance.triggerRepaint(); + } + } + } +}; + +const addNewLayer = ( + layerConfig: CustomLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const tilesURL = getCustomMapURL(layerConfig); + const layerSource = layerConfig?.source; + maplibreInstance.addSource(layerConfig.id, { + type: 'raster', + tiles: [tilesURL], + tileSize: 256, + attribution: layerSource?.attribution, + }); + const beforeMbLayerId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); + maplibreInstance.addLayer( + { + id: layerConfig.id, + type: 'raster', + source: layerConfig.id, + }, + beforeMbLayerId + ); + maplibreInstance.setLayerZoomRange( + layerConfig.id, + layerConfig.zoomRange[0], + layerConfig.zoomRange[1] + ); + } +}; + +const getCustomMapURL = (layerConfig: CustomLayerSpecification) => { + const layerSource = layerConfig?.source; + if (layerSource?.customType === 'tms') { + return layerSource?.url; + } else if (layerSource?.customType === 'wms') { + const referenceSystemName = layerSource.version === '1.3.0' ? 'crs' : 'srs'; + 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}`; + } else { + return ''; + } +}; + +export const CustomLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: CustomLayerSpecification, + beforeLayerId: string | undefined + ) => { + if (layerExistInMbSource(layerConfig.id, maplibreRef)) { + updateLayerConfig(layerConfig, maplibreRef); + } else { + addNewLayer(layerConfig, maplibreRef, beforeLayerId); + } + }, + remove: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.removeLayer(mbLayer.id); + } + }); + }, + hide: (maplibreRef: MaplibreRef, layerConfig: OSMLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((mbLayer: { id: any }) => { + if (mbLayer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayoutProperty(mbLayer.id, 'visibility', layerConfig.visibility); + } + }); + }, +}; diff --git a/public/model/documentLayerFunctions.ts b/public/model/documentLayerFunctions.ts new file mode 100644 index 00000000..befcf5b5 --- /dev/null +++ b/public/model/documentLayerFunctions.ts @@ -0,0 +1,401 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre } from 'maplibre-gl'; +import { parse } from 'wellknown'; +import { DocumentLayerSpecification } from './mapLayerType'; +import { convertGeoPointToGeoJSON, isGeoJSON } from '../utils/geo_formater'; +import { getMaplibreBeforeLayerId, layerExistInMbSource } from './layersFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} +// https://opensearch.org/docs/1.3/opensearch/supported-field-types/geo-shape +const openSearchGeoJSONMap = new Map([ + ['point', 'Point'], + ['linestring', 'LineString'], + ['polygon', 'Polygon'], + ['multipoint', 'MultiPoint'], + ['multilinestring', 'MultiLineString'], + ['multipolygon', 'MultiPolygon'], + ['geometrycollection', 'GeometryCollection'], +]); + +const buildLayerSuffix = (layerId: string, mapLibreType: string) => { + if (mapLibreType.toLowerCase() === 'circle') { + return layerId; + } + if (mapLibreType.toLowerCase() === 'line') { + return layerId + '-line'; + } + if (mapLibreType.toLowerCase() === 'fill') { + return layerId + '-fill'; + } + if (mapLibreType.toLowerCase() === 'fill-outline') { + return layerId + '-outline'; + } + // if unknown type is found, use layerId as default + return layerId; +}; + +const getFieldValue = (data: any, name: string) => { + if (!name) { + return null; + } + const keys = name.split('.'); + return keys.reduce((pre, cur) => { + return pre?.[cur]; + }, data); +}; + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +const getGeoFieldType = (layerConfig: DocumentLayerSpecification) => { + return layerConfig?.source?.geoFieldType; +}; + +const getGeoFieldName = (layerConfig: DocumentLayerSpecification) => { + return layerConfig?.source?.geoFieldName; +}; + +const buildGeometry = (fieldType: string, location: any) => { + if (isGeoJSON(location)) { + return { + type: openSearchGeoJSONMap.get(location.type?.toLowerCase()), + coordinates: location.coordinates, + }; + } + + if (typeof location === 'string') { + // Check if location is WKT format + const geometry = parse(location); + if (geometry) { + return geometry; + } + } + // Geopoint supports other format like object, string, array, + if (fieldType === 'geo_point') { + // convert other supported formats to GeoJSON + return convertGeoPointToGeoJSON(location); + } + // We don't support any other format + return undefined; +}; + +const buildProperties = (document: any, fields: string[]) => { + const property: { [name: string]: any } = {}; + if (!fields) { + return property; + } + fields.forEach((field) => { + const fieldValue = getFieldValue(document._source, field); + if (fieldValue) { + property[field] = fieldValue; + } + }); + return property; +}; + +const getLayerSource = (data: any, layerConfig: DocumentLayerSpecification) => { + const geoFieldName = getGeoFieldName(layerConfig); + const geoFieldType = getGeoFieldType(layerConfig); + const featureList: any = []; + data.forEach((item: any) => { + const geoFieldValue = getFieldValue(item._source, geoFieldName); + const geometry = buildGeometry(geoFieldType, geoFieldValue); + if (geometry) { + const feature = { + geometry, + properties: buildProperties(item, layerConfig.source.tooltipFields), + }; + featureList.push(feature); + } + }); + return { + type: 'FeatureCollection', + features: featureList, + }; +}; + +const addNewLayer = ( + layerConfig: DocumentLayerSpecification, + maplibreRef: MaplibreRef, + data: any, + beforeLayerId: string | undefined +) => { + const maplibreInstance = maplibreRef.current; + const mbLayerBeforeId = getMaplibreBeforeLayerId(layerConfig, maplibreRef, beforeLayerId); + const addLineLayer = ( + documentLayerConfig: DocumentLayerSpecification, + beforeId: string | undefined + ) => { + const lineLayerId = buildLayerSuffix(documentLayerConfig.id, 'line'); + maplibreInstance?.addLayer( + { + id: lineLayerId, + type: 'line', + source: documentLayerConfig.id, + filter: ['==', '$type', 'LineString'], + paint: { + 'line-color': documentLayerConfig.style?.fillColor, + 'line-opacity': documentLayerConfig.opacity / 100, + 'line-width': documentLayerConfig.style?.borderThickness, + }, + }, + beforeId + ); + maplibreInstance?.setLayoutProperty(lineLayerId, 'visibility', documentLayerConfig.visibility); + maplibreInstance?.setLayerZoomRange( + lineLayerId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + }; + + const addCircleLayer = ( + documentLayerConfig: DocumentLayerSpecification, + beforeId: string | undefined + ) => { + const circleLayerId = buildLayerSuffix(documentLayerConfig.id, 'circle'); + maplibreInstance?.addLayer( + { + id: circleLayerId, + type: 'circle', + source: layerConfig.id, + filter: ['==', '$type', 'Point'], + paint: { + 'circle-radius': documentLayerConfig.style?.markerSize, + 'circle-color': documentLayerConfig.style?.fillColor, + 'circle-opacity': documentLayerConfig.opacity / 100, + 'circle-stroke-width': documentLayerConfig.style?.borderThickness, + 'circle-stroke-color': documentLayerConfig.style?.borderColor, + }, + }, + beforeId + ); + maplibreInstance?.setLayoutProperty( + circleLayerId, + 'visibility', + documentLayerConfig.visibility + ); + maplibreInstance?.setLayerZoomRange( + circleLayerId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + }; + + const addFillLayer = ( + documentLayerConfig: DocumentLayerSpecification, + beforeId: string | undefined + ) => { + const fillLayerId = buildLayerSuffix(documentLayerConfig.id, 'fill'); + maplibreInstance?.addLayer( + { + id: fillLayerId, + type: 'fill', + source: layerConfig.id, + filter: ['==', '$type', 'Polygon'], + paint: { + 'fill-color': documentLayerConfig.style?.fillColor, + 'fill-opacity': documentLayerConfig.opacity / 100, + }, + }, + beforeId + ); + maplibreInstance?.setLayoutProperty(fillLayerId, 'visibility', documentLayerConfig.visibility); + maplibreInstance?.setLayerZoomRange( + fillLayerId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + // Due to limitations on WebGL, fill can't render outlines with width wider than 1, + // so we have to create another style layer with type=line to apply width. + const outlineId = buildLayerSuffix(documentLayerConfig.id, 'fill-outline'); + maplibreInstance?.addLayer( + { + id: outlineId, + type: 'line', + source: layerConfig.id, + filter: ['==', '$type', 'Polygon'], + paint: { + 'line-color': layerConfig.style?.borderColor, + 'line-opacity': layerConfig.opacity / 100, + 'line-width': layerConfig.style?.borderThickness, + }, + }, + beforeId + ); + maplibreInstance?.setLayoutProperty(outlineId, 'visibility', layerConfig.visibility); + maplibreInstance?.setLayerZoomRange( + outlineId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + }; + + if (maplibreInstance) { + const source = getLayerSource(data, layerConfig); + maplibreInstance.addSource(layerConfig.id, { + type: 'geojson', + data: source, + }); + addCircleLayer(layerConfig, mbLayerBeforeId); + const geoFieldType = getGeoFieldType(layerConfig); + if (geoFieldType === 'geo_shape') { + addLineLayer(layerConfig, mbLayerBeforeId); + addFillLayer(layerConfig, mbLayerBeforeId); + } + } +}; + +const updateCircleLayer = ( + maplibreInstance: Maplibre, + documentLayerConfig: DocumentLayerSpecification +) => { + const circleLayerId = buildLayerSuffix(documentLayerConfig.id, 'circle'); + const circleLayerStyle = documentLayerConfig.style; + maplibreInstance?.setLayerZoomRange( + circleLayerId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + maplibreInstance?.setPaintProperty( + circleLayerId, + 'circle-opacity', + documentLayerConfig.opacity / 100 + ); + maplibreInstance?.setPaintProperty(circleLayerId, 'circle-color', circleLayerStyle?.fillColor); + maplibreInstance?.setPaintProperty( + circleLayerId, + 'circle-stroke-color', + circleLayerStyle?.borderColor + ); + maplibreInstance?.setPaintProperty( + circleLayerId, + 'circle-stroke-width', + circleLayerStyle?.borderThickness + ); + maplibreInstance?.setPaintProperty(circleLayerId, 'circle-radius', circleLayerStyle?.markerSize); +}; + +const updateLineLayer = ( + maplibreInstance: Maplibre, + documentLayerConfig: DocumentLayerSpecification +) => { + const lineLayerId = buildLayerSuffix(documentLayerConfig.id, 'line'); + maplibreInstance?.setLayerZoomRange( + lineLayerId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + maplibreInstance?.setPaintProperty( + lineLayerId, + 'line-opacity', + documentLayerConfig.opacity / 100 + ); + maplibreInstance?.setPaintProperty( + lineLayerId, + 'line-color', + documentLayerConfig.style?.fillColor + ); + maplibreInstance?.setPaintProperty( + lineLayerId, + 'line-width', + documentLayerConfig.style?.borderThickness + ); +}; + +const updateFillLayer = ( + maplibreInstance: Maplibre, + documentLayerConfig: DocumentLayerSpecification +) => { + const fillLayerId = buildLayerSuffix(documentLayerConfig.id, 'fill'); + maplibreInstance?.setLayerZoomRange( + fillLayerId, + documentLayerConfig.zoomRange[0], + documentLayerConfig.zoomRange[1] + ); + maplibreInstance?.setPaintProperty( + fillLayerId, + 'fill-opacity', + documentLayerConfig.opacity / 100 + ); + maplibreInstance?.setPaintProperty( + fillLayerId, + 'fill-color', + documentLayerConfig.style?.fillColor + ); + maplibreInstance?.setPaintProperty( + fillLayerId, + 'fill-outline-color', + documentLayerConfig.style?.borderColor + ); + const outlineLayerId = buildLayerSuffix(documentLayerConfig.id, 'fill-outline'); + maplibreInstance?.setPaintProperty( + outlineLayerId, + 'line-color', + documentLayerConfig.style?.borderColor + ); + maplibreInstance?.setPaintProperty( + outlineLayerId, + 'line-width', + documentLayerConfig.style?.borderThickness + ); +}; + +const updateLayerConfig = ( + layerConfig: DocumentLayerSpecification, + maplibreRef: MaplibreRef, + data: any +) => { + const maplibreInstance = maplibreRef.current; + if (maplibreInstance) { + const dataSource = maplibreInstance?.getSource(layerConfig.id); + if (dataSource) { + // @ts-ignore + dataSource.setData(getLayerSource(data, layerConfig)); + } + updateCircleLayer(maplibreInstance, layerConfig); + const geoFieldType = getGeoFieldType(layerConfig); + if (geoFieldType === 'geo_shape') { + updateLineLayer(maplibreInstance, layerConfig); + updateFillLayer(maplibreInstance, layerConfig); + } + } +}; + +export const DocumentLayerFunctions = { + render: ( + maplibreRef: MaplibreRef, + layerConfig: DocumentLayerSpecification, + data: any, + beforeLayerId: string | undefined + ) => { + if (layerExistInMbSource(layerConfig.id, maplibreRef)) { + updateLayerConfig(layerConfig, maplibreRef, data); + } else { + addNewLayer(layerConfig, maplibreRef, data, beforeLayerId); + } + }, + remove: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((layer: { id: any }) => { + if (layer.id.includes(layerConfig.id)) { + maplibreRef.current?.removeLayer(layer.id); + } + }); + }, + hide: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => { + const layers = getCurrentStyleLayers(maplibreRef); + layers.forEach((layer) => { + if (layer.id.includes(layerConfig.id)) { + maplibreRef.current?.setLayoutProperty(layer.id, 'visibility', layerConfig.visibility); + } + }); + }, +}; diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts new file mode 100644 index 00000000..2f34cde3 --- /dev/null +++ b/public/model/layerRenderController.ts @@ -0,0 +1,153 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre } from 'maplibre-gl'; +import { DocumentLayerSpecification, MapLayerSpecification } from './mapLayerType'; +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; +import { + buildOpenSearchQuery, + Filter, + GeoBoundingBoxFilter, + getTime, + IOpenSearchDashboardsSearchResponse, + isCompleteResponse, +} from '../../../../src/plugins/data/common'; +import { layersFunctionMap } from './layersFunctions'; +import { MapServices } from '../types'; +import { MapState } from './mapState'; + +interface MaplibreRef { + current: Maplibre | null; +} + +// OpenSearch only accepts longitude in range [-180, 180] +// Maplibre could return value out of the range +function adjustLongitudeForSearch(lon: number) { + if (lon < -180) { + return -180; + } + if (lon > 180) { + return 180; + } + return lon; +} + +export const prepareDataLayerSource = ( + layer: MapLayerSpecification, + mapState: MapState, + { data, notifications }: MapServices, + filters: Filter[] = [] +): Promise => { + return new Promise(async (resolve, reject) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const sourceConfig = layer.source; + const indexPattern = await data.indexPatterns.get(sourceConfig.indexPatternId); + const indexPatternRefName = sourceConfig?.indexPatternRefName; + const geoField = sourceConfig.geoFieldName; + const sourceFields: string[] = [geoField]; + if (sourceConfig.showTooltips && sourceConfig.tooltipFields.length > 0) { + sourceFields.push(...sourceConfig.tooltipFields); + } + let buildQuery; + if (indexPattern) { + const timeFilters = getTime(indexPattern, mapState.timeRange); + buildQuery = buildOpenSearchQuery( + indexPattern, + [], + [ + ...filters, + ...(layer.source.filters ? layer.source.filters : []), + ...(timeFilters ? [timeFilters] : []), + ] + ); + } + const request = { + params: { + index: indexPatternRefName, + size: layer.source.documentRequestNumber, + body: { + _source: sourceFields, + query: buildQuery, + }, + }, + }; + + const search$ = data.search.search(request).subscribe({ + next: (response: IOpenSearchDashboardsSearchResponse) => { + if (isCompleteResponse(response)) { + const dataSource: object = response.rawResponse.hits.hits; + search$.unsubscribe(); + resolve({ dataSource, layer }); + } else { + notifications.toasts.addWarning('An error has occurred when query dataSource'); + search$.unsubscribe(); + reject(); + } + }, + error: (e: Error) => { + data.search.showError(e); + }, + }); + } + }); +}; + +export const handleDataLayerRender = ( + mapLayer: DocumentLayerSpecification, + mapState: MapState, + services: MapServices, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + const filters: Filter[] = []; + const geoField = mapLayer.source.geoFieldName; + const geoFieldType = mapLayer.source.geoFieldType; + + // geo bounding box query supports geo_point fields + if ( + geoFieldType === 'geo_point' && + mapLayer.source.useGeoBoundingBoxFilter && + maplibreRef.current + ) { + const mapBounds = maplibreRef.current.getBounds(); + const filterBoundingBox = { + bottom_right: { + lon: adjustLongitudeForSearch(mapBounds.getSouthEast().lng), + lat: mapBounds.getSouthEast().lat, + }, + top_left: { + lon: adjustLongitudeForSearch(mapBounds.getNorthWest().lng), + lat: mapBounds.getNorthWest().lat, + }, + }; + const geoBoundingBoxFilter: GeoBoundingBoxFilter = { + meta: { + disabled: false, + negate: false, + alias: null, + params: filterBoundingBox, + }, + geo_bounding_box: { + [geoField]: filterBoundingBox, + }, + }; + filters.push(geoBoundingBoxFilter); + } + + return prepareDataLayerSource(mapLayer, mapState, services, filters).then((result) => { + const { layer, dataSource } = result; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + } + }); +}; + +export const handleReferenceLayerRender = ( + layer: MapLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +) => { + layersFunctionMap[layer.type].render(maplibreRef, layer, beforeLayerId); +}; diff --git a/public/model/layersFunctions.ts b/public/model/layersFunctions.ts new file mode 100644 index 00000000..68634816 --- /dev/null +++ b/public/model/layersFunctions.ts @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre } from 'maplibre-gl'; +import { + DASHBOARDS_MAPS_LAYER_ICON, + DASHBOARDS_MAPS_LAYER_NAME, + DASHBOARDS_MAPS_LAYER_TYPE, +} from '../../common'; +import { OSMLayerFunctions } from './OSMLayerFunctions'; +import { DocumentLayerFunctions } from './documentLayerFunctions'; +import { MapLayerSpecification } from './mapLayerType'; +import { CustomLayerFunctions } from './customLayerFunctions'; + +interface MaplibreRef { + current: Maplibre | null; +} + +interface MaplibreRef { + current: Maplibre | null; +} + +const getAllMaplibreLayersIncludesId = (maplibreRef: MaplibreRef, layerId?: string) => { + if (!layerId && !maplibreRef) { + return []; + } + return ( + maplibreRef.current + ?.getStyle() + .layers.filter((layer) => layer.id?.includes(String(layerId)) === true) || [] + ); +}; + +export const LayerActions = { + move: (maplibreRef: MaplibreRef, sourceId: string, beforeId?: string) => { + const sourceMaplibreLayers = getAllMaplibreLayersIncludesId(maplibreRef, sourceId); + if (!sourceMaplibreLayers) { + return; + } + const beforeMaplibreLayers = getAllMaplibreLayersIncludesId(maplibreRef, beforeId); + if (!beforeMaplibreLayers || beforeMaplibreLayers.length < 1) { + // move to top + sourceMaplibreLayers.forEach((layer) => maplibreRef.current?.moveLayer(layer.id)); + return; + } + const topOfBeforeLayer = beforeMaplibreLayers[0]; + sourceMaplibreLayers.forEach((layer) => + maplibreRef.current?.moveLayer(layer.id, topOfBeforeLayer.id) + ); + return; + }, +}; + +export const layersFunctionMap: { [key: string]: any } = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: OSMLayerFunctions, + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DocumentLayerFunctions, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: CustomLayerFunctions, +}; + +export const layersTypeNameMap: { [key: string]: string } = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_NAME.OPENSEARCH_MAP, + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_NAME.DOCUMENTS, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_NAME.CUSTOM_MAP, +}; + +const getCurrentStyleLayers = (maplibreRef: MaplibreRef) => { + return maplibreRef.current?.getStyle().layers || []; +}; + +export const getMaplibreBeforeLayerId = ( + selectedLayer: MapLayerSpecification, + maplibreRef: MaplibreRef, + beforeLayerId: string | undefined +): string | undefined => { + const currentLoadedMbLayers = getCurrentStyleLayers(maplibreRef); + if (beforeLayerId) { + const beforeMbLayer = currentLoadedMbLayers.find((mbLayer) => + mbLayer.id.includes(beforeLayerId) + ); + return beforeMbLayer?.id; + } + return undefined; +}; + +export const layerExistInMbSource = (layerConfigId: string, maplibreRef: MaplibreRef) => { + const layers = getCurrentStyleLayers(maplibreRef); + for (const layer in layers) { + if (layers[layer].id.includes(layerConfigId)) { + return true; + } + } + return false; +}; + +export const layersTypeIconMap: { [key: string]: string } = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: DASHBOARDS_MAPS_LAYER_ICON.OPENSEARCH_MAP, + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: DASHBOARDS_MAPS_LAYER_ICON.DOCUMENTS, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: DASHBOARDS_MAPS_LAYER_ICON.CUSTOM_MAP, +}; + +export const referenceLayerTypeLookup = { + [DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: true, + [DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: true, + [DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: false, +}; diff --git a/public/model/mapLayerType.ts b/public/model/mapLayerType.ts new file mode 100644 index 00000000..0d558103 --- /dev/null +++ b/public/model/mapLayerType.ts @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Filter } from '../../../../src/plugins/data/public'; + +/* eslint @typescript-eslint/consistent-type-definitions: ["error", "type"] */ +export type MapLayerSpecification = + | OSMLayerSpecification + | DocumentLayerSpecification + | CustomLayerSpecification; + +export type OSMLayerSpecification = { + name: string; + id: string; + type: 'opensearch_vector_tile_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + dataURL: string; + }; + style: { + styleURL: string; + }; +}; + +export type DocumentLayerSpecification = { + name: string; + id: string; + type: 'documents'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + indexPatternRefName: string; + indexPatternId: string; + geoFieldType: 'geo_point' | 'geo_shape'; + geoFieldName: string; + documentRequestNumber: number; + showTooltips: boolean; + tooltipFields: string[]; + useGeoBoundingBoxFilter: boolean; + filters: Filter[]; + }; + style: { + fillColor: string; + borderColor: string; + borderThickness: number; + markerSize: number; + }; +}; + +export type CustomLayerSpecification = CustomTMSLayerSpecification | CustomWMSLayerSpecification; + +export type CustomTMSLayerSpecification = { + name: string; + id: string; + type: 'custom_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + url: string; + customType: 'tms'; + attribution: string; + }; +}; + +export type CustomWMSLayerSpecification = { + name: string; + id: string; + type: 'custom_map'; + description: string; + zoomRange: number[]; + opacity: number; + visibility: string; + source: { + url: string; + customType: 'wms'; + attribution: string; + layers: string; + styles: string; + version: string; + format: string; + crs: string; + bbox: string; + }; +}; diff --git a/public/model/mapState.ts b/public/model/mapState.ts new file mode 100644 index 00000000..4d4ceddc --- /dev/null +++ b/public/model/mapState.ts @@ -0,0 +1,10 @@ +import { Query, TimeRange } from '../../../../src/plugins/data/common'; + +export interface MapState { + timeRange: TimeRange; + query: Query; + refreshInterval: { + pause: boolean; + value: number; + }; +} diff --git a/public/plugin.tsx b/public/plugin.tsx new file mode 100644 index 00000000..3fd3d89b --- /dev/null +++ b/public/plugin.tsx @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, +} from '../../../src/core/public'; +import { + AppPluginStartDependencies, + MapServices, CustomImportMapPluginSetup, CustomImportMapPluginStart, +} from './types'; +import {PLUGIN_NAME, PLUGIN_NAVIGATION_BAR_ID, PLUGIN_NAVIGATION_BAR_TILE} from '../common/constants/shared'; + +import { AppPluginSetupDependencies } from './types'; +import { RegionMapVisualizationDependencies } from '../../../src/plugins/region_map/public'; +import { VectorUploadOptions } from './components/vector_upload_options'; + +export class CustomImportMapPlugin + implements Plugin { + public setup( + core: CoreSetup, + { regionMap }: AppPluginSetupDependencies + ): CustomImportMapPluginSetup { + // Register an application into the side navigation menu + core.application.register({ + id: PLUGIN_NAVIGATION_BAR_ID, + title: PLUGIN_NAVIGATION_BAR_TILE, + category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in opensearch_dashboards.json + const [coreStart, depsStart] = await core.getStartServices(); + const { navigation, data } = depsStart as AppPluginStartDependencies; + + // make sure the index pattern list is up to date + data.indexPatterns.clearCache(); + // make sure a default index pattern exists + // if not, the page will be redirected to management and maps won't be rendered + await data.indexPatterns.ensureDefaultIndexPattern(); + + const services: MapServices = { + ...coreStart, + setHeaderActionMenu: params.setHeaderActionMenu, + appBasePath: params.history, + element: params.element, + navigation, + toastNotifications: coreStart.notifications.toasts, + history: params.history, + data, + }; + // Render the application + return renderApp(params, services); + }, + }); + + regionMap.addOptionTab({ + name: 'controls', + title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.controlTabs.controlsTitle', { + defaultMessage: 'Import Vector Map', + }), + editor: (props: RegionMapVisualizationDependencies) => , + }); + + // Return methods that should be available to other plugins + return { + getGreeting() { + return i18n.translate('mapsDashboards.greetingText', { + defaultMessage: 'Hello from {name}!', + values: { + name: PLUGIN_NAME, + }, + }); + }, + }; + } + + public start(core: CoreStart): CustomImportMapPluginStart { + return {}; + } + + public stop() {} +} diff --git a/custom_import_map/public/services.ts b/public/services.ts similarity index 100% rename from custom_import_map/public/services.ts rename to public/services.ts diff --git a/public/types.ts b/public/types.ts new file mode 100644 index 00000000..d34f7273 --- /dev/null +++ b/public/types.ts @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + AppMountParameters, + CoreStart, + SavedObjectsClient, + ToastsStart, +} from 'opensearch-dashboards/public'; +import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; + +import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; + savedObjects: SavedObjectsClient; + data: DataPublicPluginStart; +} + +export interface MapServices extends CoreStart { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + appBasePath: AppMountParameters['history']; + element: AppMountParameters['element']; + navigation: NavigationPublicPluginStart; + toastNotifications: ToastsStart; + history: AppMountParameters['history']; + data: DataPublicPluginStart; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CustomImportMapPluginSetup { + getGreeting: () => string; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CustomImportMapPluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; +} + +export interface AppPluginSetupDependencies { + regionMap: RegionMapPluginSetup; +} diff --git a/public/utils/breadcrumbs.ts b/public/utils/breadcrumbs.ts new file mode 100644 index 00000000..daa69ea2 --- /dev/null +++ b/public/utils/breadcrumbs.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import {PLUGIN_NAVIGATION_BAR_ID} from '../../common'; + +export function getMapsLandingBreadcrumbs(navigateToApp: any) { + return [ + { + text: i18n.translate('maps.listing.breadcrumb', { + defaultMessage: 'Maps', + }), + onClick: () => navigateToApp(PLUGIN_NAVIGATION_BAR_ID), + }, + ]; +} + +export function getCreateBreadcrumbs(navigateToApp: any) { + return [ + ...getMapsLandingBreadcrumbs(navigateToApp), + { + text: i18n.translate('maps.create.breadcrumb', { + defaultMessage: 'Create', + }), + }, + ]; +} + +export function getSavedMapBreadcrumbs(text: string, navigateToApp: any) { + return [ + ...getMapsLandingBreadcrumbs(navigateToApp), + { + text, + }, + ]; +} diff --git a/public/utils/geo_formater.ts b/public/utils/geo_formater.ts new file mode 100644 index 00000000..2c0c274b --- /dev/null +++ b/public/utils/geo_formater.ts @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const geoJSONTypes: string[] = [ + 'point', + 'linestring', + 'polygon', + 'multipoint', + 'multilinestring', + 'multipolygon', + 'geometrycollection', +]; + +export function isGeoJSON(value: { type: any; coordinates: any }) { + if (!value) return false; + if (!value.type || !value.coordinates) { + return false; + } + const geoJSONType = value.type; + if (geoJSONTypes.includes(geoJSONType.toLowerCase())) { + return true; + } + return false; +} + +function buildGeoJSONOfTypePoint(lon: number, lat: number) { + return { + type: 'Point', + coordinates: [lon, lat], + }; +} + +export function convertGeoPointToGeoJSON(location: any) { + // An object with 'lat' and 'lon' properties + if (location?.lat && location?.lon) { + return buildGeoJSONOfTypePoint(location?.lon, location?.lat); + } + // Geopoint as an array && support either (lon/lat) or (lon/lat/z) + if (Array.isArray(location) && (location.length === 2 || location.length === 3)) { + return buildGeoJSONOfTypePoint(location[0], location[1]); + } + + if (typeof location !== 'string') { + return undefined; + } + // Geopoint as a string && support either (lat,lon) or (lat, lon, z) + const values = location.trim().split(','); + if (values && (values.length === 2 || values.length === 3)) { + return buildGeoJSONOfTypePoint(parseFloat(values[1].trim()), parseFloat(values[0].trim())); + } + // TODO Geopoint as geohash + return undefined; +} diff --git a/public/utils/getIntialConfig.ts b/public/utils/getIntialConfig.ts new file mode 100644 index 00000000..6a05c892 --- /dev/null +++ b/public/utils/getIntialConfig.ts @@ -0,0 +1,117 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { v4 as uuidv4 } from 'uuid'; +import { + DOCUMENTS, + DOCUMENTS_DEFAULT_MARKER_SIZE, + DOCUMENTS_DEFAULT_REQUEST_NUMBER, + DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, + DOCUMENTS_DEFAULT_TOOLTIPS, + LAYER_VISIBILITY, + MAP_DATA_LAYER_DEFAULT_OPACITY, + MAP_DEFAULT_MAX_ZOOM, + MAP_DEFAULT_MIN_ZOOM, + MAP_LAYER_DEFAULT_BORDER_THICKNESS, + MAP_REFERENCE_LAYER_DEFAULT_OPACITY, + MAP_VECTOR_TILE_BASIC_STYLE, + MAP_VECTOR_TILE_DATA_SOURCE, + OPENSEARCH_MAP_LAYER, + CUSTOM_MAP, +} from '../../common'; +import { MapState } from '../model/mapState'; + +export const getLayerConfigMap = () => ({ + [OPENSEARCH_MAP_LAYER.type]: { + name: '', + description: '', + type: OPENSEARCH_MAP_LAYER.type, + id: uuidv4(), + zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], + opacity: MAP_REFERENCE_LAYER_DEFAULT_OPACITY, + visibility: LAYER_VISIBILITY.VISIBLE, + source: { + dataURL: MAP_VECTOR_TILE_DATA_SOURCE, + }, + style: { + styleURL: MAP_VECTOR_TILE_BASIC_STYLE, + }, + }, + [DOCUMENTS.type]: { + name: '', + description: '', + type: DOCUMENTS.type, + id: uuidv4(), + zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], + opacity: MAP_DATA_LAYER_DEFAULT_OPACITY, + visibility: LAYER_VISIBILITY.VISIBLE, + source: { + indexPatternRefName: undefined, + geoFieldType: undefined, + geoFieldName: undefined, + documentRequestNumber: DOCUMENTS_DEFAULT_REQUEST_NUMBER, + tooltipFields: DOCUMENTS_DEFAULT_TOOLTIPS, + showTooltips: DOCUMENTS_DEFAULT_SHOW_TOOLTIPS, + }, + style: { + ...getStyleColor(), + borderThickness: MAP_LAYER_DEFAULT_BORDER_THICKNESS, + markerSize: DOCUMENTS_DEFAULT_MARKER_SIZE, + }, + }, + [CUSTOM_MAP.type]: { + name: '', + description: '', + type: CUSTOM_MAP.type, + id: uuidv4(), + zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM], + opacity: MAP_REFERENCE_LAYER_DEFAULT_OPACITY, + visibility: LAYER_VISIBILITY.VISIBLE, + source: { + url: '', + customType: 'wms', + attribution: '', + layers: '', + styles: '', + version: '', + format: '', + crs: '', + bbox: '', + }, + }, +}); + +const getInitialColor = () => { + const colorCode = (Math.random() * 0xfffff * 1000000).toString(16); + return '#' + colorCode.slice(0, 6); +}; + +export const getStyleColor = () => { + const initialColor = getInitialColor(); + return { + fillColor: initialColor, + borderColor: initialColor, + }; +}; + +export const getInitialMapState = (): MapState => { + const timeRange = { + from: 'now-15m', + to: 'now', + }; + const query = { + query: '', + language: 'kuery', + }; + const refreshInterval = { + pause: true, + value: 12000, + }; + return { + timeRange, + query, + refreshInterval, + }; +}; diff --git a/release-notes/opensearch-dashboards-maps.release-notes-2.4.0.0.md b/release-notes/opensearch-dashboards-maps.release-notes-2.4.0.0.md index 7d559642..459c38a4 100644 --- a/release-notes/opensearch-dashboards-maps.release-notes-2.4.0.0.md +++ b/release-notes/opensearch-dashboards-maps.release-notes-2.4.0.0.md @@ -5,5 +5,4 @@ Compatible with OpenSearch and OpenSearch Dashboards Version 2.4.0 * Add windows and mac platform to run unit test ([#74](https://github.com/opensearch-project/dashboards-maps/pull/74)) ### Maintenance -* Version bump to 2.4 for dependent packages ([#86](https://github.com/opensearch-project/dashboards-maps/pull/86)) * Bump version to 2.4.0.0 ([#70](https://github.com/opensearch-project/dashboards-maps/pull/70)) \ No newline at end of file diff --git a/custom_import_map/server/clusters/geospatial_cluster.js b/server/clusters/geospatial_cluster.js similarity index 100% rename from custom_import_map/server/clusters/geospatial_cluster.js rename to server/clusters/geospatial_cluster.js diff --git a/custom_import_map/server/clusters/geospatial_plugin.ts b/server/clusters/geospatial_plugin.ts similarity index 100% rename from custom_import_map/server/clusters/geospatial_plugin.ts rename to server/clusters/geospatial_plugin.ts diff --git a/custom_import_map/server/clusters/index.ts b/server/clusters/index.ts similarity index 100% rename from custom_import_map/server/clusters/index.ts rename to server/clusters/index.ts diff --git a/custom_import_map/server/index.ts b/server/index.ts similarity index 100% rename from custom_import_map/server/index.ts rename to server/index.ts diff --git a/custom_import_map/server/plugin.ts b/server/plugin.ts similarity index 82% rename from custom_import_map/server/plugin.ts rename to server/plugin.ts index d92f3650..0be30228 100644 --- a/custom_import_map/server/plugin.ts +++ b/server/plugin.ts @@ -16,6 +16,8 @@ import { CustomImportMapPluginSetup, CustomImportMapPluginStart } from './types' import { createGeospatialCluster } from './clusters'; import { GeospatialService, OpensearchService } from './services'; import { geospatial, opensearch } from '../server/routes'; +import { mapSavedObjectsType } from './saved_objects'; +import { capabilitiesProvider } from './saved_objects/capabilities_provider'; export class CustomImportMapPlugin implements Plugin { @@ -29,6 +31,7 @@ export class CustomImportMapPlugin public async setup(core: CoreSetup) { this.logger.debug('customImportMap: Setup'); + // @ts-ignore const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); const geospatialClient = createGeospatialCluster(core, globalConfig); @@ -41,6 +44,12 @@ export class CustomImportMapPlugin geospatial(geospatialService, router); opensearch(opensearchService, router); + // Register saved object types + core.savedObjects.registerType(mapSavedObjectsType); + + // Register capabilities + core.capabilities.registerProvider(capabilitiesProvider); + return {}; } diff --git a/custom_import_map/server/routes/geospatial.ts b/server/routes/geospatial.ts similarity index 100% rename from custom_import_map/server/routes/geospatial.ts rename to server/routes/geospatial.ts diff --git a/custom_import_map/server/routes/index.ts b/server/routes/index.ts similarity index 100% rename from custom_import_map/server/routes/index.ts rename to server/routes/index.ts diff --git a/custom_import_map/server/routes/opensearch.ts b/server/routes/opensearch.ts similarity index 100% rename from custom_import_map/server/routes/opensearch.ts rename to server/routes/opensearch.ts diff --git a/server/saved_objects/capabilities_provider.ts b/server/saved_objects/capabilities_provider.ts new file mode 100644 index 00000000..dde86a4f --- /dev/null +++ b/server/saved_objects/capabilities_provider.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const capabilitiesProvider = () => ({ + map: { + // TODO: investigate which capabilities we need to provide + // createNew: true, + // createShortUrl: true, + // delete: true, + show: true, + // showWriteControls: true, + // save: true, + // saveQuery: true, + }, +}); diff --git a/server/saved_objects/index.ts b/server/saved_objects/index.ts new file mode 100644 index 00000000..416ff0ab --- /dev/null +++ b/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { mapSavedObjectsType } from './map_saved_object'; diff --git a/server/saved_objects/map_saved_object.ts b/server/saved_objects/map_saved_object.ts new file mode 100644 index 00000000..ca38099a --- /dev/null +++ b/server/saved_objects/map_saved_object.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from 'opensearch-dashboards/server'; + +export const mapSavedObjectsType: SavedObjectsType = { + name: 'map', + hidden: false, + namespaceType: 'agnostic', + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/maps-dashboards#/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'map.show', + }; + }, + getEditUrl(obj) { + return `/management/opensearch-dashboards/objects/map/${encodeURIComponent(obj.id)}`; + }, + }, + mappings: { + properties: { + title: { type: 'text' }, + description: { type: 'text' }, + layerList: { type: 'text', index: false }, + uiState: { type: 'text', index: false }, + mapState: { type: 'text', index: false }, + version: { type: 'integer' }, + // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow + // When we save a saved object, the saved object plugin will extract the search source into two parts + // Some information will be put into kibanaSavedObjectMeta while others will be created as a reference object and pushed to the reference array + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false } }, + }, + }, + }, + migrations: {}, +}; diff --git a/custom_import_map/server/services/geospatial_service.js b/server/services/geospatial_service.js similarity index 100% rename from custom_import_map/server/services/geospatial_service.js rename to server/services/geospatial_service.js diff --git a/custom_import_map/server/services/index.js b/server/services/index.ts similarity index 100% rename from custom_import_map/server/services/index.js rename to server/services/index.ts diff --git a/custom_import_map/server/services/opensearch_service.js b/server/services/opensearch_service.js similarity index 100% rename from custom_import_map/server/services/opensearch_service.js rename to server/services/opensearch_service.js diff --git a/custom_import_map/server/services/utils/constants.ts b/server/services/utils/constants.ts similarity index 100% rename from custom_import_map/server/services/utils/constants.ts rename to server/services/utils/constants.ts diff --git a/custom_import_map/server/types.ts b/server/types.ts similarity index 100% rename from custom_import_map/server/types.ts rename to server/types.ts diff --git a/custom_import_map/test/enzyme.js b/test/enzyme.js similarity index 100% rename from custom_import_map/test/enzyme.js rename to test/enzyme.js diff --git a/custom_import_map/test/jest.config.js b/test/jest.config.js similarity index 92% rename from custom_import_map/test/jest.config.js rename to test/jest.config.js index 63cb9c63..1d87b42b 100644 --- a/custom_import_map/test/jest.config.js +++ b/test/jest.config.js @@ -22,9 +22,9 @@ module.exports = { snapshotSerializers: ['../../node_modules/enzyme-to-json/serializer'], coverageReporters: ['lcov', 'text', 'cobertura'], testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'], - coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/test/',], + coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/test/'], clearMocks: true, testPathIgnorePatterns: ['/build/', '/node_modules/'], - modulePathIgnorePatterns: ['customImportMapDashboards'], + modulePathIgnorePatterns: [], testEnvironment: 'jest-environment-jsdom', }; diff --git a/custom_import_map/test/mocks/styleMock.js b/test/mocks/styleMock.js similarity index 100% rename from custom_import_map/test/mocks/styleMock.js rename to test/mocks/styleMock.js diff --git a/custom_import_map/test/polyfills.js b/test/polyfills.js similarity index 100% rename from custom_import_map/test/polyfills.js rename to test/polyfills.js diff --git a/custom_import_map/test/polyfills/mutationObserver.js b/test/polyfills/mutationObserver.js similarity index 100% rename from custom_import_map/test/polyfills/mutationObserver.js rename to test/polyfills/mutationObserver.js diff --git a/custom_import_map/test/setup.jest.js b/test/setup.jest.js similarity index 100% rename from custom_import_map/test/setup.jest.js rename to test/setup.jest.js diff --git a/custom_import_map/test/setupTests.js b/test/setupTests.js similarity index 100% rename from custom_import_map/test/setupTests.js rename to test/setupTests.js diff --git a/translations/ja-JP.json b/translations/ja-JP.json new file mode 100644 index 00000000..d00fb9fb --- /dev/null +++ b/translations/ja-JP.json @@ -0,0 +1,81 @@ +{ + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + }, + "relative": { + "years": { + "units": "year" + }, + "months": { + "units": "month" + }, + "days": { + "units": "day" + }, + "hours": { + "units": "hour" + }, + "minutes": { + "units": "minute" + }, + "seconds": { + "units": "second" + } + } + }, + "messages": { + "mapsDashboards.buttonText": "Translate me to Japanese" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..01fa7676 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,58 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + // Allows for importing from `opensearch-dashboards` package for the exported types. + "opensearch-dashboards": ["./opensearch_dashboards"], + "ui/*": ["src/legacy/ui/public/*"], + "test_utils/*": ["src/test_utils/public/*"] + }, + // Support .tsx files and transform JSX into calls to React.createElement + "jsx": "react", + // Enables all strict type checking options. + "strict": true, + // enables "core language features" + "lib": [ + // ESNext auto includes previous versions all the way back to es5 + "esnext", + // includes support for browser APIs + "dom" + ], + // Node 8 should support everything output by esnext, we override this + // in webpack with loader-level compiler options + "target": "esnext", + // Use commonjs for node, overridden in webpack to keep import statements + // to maintain support for things like `await import()` + "module": "commonjs", + // Allows default imports from modules with no default export. This does not affect code emit, just type checking. + // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or + // ESNext module format is used. + "allowSyntheticDefaultImports": true, + // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. + "esModuleInterop": true, + // Resolve modules in the same way as Node.js. Aka make `require` works the + // same in TypeScript as it does in Node.js. + "moduleResolution": "node", + // Disallow inconsistently-cased references to the same file. + "forceConsistentCasingInFileNames": true, + // Disable the breaking keyof behaviour introduced in TS 2.9.2 until EUI is updated to support that too + "keyofStringsOnly": true, + // Forbid unused local variables as the rule was deprecated by ts-lint + "noUnusedLocals": true, + // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. + "downlevelIteration": true, + // import tslib helpers rather than inlining helpers for iteration or spreading, for instance + "importHelpers": true, + // adding global typings + "types": ["node", "jest", "react"] + }, + "include": [ + "server/**/*", + "public/**/*", + "utils/**/*", + "models/**/*", + "test/**/*" + ], + "exclude": ["node_modules", "*/node_modules/"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..f21443e3 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1433 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cypress/request@^2.88.10": + version "2.88.10" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce" + integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + http-signature "~1.3.6" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^8.3.2" + +"@cypress/skip-test@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@cypress/skip-test/-/skip-test-2.6.1.tgz#44a4bc4c2b2e369a7661177c9b38e50d417a36ea" + integrity sha512-X+ibefBiuOmC5gKG91wRIT0/OqXeETYvu7zXktjZ3yLeO186Y8ia0K7/gQUpAwuUi28DuqMd1+7tBQVtPkzbPA== + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + +"@mapbox/geojson-rewind@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz#591a5d71a9cd1da1a0bf3420b3bea31b0fc7946a" + integrity sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA== + dependencies: + get-stream "^6.0.1" + minimist "^1.2.6" + +"@mapbox/jsonlint-lines-primitives@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" + integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ== + +"@mapbox/mapbox-gl-supported@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz#c15367178d8bfe4765e6b47b542fe821ce259c7b" + integrity sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ== + +"@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" + integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ== + +"@mapbox/tiny-sdf@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz#cdba698d3d65087643130f9af43a2b622ce0b372" + integrity sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw== + +"@mapbox/unitbezier@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz#d32deb66c7177e9e9dfc3bbd697083e2e657ff01" + integrity sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw== + +"@mapbox/vector-tile@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666" + integrity sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw== + dependencies: + "@mapbox/point-geometry" "~0.1.0" + +"@mapbox/whoots-js@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe" + integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q== + +"@opensearch-dashboards-test/opensearch-dashboards-test-library@git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#main": + version "1.0.4" + resolved "git+https://github.com/opensearch-project/opensearch-dashboards-test-library.git#9c0adaa6d492accd9f737aa905b2a2ca3be6840f" + +"@types/geojson@*", "@types/geojson@^7946.0.10": + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + +"@types/mapbox__point-geometry@*", "@types/mapbox__point-geometry@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz#488a9b76e8457d6792ea2504cdd4ecdd9860a27e" + integrity sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA== + +"@types/mapbox__vector-tile@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz#8fa1379dbaead1e1b639b8d96cfd174404c379d6" + integrity sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g== + dependencies: + "@types/geojson" "*" + "@types/mapbox__point-geometry" "*" + "@types/pbf" "*" + +"@types/node@*": + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + +"@types/node@^14.14.31": + version "14.18.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835" + integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ== + +"@types/pbf@*", "@types/pbf@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" + integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== + +"@types/sinonjs__fake-timers@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" + integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== + +"@types/sizzle@^2.3.2": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + +"@types/wellknown@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@types/wellknown/-/wellknown-0.5.4.tgz#1f12a2ca9cda236673272688b7549a30f0c9e3bb" + integrity sha512-zcsf4oHeEcvpvWhDOB1+qNK04oFJtsmv7Otb1Vy8w8GAqQkgRrVkI/C76PdwtpiT216aM1SbhkKgKWzGLhHKng== + +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +cachedir@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== + +ci-info@^3.2.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" + integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-table3@~0.6.1: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.16: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@~1.5.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + integrity sha512-H6xsIBfQ94aESBG8jGHXQ7i5AEpy5ZeVaLDOisDICiTCKpqEfr34/KmTrspKQNoLKNu9gTkovlpQcUi630AKiQ== + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csscolorparser@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" + integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== + +cypress-file-upload@^5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" + integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== + +cypress-multi-reporters@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.2.tgz#129dfeffa00d4deca3e9f58d84570b9962c28c2b" + integrity sha512-lvwGwHqZG5CwGxBJ6UJXWaxlWGkJgxBjP0h+IVLrrwRlJpT4coSwwt+UzMdeqEMrzT4IDfhbtmUNOiDleisOYA== + dependencies: + debug "^4.1.1" + lodash "^4.17.15" + +cypress@9.5.4: + version "9.5.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.4.tgz#49d9272f62eba12f2314faf29c2a865610e87550" + integrity sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q== + dependencies: + "@cypress/request" "^2.88.10" + "@cypress/xvfb" "^1.2.4" + "@types/node" "^14.14.31" + "@types/sinonjs__fake-timers" "8.1.1" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + buffer "^5.6.0" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.1" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "^4.3.2" + enquirer "^2.3.6" + eventemitter2 "^6.4.3" + execa "4.1.0" + executable "^4.1.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.0" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr2 "^3.8.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.6" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + proxy-from-env "1.0.0" + request-progress "^3.0.0" + semver "^7.3.2" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +dayjs@^1.10.4: + version "1.11.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" + integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +earcut@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eventemitter2@^6.4.3: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + +execa@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +geojson-vt@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" + integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg== + +get-stream@^5.0.0, get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +gl-matrix@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.3.tgz#fc1191e8320009fd4d20e9339595c6041ddc22c9" + integrity sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA== + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== + dependencies: + assert-plus "^1.0.0" + jsprim "^2.0.2" + sshpk "^1.14.1" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +ieee754@^1.1.12, ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +is-ci@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +kdbush@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" + integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== + +listr2@^3.8.3: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +maplibre-gl@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-2.4.0.tgz#2b53dbf526626bf4ee92ad4f33f13ef09e5af182" + integrity sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA== + dependencies: + "@mapbox/geojson-rewind" "^0.5.2" + "@mapbox/jsonlint-lines-primitives" "^2.0.2" + "@mapbox/mapbox-gl-supported" "^2.0.1" + "@mapbox/point-geometry" "^0.1.0" + "@mapbox/tiny-sdf" "^2.0.5" + "@mapbox/unitbezier" "^0.0.1" + "@mapbox/vector-tile" "^1.3.1" + "@mapbox/whoots-js" "^3.1.0" + "@types/geojson" "^7946.0.10" + "@types/mapbox__point-geometry" "^0.1.2" + "@types/mapbox__vector-tile" "^1.3.0" + "@types/pbf" "^3.0.2" + csscolorparser "~1.0.3" + earcut "^2.2.4" + geojson-vt "^3.2.1" + gl-matrix "^3.4.3" + global-prefix "^3.0.0" + murmurhash-js "^1.0.0" + pbf "^3.2.1" + potpack "^1.0.2" + quickselect "^2.0.0" + supercluster "^7.1.5" + tinyqueue "^2.0.3" + vt-pbf "^3.1.3" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6, minimist@~1.2.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +murmurhash-js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" + integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== + +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +pbf@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" + integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== + dependencies: + ieee754 "^1.1.12" + resolve-protobuf-schema "^2.1.0" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +potpack@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14" + integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ== + +prettier@^2.1.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== + +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw== + +protocol-buffers-schema@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" + integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== + +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== + dependencies: + throttleit "^1.0.0" + +resolve-protobuf-schema@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rxjs@^7.5.1: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.3.2: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +sshpk@^1.14.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supercluster@^7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3" + integrity sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg== + dependencies: + kdbush "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tinyqueue@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== + +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typedarray@~0.0.5: + version "0.0.7" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.7.tgz#799207136a37f3b3efb8c66c40010d032714dc73" + integrity sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vt-pbf@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" + integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA== + dependencies: + "@mapbox/point-geometry" "0.1.0" + "@mapbox/vector-tile" "^1.3.1" + pbf "^3.2.1" + +wellknown@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wellknown/-/wellknown-0.5.0.tgz#09ae9871fa826cf0a6ec1537ef00c379d78d7101" + integrity sha512-za5vTLuPF9nmrVOovYQwNEWE/PwJCM+yHMAj4xN1WWUvtq9OElsvKiPL0CR9rO8xhrYqL7NpI7IknqR8r6eYOg== + dependencies: + concat-stream "~1.5.0" + minimist "~1.2.0" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0"