diff --git a/.eslintrc.js b/.eslintrc.js index 2381a307191..90e789d38e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -459,7 +459,7 @@ module.exports = { * Files that run BEFORE node version check */ { - files: ['scripts/**/*.js', 'src/setup_node_env/**/*.js'], + files: ['scripts/**/*.js', 'src/setup_node_env/**/!(*.test).js'], rules: { 'import/no-commonjs': 'off', 'prefer-object-spread/prefer-object-spread': 'off', diff --git a/.github/workflows/add-untriaged.yml b/.github/workflows/add-untriaged.yml new file mode 100644 index 00000000000..9dcc7020d24 --- /dev/null +++ b/.github/workflows/add-untriaged.yml @@ -0,0 +1,19 @@ +name: Apply 'untriaged' label during issue lifecycle + +on: + issues: + types: [opened, reopened, transferred] + +jobs: + apply-label: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['untriaged'] + }) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index f515b254b86..0ba694cbf21 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -12,6 +12,17 @@ jobs: contents: write pull-requests: write name: Backport + # Only react to merged PRs for security reasons. + # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. + if: > + github.event.pull_request.merged + && ( + github.event.action == 'closed' + || ( + github.event.action == 'labeled' + && contains(github.event.label.name, 'backport') + ) + ) steps: - name: GitHub App token id: github_app_token @@ -22,10 +33,9 @@ jobs: # opensearch-trigger-bot installation ID installation_id: 22958780 - # Using fork of https://github.com/tibdex/backport - # https://github.com/tibdex/backport/pull/81 - name: Backport - uses: VachaShah/backport@v1.1.4 + uses: VachaShah/backport@v2.1.0 with: github_token: ${{ steps.github_app_token.outputs.token }} - branch_name: backport/backport-${{ github.event.number }} + head_template: backport/backport-<%= number %>-to-<%= base %> + files_to_skip: "CHANGELOG.md" diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index f893f1398e5..5daef79cc8b 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -3,16 +3,18 @@ name: Build and test -# trigger on every commit push and PR for all branches except feature branches and pushes for backport branches +# trigger on every commit push and PR for all branches except pushes for backport branches on: push: - branches: [ '**', '!feature/**', '!backport/**' ] + branches: ['**', '!backport/**'] paths-ignore: - '**/*.md' + - 'docs/**' pull_request: - branches: [ '**', '!feature/**' ] + branches: ['**'] paths-ignore: - '**/*.md' + - 'docs/**' env: TEST_BROWSER_HEADLESS: 1 @@ -26,17 +28,34 @@ env: jobs: build-lint-test: - runs-on: ubuntu-latest - container: - image: docker://opensearchstaging/ci-runner:ci-runner-rockylinux8-opensearch-dashboards-integtest-v2 - options: --user 1001 - name: Build and Verify + name: Build and Verify on ${{ matrix.name }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + include: + - os: ubuntu-latest + name: Linux + - os: windows-latest + name: Windows + runs-on: ${{ matrix.os }} steps: + - name: Configure git's autocrlf (Windows only) + if: matrix.os == 'windows-latest' + run: | + git config --global core.autocrlf false + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Setup JDK (Windows only) + if: matrix.os == 'windows-latest' + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version-file: '.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -45,14 +64,42 @@ jobs: run: | npm uninstall -g yarn npm i -g yarn@1.22.10 + yarn config set network-timeout 1000000 -g - - name: Run bootstrap + - name: Configure Yarn Cache (Linux) + if: matrix.os != 'windows-latest' + run: echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $GITHUB_ENV + + - name: Configure Yarn Cache (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $env:GITHUB_ENV + echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Initialize Yarn Cache + uses: actions/cache@v3 + with: + path: ${{ env.YARN_CACHE_LOCATION }} + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn- + + - name: Run bootstrap (Linux) + if: matrix.os != 'windows-latest' run: yarn osd bootstrap + - name: Run bootstrap (Windows) + if: matrix.os == 'windows-latest' + run: yarn osd bootstrap || yarn osd bootstrap + - name: Run linter id: linter run: yarn lint + - name: Validate NOTICE file + id: notice-validate + run: yarn notice:validate + - name: Run unit tests with coverage id: unit-tests run: yarn test:jest:ci:coverage @@ -66,28 +113,44 @@ jobs: uses: codecov/codecov-action@v3 with: directory: ./target/opensearch-dashboards-coverage + flags: ${{ matrix.name }} - name: Run integration tests id: integration-tests run: yarn test:jest_integration:ci functional-tests: - runs-on: ubuntu-latest - container: - image: docker://opensearchstaging/ci-runner:ci-runner-rockylinux8-opensearch-dashboards-integtest-v2 - options: --user 1001 - name: Run functional tests + name: Run functional tests on ${{ matrix.name }} (ciGroup${{ matrix.group }}) strategy: matrix: - group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] + os: [ubuntu-latest, windows-latest] + group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] + include: + - os: ubuntu-latest + name: Linux + - os: windows-latest + name: Windows + runs-on: ${{ matrix.os }} steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} + - name: Configure git's autocrlf (Windows only) + if: matrix.os == 'windows-latest' + run: | + git config --global core.autocrlf false + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Setup JDK (Windows only) + if: matrix.os == 'windows-latest' + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version-file: '.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -96,14 +159,37 @@ jobs: run: | npm uninstall -g yarn npm i -g yarn@1.22.10 + yarn config set network-timeout 1000000 -g + + - name: Configure Yarn Cache (Linux) + if: matrix.os != 'windows-latest' + run: echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $GITHUB_ENV + + - name: Configure Yarn Cache (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $env:GITHUB_ENV + echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Initialize Yarn Cache + uses: actions/cache@v3 + with: + path: ${{ env.YARN_CACHE_LOCATION }} + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn- - # image has the latest chrome v99 - name: Setup chromedriver - run: yarn add --dev chromedriver@99.0.0 + run: node scripts/upgrade_chromedriver.js - - name: Run bootstrap + - name: Run bootstrap (Linux) + if: matrix.os != 'windows-latest' run: yarn osd bootstrap + - name: Run bootstrap (Windows) + if: matrix.os == 'windows-latest' + run: yarn osd bootstrap || yarn osd bootstrap + - name: Build plugins run: node scripts/build_opensearch_dashboards_platform_plugins --no-examples --workers 10 @@ -116,34 +202,52 @@ jobs: JOB: ci${{ matrix.group }} CACHE_DIR: ciGroup${{ matrix.group }} + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: failure-artifacts-ci${{ matrix.group }} + path: | + test/*/failure_debug/ + test/*/screenshots/ + build-min-artifact-tests: - runs-on: ubuntu-latest - container: - image: docker://opensearchstaging/ci-runner:ci-runner-rockylinux8-opensearch-dashboards-integtest-v2 - options: --user 1001 - name: Build min release artifacts - defaults: - run: - working-directory: ./artifacts + name: Build min release artifacts on ${{ matrix.name }} strategy: matrix: include: - - name: Linux x64 + - os: ubuntu-latest + name: Linux x64 ext: tar.gz suffix: linux-x64 script: build-platform --linux --skip-os-packages - - name: Linux ARM64 + - os: ubuntu-latest + name: Linux ARM64 ext: tar.gz suffix: linux-arm64 script: build-platform --linux-arm --skip-os-packages + - os: windows-latest + name: Windows x64 + ext: zip + suffix: windows-x64 + script: build-platform --windows --skip-os-packages + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ./artifacts steps: + - name: Configure git's autocrlf (Windows only) + if: matrix.os == 'windows-latest' + run: | + git config --global core.autocrlf false + working-directory: . + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ./artifacts - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version-file: './artifacts/.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -152,15 +256,46 @@ jobs: run: | npm uninstall -g yarn npm i -g yarn@1.22.10 + yarn config set network-timeout 1000000 -g - - name: Get package version + - name: Configure Yarn Cache (Linux) + if: matrix.os != 'windows-latest' + run: echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $GITHUB_ENV + + - name: Configure Yarn Cache (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $env:GITHUB_ENV + echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Initialize Yarn Cache + uses: actions/cache@v3 + with: + path: ${{ env.YARN_CACHE_LOCATION }} + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn- + + - name: Get package version (Linux) + if: matrix.os != 'windows-latest' run: | echo "VERSION=$(yarn --silent pkg-version)" >> $GITHUB_ENV - - name: Get artifact build name + - name: Get artifact build name (Linux) + if: matrix.os != 'windows-latest' run: | echo "ARTIFACT_BUILD_NAME=opensearch-dashboards-${{ env.VERSION }}-${{ matrix.suffix }}.${{ matrix.ext }}" >> $GITHUB_ENV + - name: Get package version (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "VERSION=$(yarn --silent pkg-version)" >> $env:GITHUB_ENV + + - name: Get artifact build name (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "ARTIFACT_BUILD_NAME=opensearch-dashboards-${{ env.VERSION }}-${{ matrix.suffix }}.${{ matrix.ext }}" >> $env:GITHUB_ENV + - name: Run bootstrap run: yarn osd bootstrap @@ -186,10 +321,10 @@ jobs: working-directory: ./artifacts strategy: matrix: - version: [ osd-2.0.0, osd-2.1.0, osd-2.2.0, osd-2.3.0 ] + version: [osd-2.0.0, osd-2.1.0, osd-2.2.0, osd-2.3.0, osd-2.4.0, osd-2.5.0] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ./artifacts @@ -197,7 +332,7 @@ jobs: - run: echo [NOTE] These tests will be ran using Linux x64 release builds without security - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version-file: './artifacts/.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -206,6 +341,18 @@ jobs: run: | npm uninstall -g yarn npm i -g yarn@1.22.10 + yarn config set network-timeout 1000000 -g + + - name: Configure Yarn Cache + run: echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $GITHUB_ENV + + - name: Initialize Yarn Cache + uses: actions/cache@v3 + with: + path: ${{ env.YARN_CACHE_LOCATION }} + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + yarn- - name: Get package version run: | diff --git a/.github/workflows/changelog_verifier.yml b/.github/workflows/changelog_verifier.yml index 96f99f17b01..0890ea8b8fb 100644 --- a/.github/workflows/changelog_verifier.yml +++ b/.github/workflows/changelog_verifier.yml @@ -1,6 +1,7 @@ name: "Changelog Verifier" on: pull_request: + branches: [ '**', '!feature/**' ] types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] jobs: @@ -15,4 +16,4 @@ jobs: - uses: dangoslen/changelog-enforcer@v3 with: - skipLabels: "autocut" + skipLabels: "autocut, Skip-Changelog" diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index afe33c85ce1..5c3371db0f7 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -1,9 +1,9 @@ name: Run cypress tests -# trigger on every PR for all branches except feature branches +# trigger on every PR for all branches on: pull_request: - branches: [ '**', '!feature/**' ] + branches: [ '**' ] paths-ignore: - '**/*.md' @@ -11,7 +11,8 @@ env: FTR_PATH: 'ftr' START_CMD: 'node ../scripts/opensearch_dashboards --dev --no-base-path --no-watch' OPENSEARCH_SNAPSHOT_CMD: 'node ../scripts/opensearch snapshot' - SPEC: 'cypress/integration/core-opensearch-dashboards/opensearch-dashboards/*.js,' + SPEC: 'cypress/integration/core-opensearch-dashboards/opensearch-dashboards/**/*.js,' + CYPRESS_ENV: 'env CYPRESS_VISBUILDER_ENABLED=true ' jobs: cypress-tests: @@ -75,7 +76,7 @@ jobs: working-directory: ${{ env.FTR_PATH }} start: ${{ env.OPENSEARCH_SNAPSHOT_CMD }}, ${{ env.START_CMD }} wait-on: 'http://localhost:9200, http://localhost:5601' - command: yarn cypress:run-without-security --browser chromium --spec ${{ env.SPEC }} + command: ${{ env.CYPRESS_ENV }} yarn cypress:run-without-security --browser chromium --spec ${{ env.SPEC }} # Screenshots are only captured on failure, will change this once we do visual regression tests - uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 01c2aaeaf9e..3f1759e6665 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .ackrc /.opensearch /.chromium +/package.json.bak .DS_Store .node_binaries .native_modules diff --git a/.node-version b/.node-version index a3eb5a03fa6..f46d5e39424 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.20.0 +14.21.3 diff --git a/.nvmrc b/.nvmrc index a3eb5a03fa6..f46d5e39424 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.20.0 +14.21.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index eb9fb0ceed4..351ec10dd7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### πŸ›‘ Security +- [Legacy Maps Plugin] Prevent reverse-tabnabbing ([#2540](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2540)) +- Eliminate dependency on `got` versions older than 11.8.5 ([#2801](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2801)) +- [Multi DataSource] Add explicit no spellcheck on password fields ([#2818](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2818)) +- [CVE-2022-25912] Bumps simple-git from 3.4.0 to 3.15.0 ([#3036](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3036)) +- [CVE-2022-35256] Bumps node version from 14.20.0 to 14.20.1 [#3166](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3166)) +- [CVE-2022-46175] Bumps json5 version from 1.0.1 and 2.2.1 to 1.0.2 and 2.2.3 ([#3201](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3201)) +- [CVE-2022-25860] Bumps simple-git from 3.15.1 to 3.16.0 ([#3345](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3345)) +- [Security] Bumps hapi/statehood to 7.0.4 ([#3411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3411)) +- [CVE-2023-25166] Bump formula to 3.0.1 ([#3416](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3416)) +- [CVE-2023-25653] Bump node-jose to 2.2.0 ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445)) +- [CVE-2023-26486][cve-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) + ### πŸ“ˆ Features/Enhancements - [MD] Support legacy client for data source ([#2204](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2204)) @@ -27,12 +39,35 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Vis Builder] Change classname prefix wiz to vb ([#2581](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2581/files)) - [Vis Builder] Change wizard to vis_builder in file names and paths ([#2587](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2587)) - [Windows] Facilitate building and running OSD and plugins on Windows platforms ([#2601](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2601)) -- [Windows] Add helper functions to work around the differences of platforms ([#2681](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2681)) +- [Windows] Add `@osd/cross-platform` package to standardize path handling across platforms ([#2703](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2703)) - [Multi DataSource] Address UX comments on Data source list and create page ([#2625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2625)) - [Vis Builder] Rename wizard to visBuilder in i18n id and formatted message id ([#2635](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2635)) - [Vis Builder] Rename wizard to visBuilder in class name, type name and function name ([#2639](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2639)) - [Vis Builder] Rename wizard on save modal and visualization table ([#2645](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2645)) +- [Vis Builder] Adds functional tests to CI ([#2728](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2728)) +- [Vis Builder] Enable VisBuilder by default ([#2725](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2725)) - Change save object type, wizard id and name to visBuilder #2673 ([#2673](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2673)) +- [Multi DataSource] Update MD data source documentation link ([#2693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2693)) +- [Save Object Aggregation View] Add extension point in saved object management to register namespaces and show filter ([#2656](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2656)) +- [Save Object Aggregation View] Fix for export all after scroll count response changed in PR#2656 ([#2696](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2696)) +- [Vis Builder] Add an experimental table visualization in vis builder ([#2705](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2705)) +- [Vis Builder] Add field summary popovers ([#2682](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2682)) +- [I18n] Register ru, ru-RU locale ([#2817](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2817)) +- Add yarn opensearch arg to setup plugin dependencies ([#2544](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2544)) +- [Multi DataSource] Test the connection to an external data source when creating or updating ([#2973](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2973)) +- [Table Visualization] Refactor table visualization using React and DataGrid component ([#2863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2863)) +- [Vis Builder] Add redux store persistence ([#3088](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3088)) +- [Multi DataSource] Improve test connection ([#3110](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3110)) +- [Vis Builder] Add app filter and query persistence without using state container ([#3100](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3100)) +- [Optimizer] Increase timeout waiting for the exiting of an optimizer worker ([#3193](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3193)) +- [Data] Update `createAggConfig` so that newly created configs can be added to beginning of `aggConfig` array ([#3160](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3160)) +- Add disablePrototypePoisoningProtection configuration to prevent JS client from erroring when cluster utilizes JS reserved words ([#2992](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2992)) +- [Multiple DataSource] Add support for SigV4 authentication ([#3058](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3058)) +- Make build scripts find and use the latest version of Node.js that satisfies `engines.node` ([#3467](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3467)) +- [Multiple DataSource] Refactor test connection to support SigV4 auth type ([#3456](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3456)) +- [Darwin] Add support for Darwin for running OpenSearch snapshots with `yarn opensearch snapshot` ([#3537](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3537)) +- [Vis Builder] Add metric to metric, bucket to bucket aggregation persistence ([#3495](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3495)) +- Use mirrors to download Node.js binaries to escape sporadic 404 errors ([#3619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3619)) ### πŸ› Bug Fixes @@ -40,7 +75,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Vis Builder] Fixes visualization shift when editing agg ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) - [Vis Builder] Renames "Histogram" to "Bar" in vis type picker ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) - [Vis Builder] Update vislib params and misc fixes ([2610](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2610)) -* [Vis Builder] Bug fixes for datasource picker and auto time interval ([2632](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2632)) +- [Vis Builder] Bug fixes for datasource picker and auto time interval ([2632](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2632)) - [MD] Add data source param to low-level search call in Discover ([#2431](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2431)) - [Multi DataSource] Skip data source view in index pattern step when pick default ([#2574](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2574)) - [Multi DataSource] Address UX comments on Edit Data source page ([#2629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2629)) @@ -48,23 +83,90 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multi DataSource] Address UX comments on index pattern management stack ([#2611](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2611)) - [Multi DataSource] Apply get indices error handling in step index pattern ([#2652](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2652)) - [Vis Builder] Last Updated Timestamp for visbuilder savedobject is getting Generated ([#2628](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2628)) +- [Vis Builder] fixes filters for table visualisation ([#3210](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3210)) +- Removed Leftover X Pack references ([#2638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2638)) +- Removes Add Integration button ([#2723](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2723)) +- Change geckodriver version to make consistency ([#2772](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2772)) +- [Multi DataSource] Update default audit log path ([#2793](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2793)) +- [Table Visualization] Fix first column sort issue ([#2828](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2828)) +- Temporary workaround for task-kill exceptions on Windows when it is passed a pid for a process that is already dead ([#2842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2842)) +- [Vis Builder] Fix empty workspace animation does not work in firefox ([#2853](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2853)) +- Bumped `del` version to fix MacOS race condition ([#2847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2873)) +- [Build] Fixed "Last Access Time" not being set by `scanCopy` on Windows ([#2964](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2964)) +- [Vis Builder] Add global data persistence for vis builder #2896 ([#2896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2896)) +- Update `leaflet-vega` and fix its usage ([#3005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3005)) +- [Table Visualization][bug] Fix Url content display issue in table ([#2918](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2918)) +- Fixes misleading embaddable plugin error message ([#3043](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3043)) +- [MD] Update dummy url in tests to follow lychee url allowlist ([#3099](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3099)) +- Adds config override to fix obsolete theme:version config value of v8 (beta) rendering issue ([#3045](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3045)) +- [CI] Update test workflow to increase network-timeout for yarn for installing dependencies ([#3118](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3118)) +- [VisBuilder] Fixes pipeline aggs ([#3137](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3137)) +- [Region Maps] Fixes bug that prevents selected join field to be used ([#3213](Fix bug that prevents selected join field to be used)) +- [Multi DataSource]Update test connection button text([#3247](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3247)) +- [Region Maps] Add ui setting to configure custom vector map's size parameter([#3399](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3399)) +- [Search Telemetry] Fixes search telemetry's observable object that won't be GC-ed([#3390](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3390)) ### 🚞 Infrastructure - Add CHANGELOG.md and related workflows ([#2414](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2414)) +- Update backport custom branch name to utilize head template ([#2766](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2766)) +- Re-enable CI workflows for feature branckes ([#2908](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2908)) +- Add Windows CI workflows ([#2966](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2966)) +- Add automatic selection of the appropriate version of chrome driver to run functional tests ([#2990](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2990)) +- Add recording of functional test artifacts if they fail ([#3190](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3190)) +- Improve yarn's performance in workflows by caching yarn's cache folder ([#3194](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3194)) +- Fix detection of Chrome's version on Darwin during CI ([#3296](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3296)) +- Upgrade yarn version to be compatible with @openearch-project/opensearch ([#3443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3443)) +- [CI] Reduce redundancy by using matrix strategy on Windows and Linux workflows ([#3514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3514)) ### πŸ“ Documentation +- Add the release runbook to RELEASING.md ([#2533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2533)) +- [MD] Add design documents of multiple data source feature [#2538](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2538) +- [MD] Tweak multiple data source design doc [#2724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2724) +- Corrected README and help command of osd-plugin-helpers ([#2810](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2810)) +- Add `current-usage.md` and more details to `README.md` of `charts` plugin ([#2695](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2695)) +- [Doc] Add readme for global query persistence ([#3001](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3001)) +- Updates NOTICE file, adds validation to GitHub CI ([#3051](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3051)) +- [Doc] Add current plugin persistence implementation readme ([#3081](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3081)) +- [Doc] Improve DEVELOPER_GUIDE to make first time setup quicker and easier ([#3421](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3421)) +- Correct copyright date range of NOTICE file and notice generator ([#3308](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3308)) +- Simplify the in-code instructions for upgrading `re2` ([#3328](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3328)) +- [Doc] Add docker dev set up instruction ([#3444](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3444)) +- [Doc] UI actions explorer ([#3614](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3614)) + ### πŸ›  Maintenance - Adding @zhongnansu as maintainer. ([#2590](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2590)) +- Removes `minimatch` manual resolution ([#3019](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3019)) +- Remove `github-checks-reporter`, an unused dependency ([#3126](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3126)) +- Upgrade `vega-lite` dependency to ^5.6.0 ([#3076](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3076)) +- Bumps `re2` and `supertest` ([3018](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3018)) +- Bump `vega-tooltip` version from ^0.24.2 to ^0.30.0 ([#3358](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3358)) +- Allow relaxing the Node.js runtime version requirement ([3402](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3402)) +- Relax the Node.js requirement to `^14.20.1` ([3463](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3463)) +- Bump the version of Node.js installed by `nvm` to `14.21.3` ([3463](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3463)) +- Remove the unused `renovate.json5` file ([3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) ### πŸͺ› Refactoring -* [MD] Refactor data source error handling ([#2661](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2661)) + +- [MD] Refactor data source error handling ([#2661](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2661)) +- Refactor and improve Discover field summaries ([#2391](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2391)) +- [Vis Builder] Removed Hard Coded Strings and Used i18n to transalte([#2867](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2867)) +- [Console] Replace jQuery.ajax with core.http when calling OSD APIs in console ([#3080]https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3080)) ### πŸ”© Tests - [Multi DataSource] Add unit test coverage for Update Data source management stack ([#2567](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2567)) +- [BWC Tests] Add BWC tests for 2.5.0 ([#2890](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2890)) +- Add retrial of flaky tests ([#2967](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2967)) +- Fix incorrect validation of time values in JUnit Reporter ([#2965](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2965)) +- Make tests covering plugin installation on cluster snapshots work across platforms ([#2994](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2994)) +- Correct the linting logic for `no-restricted-path` to ignore trailing slashes ([#3020](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3020)) +- [Tests] Bumps `chromedriver` to v107 ([#3017](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3017)) +- [Vis Builder] Adds field unit tests ([#3211](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3211)) +- [BWC Tests] Add BWC tests for 2.6.0 ([#3356](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3356)) +- Prevent primitive linting limitations from being applied to unit tests found under `src/setup_node_env` ([#3403](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3403)) ## [2.x] @@ -81,6 +183,10 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Resolve sub-dependent d3-color version and potential security issue ([#2454](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2454)) - [CVE-2022-3517] Bumps minimatch from 3.0.4 to 3.0.5 and [IBM X-Force ID: 220063] unset-value from 1.0.1 to 2.0.1 ([#2640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2640)) - [CVE-2022-37601] Bump loader-utils to 2.0.3 ([#2689](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2689)) +- [CVE-2022-37599] Bump loader-utils to 2.0.4 ([#3031](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3031)) +- [CVE-2022-37603] Bump loader-utils to 2.0.4 ([#3031](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3031)) +- [WS-2021-0638][security] bump mocha to 10.1.0 ([#2711](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2711)) +- [CVE-2022-25881] Resolve http-cache-semantics to 4.1.1 ([#3409](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3409)) ### πŸ“ˆ Features/Enhancements @@ -91,6 +197,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multi DataSource] UX enhacement on index pattern management stack ([#2505](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2505)) - [Multi DataSource] UX enhancement on Data source management stack ([#2521](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2521)) - [Multi DataSource] UX enhancement on Update stored password modal for Data source management stack ([#2532](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2532)) +- [Monaco editor] Add json worker support ([#3424](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3424)) +- Enhance grouping for context menus ([#3169](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3169)) ### πŸ› Bug Fixes @@ -98,6 +206,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Viz Builder] Add index pattern info when loading embeddable ([#2363](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2363)) - Fixes management app breadcrumb error ([#2344](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2344)) - [BUG] Fix suggestion list cutoff issue ([#2607](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2607)) +- [TSVB] Fixes undefined serial diff aggregation documentation link ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) ### 🚞 Infrastructure @@ -112,6 +221,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add sample config for multi data source feature in yml template. ([#2428](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2428)) - README.md for dataSource and dataSourceManagement Plugin ([#2448](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2448)) - Updates functionl testing information in Testing.md ([#2492](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2492)) +- Fixes typo in TSVB README ([#3518](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3518)) ### πŸ›  Maintenance @@ -126,6 +236,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### πŸ”© Tests - Update caniuse to fix failed integration tests ([#2322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2322)) +- Update caniuse to 1.0.30001460 to fix failed integration tests ([#3538](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3538)) [unreleased]: https://github.com/opensearch-project/OpenSearch-Dashboards/compare/2.3.0...HEAD [2.x]: https://github.com/opensearch-project/OpenSearch-Dashboards/compare/2.3.0...2.x diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 4dc3e0d2583..ae4344022bd 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -3,76 +3,245 @@ This guide applies to all development within the OpenSearch Dashboards project and is recommended for the development of all OpenSearch Dashboards plugins. -- [Getting Started](#getting-started) -- [General](#general) -- [HTML](#html) -- [API endpoints](#api-endpoints) -- [TypeScript/JavaScript](#typeScript/javaScript) -- [SASS files](#sass-files) -- [React](#react) +- [Getting started guide](#getting-started-guide) + - [Key technologies](#key-technologies) + - [Prerequisites](#prerequisites) + - [Fork and clone OpenSearch Dashboards](#fork-and-clone-opensearch-dashboards) + - [Bootstrap OpenSearch Dashboards](#bootstrap-opensearch-dashboards) + - [Run OpenSearch](#run-opensearch) + - [Run OpenSearch Dashboards](#run-opensearch-dashboards) + - [Next Steps](#next-steps) +- [Alternative development installations](#alternative-development-installations) + - [Optional - Run OpenSearch with plugins](#optional---run-opensearch-with-plugins) + - [Alternative - Run OpenSearch from tarball](#alternative---run-opensearch-from-tarball) + - [Configure OpenSearch Dashboards for security](#configure-opensearch-dashboards-for-security) +- [Building artifacts](#building-artifacts) + - [Building the Docker image](#building-the-docker-image) +- [Code guidelines](#code-guidelines) + - [General](#general) + - [HTML](#html) + - [SASS files](#sass-files) + - [TypeScript/JavaScript](#typescriptjavascript) + - [React](#react) + - [API endpoints](#api-endpoints) -Besides the content in this developer guide, the following developer guides may also apply to all development within the OpenSearch Dashboards project. Please make sure to also read them: +## Getting started guide -- [Accessibility developer guide (EUI Docs)](https://elastic.github.io/eui/#/guidelines/accessibility) -- [SASS developer guide (EUI Docs)](https://elastic.github.io/eui/#/guidelines/sass) +This guide is for any developer who wants a running local development environment where you can make, see, and test changes. It's opinionated to get you running as quickly and easily as possible, but it's not the only way to set up a development environment. -## Getting Started +If you're only interested in installing and running this project, please see the [Installing OpenSearch Dashboards](https://opensearch.org/docs/latest/install-and-configure/install-dashboards) instead. -If you would like to install and run this project, please see the [Downloads Page](https://opensearch.org/downloads.html). +If you're planning to contribute code (features or fixes) to this repository, great! Make sure to also read the [contributing guide](CONTRIBUTING.md). -#### Prerequisites +### Key technologies -We recommend using [Node Version Manager](https://github.com/nvm-sh/nvm) to install -the node version we need. +OpenSearch Dashboards is primarily a Node.js web application built using React. To effectively contribute you should be familiar with HTML ([usage guide](#html)), SASS styling ([usage guide](#sass-files)), TypeScript and JavaScript ([usage guide](#typescriptjavascript)), and React ([usage guide](#react)). + +### Prerequisites + +To develop on OpenSearch Dashboards, you'll need: + +- A [GitHub account](https://docs.github.com/en/get-started/onboarding/getting-started-with-your-github-account) +- [`git`](https://git-scm.com/) for version control +- [`Node.js`](https://nodejs.org/), [`npm`](https://www.npmjs.com/), and [`Yarn`](https://yarnpkg.com/) for building and running the project +- A code editor of your choice, configured for JavaScript/TypeScript. If you don't have a favorite editor, we suggest [Visual Studio Code](https://code.visualstudio.com/) + +If you already have these installed or have your own preferences for installing them, skip ahead to the [Fork and clone OpenSearch Dashboards](#fork-and-clone-opensearch-dashboards) section. + +#### Install `git` + +If you don't already have it installed (check with `git --version`) we recommend following the [the `git` installation guide for your OS](https://git-scm.com/downloads). + +#### Install `node` + +We recommend using [Node Version Manager (nvm)](https://github.com/nvm-sh/nvm) to install and manage different node versions, which may differ between release branches. + +1. Install nvm (as specified by the [`nvm` README](https://github.com/nvm-sh/nvm#installing-and-updating)): `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash` +2. Install the version of the Node.js runtime defined in [`.nvmrc`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/.nvmrc): `nvm install` + +If it's the only version of node installed, it will automatically be set to the `default` alias. Otherwise, use `nvm list` to see all installed `node` versions, and `nvm use` to select the node version required by OpenSearch Dashboards. + +#### Install `yarn` + +```bash +$ npm i -g corepack +``` + +(See the [Yarn installation documentation](https://yarnpkg.com/getting-started/install) for more information.) + +### Fork and clone OpenSearch Dashboards + +All local development should be done in a [forked repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo). +Fork OpenSearch Dashboards by clicking the "Fork" button at the top of the [GitHub repository](https://github.com/opensearch-project/OpenSearch-Dashboards). + +Clone your forked version of OpenSearch Dashboards to your local machine (replace `opensearch-project` in the command below with your GitHub username): + +```bash +$ git clone git@github.com:opensearch-project/OpenSearch-Dashboards.git +``` ### Bootstrap OpenSearch Dashboards -First we need to clone and bootstrap OpenSearch Dashboards: +If you haven't already, change directories to your cloned repository directory: + ```bash -$ git clone https://github.com/opensearch-project/OpenSearch-Dashboards.git $ cd OpenSearch-Dashboards -$ nvm use -$ npm i -g yarn -$ yarn osd bootstrap # This command will also install npm dependencies ``` -### Configure OpenSearch Dashboards +The `osd bootstrap` command will install the project's dependencies and build all internal packages and plugins. Bootstrapping is necessary any time you need to update packages, plugins, or dependencies, and it's recommended to run it anytime you sync with the latest upstream changes. + +```bash +$ yarn osd bootstrap +``` + +Note: If you experience a network timeout while bootstrapping, you can update the timeout by configuring it in the [`.yarnrc`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/.yarnrc). For example: -*This step is only mandatory if you have https/authentication enabled, or if you use the OpenSearch Docker image in its default configuration.* +``` +network-timeout 1000000 +``` -Once the bootstrap of OpenSearch Dashboards is finished, you need to apply some -changes to `opensearch_dashboards.yml` in order to connect to OpenSearch. +If you've previously bootstrapped the project and need to start fresh, first run: -```yml -opensearch.hosts: ["https://localhost:9200"] -opensearch.username: "admin" # Default username on the docker image -opensearch.password: "admin" # Default password on the docker image -opensearch.ssl.verificationMode: none +```bash +$ yarn osd clean ``` ### Run OpenSearch -You need to have an OpenSearch server up and running to be able to run OpenSearch -Dashboards. In a separate terminal you can run the latest snapshot built using: +OpenSearch Dashboards requires a running version of OpenSearch to connect to. In a separate terminal you can run the latest snapshot built using: +_(Linux, Windows, Darwin (MacOS) only - for others, you'll need to [set up using Docker](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/docs/docker-dev/docker-dev-setup-manual.md) or [run OpenSearch from a tarball](#alternative---run-opensearch-from-tarball) instead)_ ```bash -$ yarn opensearch snapshot +$ yarn opensearch snapshot ``` -**Warning:** Starting the Dashboards instance before or during the initialization of the OpenSearch Server can cause Dashboards to sometimes misbehave. Ensure that the OpenSearch server instance is up and running first before starting up the Dashboards dev server from the next step. - ### Run OpenSearch Dashboards -After you've installed OpenSearch Dashboards and configured it, you can start -the development server: +_**Warning:** Starting the OpenSearch Dashboards instance before the OpenSearch server is fully initialized can cause Dashboards to misbehave. Ensure that the OpenSearch server instance is up and running first. You can validate by running `curl localhost:9200` in another console tab or window (see [OpenSearch developer guide](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#run-opensearch))._ + +Start the OpenSearch Dashboards development server: + ```bash $ yarn start ``` -When the server is up and ready, click on the link displayed in your terminal to +When the server is up and ready (the console messages will look something like this), + +``` +[info][listening] Server running at http://localhost:5603/pgt +[info][server][OpenSearchDashboards][http] http server running at http://localhost:5603/pgt +``` + +click on the link displayed in your terminal to access it. -### Building the artifacts +Note - it may take a couple minutes to generate all the necessary bundles. If the Dashboards link is not yet accessible, wait for a log message like + +``` +[success][@osd/optimizer] 28 bundles compiled successfully after 145.9 sec, watching for changes +``` + +### Next Steps + +Now that you have a development environment to play with, there are a number of different paths you may take next. + +#### Learn about the OpenSearch Dashboards architecture and plugins + +- [Introduction to OpenSearch Dashboards Plugins](https://opensearch.org/blog/dashboards-plugins-intro/) blog post +- [OpenSearch Dashboards plugin user documentation](https://opensearch.org/docs/latest/install-and-configure/install-dashboards/plugins/) (install, update, and remove) +- Much of the technical architectural information about the plugin system is located in `/src/core` + - [README](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/README.md) + - [Plugin principles](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/PRINCIPLES.md) + - [Plugin conventions](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/CONVENTIONS.md#technical-conventions) + - [Plugin testing](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/TESTING.md) + +#### Review user tutorials to understand the key features and workflows + +- The [Quickstart guide for OpenSearch Dashboards](https://opensearch.org/docs/latest/dashboards/quickstart-dashboards/) will show you how to explore, inspect, and visualize sample data +- [Running queries in the Dev Tools Console](https://opensearch.org/docs/latest/dashboards/run-queries/) provides helpful guidance on how to interact with OpenSearch + +#### Explore essential plugins and APIs + +The easiest way to understand some of the essential plugins and APIs is to run OpenSearch Dashboards with [developer examples](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/examples/) turned on: + +```bash +$ yarn start --run-examples +``` + +#### Review code guidelines and conventions + +- [Project code guidelines](#code-guidelines) +- [Project testing guidelines](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/TESTING.md) +- [Plugin conventions](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/CONVENTIONS.md#technical-conventions) + +## Alternative development installations + +Although the [getting started guide](#getting-started-guide) covers the recommended development environment setup, there are several alternatives worth being aware of. + +### Optional - Run OpenSearch with plugins + +By default, the snapshot command will run [a minimal distribution of OpenSearch](https://opensearch.org/downloads.html#minimal), with no plugins installed. + +If you would like to run OpenSearch with a particular plugin installed on the cluster snapshot, pass the `--P` flag after `yarn opensearch snapshot`. You can use the flag multiple times to install multiple plugins. The argument value can be a URL to the plugin's zip file, maven coordinates of the plugin, or a local zip file path (use `file://` followed by the absolute or relative path, in that case). For example: + +_(Linux, Windows, Darwin (MacOS) only - for others, you'll need to [run OpenSearch from a tarball](#alternative---run-opensearch-from-tarball) instead)_ +```bash +$ yarn opensearch snapshot --P https://repo1.maven.org/maven2/org/opensearch/plugin/opensearch-test-plugin/2.4.0.0/opensearch-test-plugin-2.4.0.0.zip +``` + +Note - if you add the [`security` plugin](https://github.com/opensearch-project/security), you'll also need to [configure OpenSearch Dashboards for security](#configure-opensearch-dashboards-for-security). + +#### Other snapshot configuration options + +Additional options can be passed after `yarn opensearch snapshot` to further configure the cluster snapshot. + +Options: + + --license Run with a 'oss', 'basic', or 'trial' license [default: oss] + --version Version of OpenSearch to download [default: 3.0.0}] + --base-path Path containing cache/installations [default: /home/ubuntu/OpenSearch-Dashboards/.opensearch] + --install-path Installation path, defaults to 'source' within base-path + --data-archive Path to zip or tarball containing an OpenSearch data directory to seed the cluster with. + --password Sets password for opensearch user [default: changeme] + -E Additional key=value settings to pass to OpenSearch + --download-only Download the snapshot but don't actually start it + --ssl Sets up SSL on OpenSearch + --P OpenSearch plugin artifact URL to install it on the cluster. + +```bash +$ yarn opensearch snapshot --version 2.2.0 -E cluster.name=test -E path.data=/tmp/opensearch-data --P org.opensearch.plugin:test-plugin:2.2.0.0 --P file:/home/user/opensearch-test-plugin-2.2.0.0.zip +``` + +### Alternative - Run OpenSearch from tarball + +If you would like to run OpenSearch from the tarball, you'll need to download the minimal distribution, install it, and then run the executable. (You'll also need Java installed and the `JAVA_HOME` environmental variable set - see [OpenSearch developer guide](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#install-prerequisites) for details). + +1. Download the latest minimal distribution of OpenSearch from [the downloads page](https://opensearch.org/downloads.html#minimal). Note the version and replace in commands below. +2. Unzip the `tar.gz` file: `tar -xvf opensearch--linux-x64.tar.gz` +3. Change directory: `cd opensearch-` +4. Run the cluster: `./bin/opensearch` + +Note - OpenSearch and OpenSearch Dashboards must have matching version strings. Because the tarball is the latest _released_ version of OpenSearch, it's likely behind the version on the `main` branch of OpenSearch Dashboards, which is generally set to the next upcoming major release. So to work from main, update [the OpenSearch Dashboards version in `package.json`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/package.json#L14) to match the OpenSearch version running. + +This method can also be used to develop against the [full distribution of OpenSearch](https://opensearch.org/downloads.html#opensearch) instead. In that case, you'll also need to [configure OpenSearch Dashboards for security](#configure-opensearch-dashboards-for-security). + +### Configure OpenSearch Dashboards for security + +*This step is only mandatory if you have the [`security` plugin](https://github.com/opensearch-project/security) installed on your OpenSearch cluster with https/authentication enabled.* + +Once the bootstrap of OpenSearch Dashboards is finished, you need to apply some +changes to the default [`opensearch_dashboards.yml`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/config/opensearch_dashboards.yml#L25-L72) in order to connect to OpenSearch. + +```yml +opensearch.hosts: ["https://localhost:9200"] +opensearch.username: "admin" # Default username on the docker image +opensearch.password: "admin" # Default password on the docker image +opensearch.ssl.verificationMode: none +``` + +For more detailed documentation, see [Configure TLS for OpenSearch Dashboards](https://opensearch.org/docs/latest/install-and-configure/install-dashboards/tls). + +## Building artifacts To build the artifacts for all supported platforms, run the following: @@ -86,12 +255,13 @@ If you want to build a specific platform, pass the platform flag after `yarn bui yarn build-platform --darwin ``` -You could pass one or multiple flags. If you don't pass any flag, `yarn build-platform` will build an artifact based on your local environment. +You could pass one or multiple flags. If you don't pass any flag, `yarn build-platform` will build an artifact based on your local environment. Currently, the supported flags for this script are: * `darwin` (builds Darwin x64) * `linux` (builds Linux x64) -* `linux-arm` (builds Linux ARM64). +* `linux-arm` (builds Linux ARM64). +* `windows` (builds Windows x64) If you would like to build only a DEB x64 artifact, run the following: @@ -117,7 +287,7 @@ If you would like to build only a RPM ARM64 artifact, run the following: yarn build --rpm-arm --skip-archives ``` -### Building the Docker Image +### Building the Docker image To build the Docker image, run the following: @@ -126,9 +296,11 @@ yarn osd bootstrap yarn build --docker ``` -## General +## Code guidelines + +### General -### Filenames +#### Filenames All filenames should use `snake_case`. @@ -136,12 +308,12 @@ All filenames should use `snake_case`. **Wrong:** `src/opensearch-dashboards/IndexPatterns/IndexPattern.js` -### Do not comment out code +#### Do not comment out code We use a version management system. If a line of code is no longer needed, remove it, don't simply comment it out. -### Prettier and Linting +#### Prettier and linting We are gradually moving the OpenSearch Dashboards code base over to Prettier. All TypeScript code and some JavaScript code (check `.eslintrc.js`) is using Prettier to format code. You @@ -153,11 +325,11 @@ Consider every linting rule and every Prettier rule to be also part of our devel and disable them only in exceptional cases and ideally leave a comment why they are disabled at that specific place. -## HTML +### HTML This part contains developer guide rules around general (framework agnostic) HTML usage. -### Camel case `id` and `data-test-subj` +#### Camel case `id` and `data-test-subj` Use camel case for the values of attributes such as `id` and `data-test-subj` selectors. @@ -181,14 +353,14 @@ buttons.map(btn => ( ) ``` -### Capitalization in HTML and CSS should always match +#### Capitalization in HTML and CSS should always match It's important that when you write CSS/SASS selectors using classes, IDs, and attributes (keeping in mind that we should _never_ use IDs and attributes in our selectors), that the capitalization in the CSS matches that used in the HTML. HTML and CSS follow different case sensitivity rules, and we can avoid subtle gotchas by ensuring we use the same capitalization in both of them. -### How to generate ids? +#### How to generate ids? When labeling elements (and for some other accessibility tasks) you will often need ids. Ids must be unique within the page i.e. no duplicate ids in the rendered DOM @@ -241,45 +413,50 @@ id generator. You can also use this service outside of React. -## API endpoints +### SASS files -The following developer guide rules are targeting development of server side API endpoints. +When writing a new component, create a sibling SASS file of the same name and import directly into the **top** of the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). -### Paths +All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & OpenSearch Dashboards invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/core/public/core_app/styles/_globals_v7light.scss). -API routes must start with the `/api/` path segment, and should be followed by the plugin id if applicable: +While the styles for this component will only be loaded if the component exists on the page, +the styles **will** be global and so it is recommended to use a three letter prefix on your +classes to ensure proper scope. -**Right:** `/api/marvel/nodes` +**Example:** -**Wrong:** `/marvel/api/nodes` +```tsx +// component.tsx -### snake_case +import './component.scss'; +// All other imports below the SASS import -OpenSearch Dashboards uses `snake_case` for the entire API, just like OpenSearch. All urls, paths, query string parameters, values, and bodies should be `snake_case` formatted. +export const Component = () => { + return ( +
+ ); +} +``` -_Right:_ +```scss +// component.scss +.plgComponent { ... } ``` -POST /api/opensearch-dashboards/index_patterns -{ - "id": "...", - "time_field_name": "...", - "fields": [ - ... - ] -} -``` -## TypeScript/JavaScript +Do not use the underscore `_` SASS file naming pattern when importing directly into a javascript file. + + +### TypeScript/JavaScript The following developer guide rules apply for working with TypeScript/JavaScript files. -### TypeScript vs. JavaScript +#### TypeScript vs. JavaScript Whenever possible, write code in TypeScript instead of JavaScript, especially if it's new code. Check out [TYPESCRIPT.md](TYPESCRIPT.md) for help with this process. -### Prefer modern JavaScript/TypeScript syntax +#### Prefer modern JavaScript/TypeScript syntax You should prefer modern language features in a lot of cases, e.g.: @@ -290,7 +467,7 @@ You should prefer modern language features in a lot of cases, e.g.: - Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` - Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) -### Avoid mutability and state +#### Avoid mutability and state Wherever possible, do not rely on mutable state. This means you should not reassign variables, modify object properties, or push values to arrays. @@ -310,7 +487,7 @@ function addBar(foos, foo) { } ``` -### Avoid `any` whenever possible +#### Avoid `any` whenever possible Since TypeScript 3.0 and the introduction of the [`unknown` type](https://mariusschulz.com/blog/the-unknown-type-in-typescript) there are rarely any @@ -324,7 +501,7 @@ If you’re not having `any` in your plugin or are starting a new plugin, you sh [`@typescript-eslint/no-explicit-any`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md) linting rule for your plugin via the [`.eslintrc.js`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/.eslintrc.js) config. -### Avoid non-null assertions +#### Avoid non-null assertions You should try avoiding non-null assertions (`!.`) wherever possible. By using them you tell TypeScript, that something is not null even though by it’s type it could be. Usage of non-null @@ -336,14 +513,14 @@ or using [user defined type guards](https://www.typescriptlang.org/docs/handbook to properly tell TypeScript what type a variable has. Using non-null assertion increases the risk for future bugs. In case the condition under which we assumed that the -variable can’t be null has changed (potentially even due to changes in compeltely different files), the non-null +variable can’t be null has changed (potentially even due to changes in completely different files), the non-null assertion would now wrongly disable proper type checking for us. If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the [`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) linting rule for you plugin in the [`.eslintrc.js`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/.eslintrc.js) config. -### Return/throw early from functions +#### Return/throw early from functions To avoid deep nesting of if-statements, always return a function's value as early as possible. And where possible, do any assertions first: @@ -376,7 +553,7 @@ function doStuff(val) { } ``` -### Use object destructuring +#### Use object destructuring This helps avoid temporary references and helps prevent typo-related bugs. @@ -400,7 +577,7 @@ function fullName(user) { } ``` -### Use array destructuring +#### Use array destructuring Directly accessing array values via index should be avoided, but if it is necessary, use array destructuring: @@ -416,7 +593,7 @@ const first = arr[0]; const second = arr[1]; ``` -### Magic numbers/strings +#### Avoid magic numbers/strings These are numbers (or other values) simply used in line in your code. _Do not use these_, give them a variable name so they can be understood and changed @@ -436,7 +613,7 @@ if (width < 300) { } ``` -### Modules +#### Use native ES2015 module syntax Module dependencies should be written using native ES2015 syntax wherever possible (which is almost everywhere): @@ -462,7 +639,7 @@ file that does not pass run through webpack, then use CommonJS modules. In those even rarer cases where you're writing client-side code that does not run through webpack, then do not use a module loader at all. -#### Import only top-level modules +##### Import only top-level modules The files inside a module are implementation details of that module. They should never be imported directly. Instead, you must only import the top-level @@ -488,12 +665,12 @@ import inFoo from 'foo/child'; import inSibling from '../foo/child'; ``` -### Global definitions +#### Avoid global definitions Don't do this. Everything should be wrapped in a module that can be depended on by other modules. Even things as simple as a single value should be a module. -### Only use ternary operators for small, simple code +#### Use ternary operators only for small, simple code And _never_ use multiple ternaries together, because they make it more difficult to reason about how different values flow through the conditions @@ -507,7 +684,7 @@ const foo = a === b ? 1 : 2; const foo = a === b ? 1 : a === c ? 2 : 3; ``` -### Use descriptive conditions +#### Use descriptive conditions Any non-trivial conditions should be converted to functions or assigned to descriptively named variables. By breaking up logic into smaller, @@ -540,7 +717,7 @@ if (thing instanceof Shape && !(thing instanceof Square)) { } ``` -### Name regular expressions +#### Name regular expressions ```js // good @@ -556,13 +733,13 @@ if (password.length >= 4 && /^(?=.*\d).{4,}$/.test(password)) { } ``` -### Write small functions +#### Write small functions Keep your functions short. A good function fits on a slide that the people in the last row of a big room can comfortably read. So don't count on them having perfect vision and limit yourself to ~15 lines of code per function. -### Use "rest" syntax rather than built-in `arguments` +#### Use "rest" syntax rather than built-in `arguments` For expressiveness sake, and so you can be mix dynamic and explicit arguments. @@ -579,7 +756,7 @@ function something(foo) { } ``` -### Default argument syntax +#### Use default argument syntax Always use the default argument syntax for optional arguments. @@ -612,7 +789,7 @@ function foo(options = {}, bar) { } ``` -### Use thunks to create closures, where possible +#### Use thunks to create closures, where possible For trivial examples (like the one that follows), thunks will seem like overkill, but they encourage isolating the implementation details of a closure @@ -638,7 +815,7 @@ setTimeout(() => { }, 1000); ``` -### Use slashes for comments +#### Use slashes for comments Use slashes for both single line and multi line comments. Try to write comments that explain higher level mechanisms or clarify difficult @@ -690,7 +867,7 @@ if (isSessionValid) { } ``` -### Getters and Setters +#### Use getters but not setters Feel free to use getters that are free from [side effects][sideeffect], like providing a length property for a collection class. @@ -699,44 +876,18 @@ Do not use setters, they cause more problems than they can solve. [sideeffect]: http://en.wikipedia.org/wiki/Side_effect_(computer_science) -## SASS files - -When writing a new component, create a sibling SASS file of the same name and import directly into the **top** of the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). - -All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & OpenSearch Dashboards invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/core/public/core_app/styles/_globals_v7light.scss). - -While the styles for this component will only be loaded if the component exists on the page, -the styles **will** be global and so it is recommended to use a three letter prefix on your -classes to ensure proper scope. - -**Example:** - -```tsx -// component.tsx - -import './component.scss'; -// All other imports below the SASS import - -export const Component = () => { - return ( -
- ); -} -``` - -```scss -// component.scss +#### Attribution -.plgComponent { ... } -``` - -Do not use the underscore `_` SASS file naming pattern when importing directly into a javascript file. +Parts of the JavaScript developer guide were initially forked from the +[node style guide](https://github.com/felixge/node-style-guide) created by [Felix GeisendΓΆrfer](http://felixge.de/) which is +licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) +license. -## React +### React The following developer guide rules are specific for working with the React framework. -### Prefer reactDirective over react-component +#### Prefer reactDirective over react-component When using `ngReact` to embed your react components inside Angular HTML, prefer the `reactDirective` service over the `react-component` directive. @@ -760,7 +911,7 @@ Using `react-component` means adding a bunch of components into angular, while ` ``` -### Action function names and prop function names +#### Name action functions and prop functions appropriately Name action functions in the form of a strong verb and passed properties in the form of on. E.g: @@ -769,9 +920,31 @@ Name action functions in the form of a strong verb and passed properties in the ``` -## Attribution +### API endpoints -Parts of the JavaScript developer guide were initially forked from the -[node style guide](https://github.com/felixge/node-style-guide) created by [Felix GeisendΓΆrfer](http://felixge.de/) which is -licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) -license. +The following developer guide rules are targeting development of server side API endpoints. + +#### Use only `/api/` as base path + +API routes must start with the `/api/` path segment, and should be followed by the plugin id if applicable: + +**Right:** `/api/marvel/nodes` + +**Wrong:** `/marvel/api/nodes` + +#### Use snake_case + +OpenSearch Dashboards uses `snake_case` for the entire API, just like OpenSearch. All urls, paths, query string parameters, values, and bodies should be `snake_case` formatted. + +_Right:_ + +``` +POST /api/opensearch-dashboards/index_patterns +{ + "id": "...", + "time_field_name": "...", + "fields": [ + ... + ] +} +``` diff --git a/Dockerfile b/Dockerfile index d5fddcd2c6c..8f2742a6c1f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_VERSION=14.20.0 +ARG NODE_VERSION=14.20.1 FROM node:${NODE_VERSION} AS base ENV HOME '.' @@ -13,7 +13,7 @@ RUN apt-get update && \ # Specify the version of Chrome that matches the version of chromedriver in the package.json. # A list of Chrome versions can be found here: # https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable -ARG CHROME_VERSION=100.0.4896.127-1 +ARG CHROME_VERSION=107.0.5304.121-1 RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ && wget -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \ && apt-get update \ diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3c01b79dd91..717903ef291 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,25 +1,27 @@ -# OpenSearch-Dashboards Maintainers +## Overview -## Maintainers -| Maintainer | GitHub ID | Affiliation | -| --------------- | --------- | ----------- | -| Anan | [ananzh](https://github.com/ananzh) | Amazon | -| Bishoy Boktor | [boktorbb-amzn](https://github.com/boktorbb-amzn) | Amazon | -| Mihir Soni | [mihirsoni](https://github.com/mihirsoni) | Amazon | -| Rocky | [kavilla](https://github.com/kavilla) | Amazon | -| Sean Neumann | [seanneumann](https://github.com/seanneumann) | Amazon | -| Miki Barahmand | [AMoo-Miki](https://github.com/AMoo-Miki) | Amazon | -| Ashwin P Chandran | [ashwin-pc](https://github.com/ashwin-pc) | Amazon | -| Josh Romero | [joshuarrrr](https://github.com/joshuarrrr) | Amazon | -| Abby Hu | [abbyhu2000](https://github.com/abbyhu2000) | Amazon | -| Yan Zeng | [zengyan-amazon](https://github.com/zengyan-amazon) | Amazon | -| Kristen Tian | [kristenTian](https://github.com/kristenTian) | Amazon | -| Zhongnan Su | [zhongnansu](https://github.com/zhongnansu) | Amazon | +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). -## Emeritus +## Current Maintainers + +| Maintainer | GitHub ID | Affiliation | +| ------------------------- | --------------------------------------------------- | ----------- | +| Anan Zhuang | [ananzh](https://github.com/ananzh) | Amazon | +| Bishoy Boktor | [boktorbb](https://github.com/boktorbb) | Amazon | +| Mihir Soni | [mihirsoni](https://github.com/mihirsoni) | Amazon | +| Kawika (Rocky) Avilla | [kavilla](https://github.com/kavilla) | Amazon | +| Sean Neumann | [seanneumann](https://github.com/seanneumann) | Amazon | +| Miki Barahmand | [AMoo-Miki](https://github.com/AMoo-Miki) | Amazon | +| Ashwin P Chandran | [ashwin-pc](https://github.com/ashwin-pc) | Amazon | +| Josh Romero | [joshuarrrr](https://github.com/joshuarrrr) | Amazon | +| Abby Hu | [abbyhu2000](https://github.com/abbyhu2000) | Amazon | +| Yan Zeng | [zengyan-amazon](https://github.com/zengyan-amazon) | Amazon | +| Kristen Tian | [kristenTian](https://github.com/kristenTian) | Amazon | +| Zhongnan Su | [zhongnansu](https://github.com/zhongnansu) | Amazon | +| Manasvini B Suryanarayana | [manasvinibs](https://github.com/manasvinibs) | Amazon | -| Maintainer | GitHub ID | Affiliation | -| --------------- | --------- | ----------- | -| Tommy Markley | [tmarkley](https://github.com/tmarkley) | Amazon | +## Emeritus -[This document](https://github.com/opensearch-project/.github/blob/main/MAINTAINERS.md) explains what maintainers do, and how they should be doing it. If you're interested in contributing, see [CONTRIBUTING](CONTRIBUTING.md). +| Maintainer | GitHub ID | Affiliation | +| ------------- | --------------------------------------- | ----------- | +| Tommy Markley | [tmarkley](https://github.com/tmarkley) | Amazon | diff --git a/NOTICE.txt b/NOTICE.txt index edd87ef8de7..34da5d0a6f4 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,7 +1,15 @@ -OpenSearch -Copyright 2021 OpenSearch Contributors -This product includes software, including Kibana source code, developed by Elasticsearch (http://www.elastic.co). -Copyright 2012-2021 Elasticsearch B.V. +OpenSearch (https://opensearch.org/) +Copyright OpenSearch Contributors + +This product includes software, including Kibana source code, +developed by Elasticsearch (http://www.elastic.co). +Copyright 2009-2021 Elasticsearch B.V. + +This product includes software developed by The Apache Software +Foundation (http://www.apache.org/) + +This product includes software developed by +Joda.org (http://www.joda.org/). --- Pretty handling of logarithmic axes. Copyright (c) 2007-2014 IOLA and Ole Laursen. @@ -115,10 +123,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -This product uses Noto fonts that are licensed under the SIL Open -Font License, Version 1.1. - --- Based on the scroll-into-view-if-necessary module from npm https://github.com/stipsan/compute-scroll-into-view/blob/master/src/index.ts#L269-L340 @@ -145,70 +149,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Detection Rules -Copyright 2020 Elasticsearch B.V. - ---- -This product bundles rules based on https://github.com/BlueTeamLabs/sentinel-attack -which is available under a "MIT" license. The files based on this license are: - -- defense_evasion_via_filter_manager -- discovery_process_discovery_via_tasklist_command -- persistence_priv_escalation_via_accessibility_features -- persistence_via_application_shimming -- defense_evasion_execution_via_trusted_developer_utilities - -MIT License - -Copyright (c) 2019 Edoardo Gerosa, Olaf Hartong - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- -This product bundles rules based on https://github.com/FSecureLABS/leonidas -which is available under a "MIT" license. The files based on this license are: - -- credential_access_secretsmanager_getsecretvalue.toml - -MIT License - -Copyright (c) 2020 F-Secure LABS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - --- MIT License @@ -306,43 +246,3 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---- -This product includes code in the function applyCubicBezierStyles that was -inspired by a public Codepen, which was available under a "MIT" license. - -Copyright (c) 2020 by Guillaume (https://codepen.io/guillaumethomas/pen/xxbbBKO) -MIT License http://www.opensource.org/licenses/mit-license - ---- -This product includes code that is adapted from mapbox-gl-js, which is -available under a "BSD-3-Clause" license. -https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js - -Copyright (c) 2016, Mapbox - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of Mapbox GL JS nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/RELEASING.md b/RELEASING.md index 50bb965b8d5..6be9bfc2b9e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,3 +1,76 @@ ## Releasing -This project follows [OpenSearch project branching, labelling, and releasing](https://github.com/opensearch-project/.github/blob/main/RELEASING.md). \ No newline at end of file +This project follows [OpenSearch project branching, labelling, and releasing](https://github.com/opensearch-project/.github/blob/main/RELEASING.md). + +## Runbook + +### Overview + +The OpenSearch project releases versioned distributions of OpenSearch, OpenSearch Dashboards, and the OpenSearch plugins. This runbook details the steps involved in performing major, minor, and patch version releases for the OpenSearch Dashboards project; these steps need to be completed by the release managers (RM) assigned to each release. The RM is also responsible for updating the release status on the release tracking issues maintained on GitHub ([example](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2230)). + +\*Important Dates: https://opensearch.org/releases.html + +### Release Phase 1 - Preparation + +For major and minor releases, the OpenSearch build repository [maintainers](https://github.com/opensearch-project/opensearch-build/blob/main/MAINTAINERS.md) will create a release issue in the OpenSearch Dashboards repository ([example](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2230)) which links to the overall issue in the OpenSearch build repository ([example](https://github.com/opensearch-project/opensearch-build/issues/2447)). For patch releases, they will only create the overall issue in the OpenSearch build repository ([example](https://github.com/opensearch-project/opensearch-build/issues/2650)). + +The OpenSearch Dashboards release issue will be assigned to the RM who should prepare for the release by reviewing all listed tasks. They should also compare the current release issue to the issue of the previous release version to ensure that all new processes have been captured. + +The RM should review the [public roadmap](https://github.com/orgs/opensearch-project/projects/1) and confirm the release scope with other OpenSearch Dashboards [maintainers](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/MAINTAINERS.md) as well as the feature owners. Since release labels are intended to highlight the features and fixes meant for the upcoming release, the RM should verify that all issues and pull requests are labelled accordingly. For example, if current release version was v2.3.0, all features not ready for the release should be labeled as v2.4.0 or later by discussing with the feature owners. The RM should also check all PRs for the current release version to confirm that they are merged into the `main` branch and their backported PRs are merged with all CI passing. + +#### How to validate that merged commits have been properly backported? + +1. For any PRs merged to main, make sure it has backport labels ([example - Add v2.3.0 release notes](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2318)) +2. Backport PR is generated automatically by [opensearch-trigger-bot](https://github.com/apps/opensearch-trigger-bot) + ([example - [Backport 2.3] Add v2.3.0 release notes](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2333)) +3. If the backport PR is not generated by opensearch-trigger-bot after 24 hours, create it manually following this [instruction](https://github.com/opensearch-project/.github/blob/main/RELEASING.md#backporting) +4. Ensure that all CI passed and it has two approvals. Then merge the PR. + +#### Prepare BWC data and update BWC versions + +Backward Compatibility Tests (BWC) are cypress tests that identify any obvious compatibility bugs with previous versions. The RM should generate test data and test locally following instructions [here](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/TESTING.md#backwards-compatibility-tests) and cut PR to include both generated data and version upgrade for automated build. (See example [PR](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2393/files)) + +#### Cut release branch for major / minor release + +For major / minor release, the RM should cut the release branch from the parent branch, [following OpenSearch project branching](https://github.com/opensearch-project/.github/blob/main/RELEASING.md#opensearch-branching) + +### Release Phase 2 - Pre-Release + +The release process for OpenSearch is centralized. Jenkins workflows are in place to regularly find differences in the OpenSearch and OpenSearch Dashboards components and create new snapshots for those that have been updated. The RM should update the release branch version in the distribution manifest (see example [PR](https://github.com/opensearch-project/opensearch-build/pull/2586/files)) and increment the parent branch version (see example [PR](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2295/files)). + +#### Write release notes + +OpenSearch Dashboards maintains a [CHANGELOG.md](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CHANGELOG.md) and verifies it as part of the PR checklist step. For the time being, the RM should create release notes PR with the label `doc`, referring to the `CHANGELOG.md` (see example [PR](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2318)) + +### Release Phase 3 - Release testing + +#### Verify integration and BWC test results + +The automated integration test and BWC test are executed concurrently with the release artifacts build. The RM should examine the test results and assist in triaging the broken test case. + +Example build: + +x64: https://build.ci.opensearch.org/job/integ-test-opensearch-dashboards/995/ + +arm64: https://build.ci.opensearch.org/job/integ-test-opensearch-dashboards/996/ + +Example test results: +https://opensearch-project.github.io/opensearch-dashboards-functional-test/site/?version=2.3.0&build_number=4104&test_number=996 + +Note: change `arch` to match the operating system CPU architecture for which the build is being produced. + +#### Sanity test with tarball and docker image + +Once the release candidate artifacts are built, the RM should configure the OpenSearch cluster with OpenSearch Dashboards according to the [instructions in the OpenSearch build repo](https://github.com/opensearch-project/opensearch-build/issues/2447#issuecomment-1241406594) and produce sanity tests to identify broken functionalities caused by new features / code changes. If you find any, please file bug reports and assist in triaging the bugfix. + +### Release Phase 4 - Release Announcement + +Release artifacts and announcements will be available on https://opensearch.org/releases.html. Any website documentation changes will require a PR on the [OpenSearch documentation-website repo.](https://github.com/opensearch-project/documentation-website) + +### Release Phase 5 - Post-Release + +After a release is announced, OpenSearch build repository maintainers will trigger a job that creates a tag in each repository based on the commit hash and branch that was included in the release; the release tag could take a few hours to show up on GitHub. + +The RM should update the [release page](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/) with the latest download URL and release notes after the release tag is created. + +If needed, the RM could conduct a retrospective review of the release, and publish their findings regarding any missed steps and process improvements. diff --git a/SECURITY.md b/SECURITY.md index 0b85ca04ed2..f450e11235b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,27 @@ ## Reporting a Vulnerability -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do **not** create a public GitHub issue. \ No newline at end of file +- If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do **not** create a public GitHub issue. + +- For Security-CVE related fix - + - For direct dependency - Use ```yarn upgrade package``` to update the package and in order to enforce as sub-deps please add nested-dep step2. + + - For nested dependency/sub-deps - In order to enforce package above Vx.y.z, we can add version in the resolutions [section](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/) for all the package sub-deps or specific package sub-dep. For more on version updates please see +[Why](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/#toc-why-would-you-want-to-do-this) and [How](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/#toc-how-to-use-it) to upgrade. + - To add the CVEs fix to previous versions, add label ex: backport 1.x. + + ``` + Example: foobar@1.x vulnerable package and 1.y is the fix + step 1: + For direct dependency checks: + run: yarn upgrade foobar@1.y to update the package.json + and yarn install to update the yarn.lock file + Step 2. + Check for sub deps foobar in other package. + If foobar@1.x exists for subdeps in yarn.lock file + Then edit the package.json file and add **/foobar@1.y in resolution section as shown below to enforce the 1.y. + 'resolutions': { "**/foobar": "^1.y", + "**/foo": "^2.x" , + "**/bar": "^3.k"} + Then run: yarn install for updating yarn.lock file + + \ No newline at end of file diff --git a/TESTING.md b/TESTING.md index 68077547ef8..9cca21b7eee 100644 --- a/TESTING.md +++ b/TESTING.md @@ -24,7 +24,7 @@ In general, we recommend four tiers of tests: # Requirements * Install the latest NodeJS, [NPM](https://www.npmjs.com/get-npm) and [Yarn](https://classic.yarnpkg.com/en/docs/install/#mac-stable) - * `nvm install v14.20.0` + * `nvm install v14.20.1` * `npm install -g yarn` # Running tests diff --git a/bwctest.sh b/bwctest.sh index 81b7569731c..1b44d15b64c 100755 --- a/bwctest.sh +++ b/bwctest.sh @@ -13,7 +13,7 @@ set -e -DEFAULT_VERSIONS="osd-2.0.0,osd-2.1.0,osd-2.2.0,osd-2.3.0" +DEFAULT_VERSIONS="osd-2.0.0,osd-2.1.0,osd-2.2.0,osd-2.3.0,osd-2.4.0,osd-2.5.0" function usage() { echo "" diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index d68c77ad849..6bb4f63bbbd 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -107,6 +107,11 @@ # Logs queries sent to OpenSearch. Requires logging.verbose set to true. #opensearch.logQueries: false +# Disables errors from the OpenSearch JS client and enables you to utilize protected words such as: 'boolean', 'proto', 'constructor'. +# within cluster. By default, OpenSearch Dashboards and the client will protect you against prototype poisoning attacks. +# WARNING: Index patterns are user-supplied data. Disabling this will place the expectation that you are handling the data safely. +#opensearch.disablePrototypePoisoningProtection: false + # Specifies the path where OpenSearch Dashboards creates the process ID file. #pid.file: /var/run/opensearchDashboards.pid @@ -220,7 +225,7 @@ # for reducing the load of OpenSearch cluster. # data.search.usageTelemetry.enabled: false -# Set the value of this setting to true to start exploring wizard +# Set the value of this setting to false to disable VisBuilder # functionality in Visualization. # vis_builder.enabled: false diff --git a/cypress/test-data/with-security/osd-2.4.0.tar.gz b/cypress/test-data/with-security/osd-2.4.0.tar.gz new file mode 100644 index 00000000000..36ef6f417b0 Binary files /dev/null and b/cypress/test-data/with-security/osd-2.4.0.tar.gz differ diff --git a/cypress/test-data/with-security/osd-2.5.0.tar.gz b/cypress/test-data/with-security/osd-2.5.0.tar.gz new file mode 100644 index 00000000000..8ad128f0efc Binary files /dev/null and b/cypress/test-data/with-security/osd-2.5.0.tar.gz differ diff --git a/cypress/test-data/without-security/osd-2.4.0.tar.gz b/cypress/test-data/without-security/osd-2.4.0.tar.gz new file mode 100644 index 00000000000..6cc82c6072b Binary files /dev/null and b/cypress/test-data/without-security/osd-2.4.0.tar.gz differ diff --git a/cypress/test-data/without-security/osd-2.5.0.tar.gz b/cypress/test-data/without-security/osd-2.5.0.tar.gz new file mode 100644 index 00000000000..16c8ccc2e03 Binary files /dev/null and b/cypress/test-data/without-security/osd-2.5.0.tar.gz differ diff --git a/dev-tools/get-version.sh b/dev-tools/get-version.sh deleted file mode 100755 index 555dd89e9e9..00000000000 --- a/dev-tools/get-version.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Copyright OpenSearch Contributors -# SPDX-License-Identifier: Apache-2.0 - -set -e - -PACKAGE_VERSION=$(cat package.json \ -| grep version \ -| head -1 \ -| awk -F: '{ print $2 }' \ -| sed 's/[",]//g' \ -| tr -d [:space:]) - -echo "$PACKAGE_VERSION" \ No newline at end of file diff --git a/dev-tools/install-docker-dev.sh b/dev-tools/install-docker-dev.sh new file mode 100644 index 00000000000..0d40d12afed --- /dev/null +++ b/dev-tools/install-docker-dev.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +{ # this ensures the entire script is downloaded # + +osd_docker_dev_install_dir(){ + printf "opensearch-dashboards-docker-dev" +} + +osd_download(){ + curl --fail --compressed -q "$@" +} + +osd_do_copy_dev_docker_files(){ + local INSTALL_DIR + INSTALL_DIR="$(osd_docker_dev_install_dir)" + local ENTRYPOINT_SRC + ENTRYPOINT_SRC="https://raw.githubusercontent.com/opensearch-project/OpenSearch-Dashboards/main/docs/docker-dev/entrypoint.sh" + local DOCKER_COMPOSE_SRC + DOCKER_COMPOSE_SRC="https://raw.githubusercontent.com/opensearch-project/OpenSearch-Dashboards/main/docs/docker-dev/docker-compose.yml" + + mkdir -p "$INSTALL_DIR" + osd_download -s "$ENTRYPOINT_SRC" -o "./$INSTALL_DIR/entrypoint.sh" + osd_download -s "$DOCKER_COMPOSE_SRC" -o "./$INSTALL_DIR/docker-compose.yml" +} + +osd_do_copy_dev_docker_files + +} # this ensures the entire script is downloaded # diff --git a/docs/charts/current_usage.md b/docs/charts/current_usage.md new file mode 100644 index 00000000000..f3e7db43595 --- /dev/null +++ b/docs/charts/current_usage.md @@ -0,0 +1,184 @@ +# Usage + +The purpose of this doc is to keep track of the current (as of 2022-11-14) usage of the [charts plugin](../../src/plugins/charts/), as well as other packages and tools with similar purposes. See https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2830 for more information on future plans. + +## Lifecycle methods/services + +### `ChartsPluginStart` + +1. `ChartsPluginStart['theme']` used by `discover` plugin to fetch `chartsTheme` and `chartsBaseTheme` for use in styling the histogram. + +### `ChartsPluginSetup` + +1. `ChartsPluginSetup` declared as one of `MetricVisPluginSetupDependencies` in the `vis_type_metric` plugin, but isn't actually used. +2. `ChartsPluginSetup['colors']` used by `vis_type_tagcloud` plugin. Only the seed colors are used via `d3.scale.ordinal().range(colors.seedColors)` +3. `ChartsPluginSetup.colors` and `ChartsPluginSetup.theme` used by the `vis_type_timeseries` plugin. + 1. `themeService.useChartsBaseTheme()` is used only as a fallback; otherwise theme (dark or light) is calculated from the user-specified background color + 2. `colors.mappedColors` used to fetch mapped colors only if user has not specified a color for a particular series label (and there's no color specified from the server). +4. `ChartsPluginSetup.colors.createColorLookupFunction()` is used by the `vis_type_vislib` plugin, ultimately, as part of `getColorFunc()` and `getPieColorFunc()`; the former also uses fallback for default and overwritten colors from `uiState`. +5. Set as a dependency in stub plugin `vis_type_xy`, but not actually used. + +## `uiSettings` in advanced settings `visualization:colorMapping` + +Appears to only be used by the telemetry plugin: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/95f4fd5c6a6cd59bd555bf0ec120843ef6a93566/src/plugins/telemetry/schema/oss_plugins.json#L1363 + +## Static functions and components + +### Color Maps + +#### `ColorMap` interface + +1. `region_map` plugin +2. `tile_map` plugin +3. `timeline` plugin +4. `vis_type_metric` plugin +5. `vis_type_timeline` plugin + +#### `ColorSchema` interface + +1. `maps_legacy` plugin +2. `region_map` plugin +3. `tile_map` plugin +4. `vis_type_metric` plugin +5. `vis_type_vislib` plugin +6. `visualizations` plugin +7. `visualize` plugin +8. `vis_builder` plugin + +#### `ColorSchemas` enum + +1. `region_map` plugin +2. `tile_map` plugin +3. `vis_type_metric` plugin +4. `vis_type_vislib` plugin +5. `vis_builder` plugin + +#### `RawColorSchema` interface + +Not used by any core plugins + +#### `colorSchemas` array of objects + +1. `region_map` plugin +2. `tile_map` plugin +3. `vis_type_metric` plugin +4. `vis_type_vislib` plugin +5. `vis_builder` plugin + +#### `getHeatmapColors` function + +1. `vis_type_metric` plugin +2. `vis_type_vislib` plugin + +#### `truncatedColorMaps` object + +1. `region_map` plugin +2. `tile_map` plugin + +#### `truncatedColorSchemas` array of objects + +1. `region_map` plugin +2. `tile_map` plugin + +#### `vislibColorMaps` object + +1. `vis_type_metric` plugin + +### React components + +These components may eventually make more sense elsewhere. See https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2832 for one such proposal. + +#### `ColorModes` object + +1. `vis_type_metric` plugin +2. `vis_type_vislib` plugin +3. `vis_builder` plugin + +#### `Rotates` object + +1. `vis_type_vislib` plugin + +#### `BasicOptions` component + +1. `tile_map` plugin +2. `vis_type_vislib` plugin +3. `vis_builder` plugin + +#### `ColorRanges` component + +1. `vis_type_metric` plugin +2. `vis_type_vislib` plugin +3. `vis_builder` plugin + +#### `ColorSchemaOptions` component + +Accounts for customized `vis.colors` in the `uiState`. Supports setting custom colors via legend, and resetting. + +1. `vis_type_metric` plugin. Doesn't actually support custom colors +2. `vis_type_vislib` plugin +3. `vis_builder` plugin (metric visualization). Doesn't support custom colors + +#### `NumberInputOption` component + +1. `region_map` plugin +2. `vis_type_table` plugin +3. `vis_type_vislib` plugin + +#### `RangeOption` component + +1. `tile_map` plugin +2. `vis_type_markdown` plugin +3. `vis_type_metric` plugin +4. `vis_type_timeseries` plugin +5. `vis_builder` plugin + +#### `RequiredNumberInputOption` component + +1. `vis_type_vislib` plugin + +#### `SelectOption` component + +1. `index_pattern_management` plugin +2. `maps_legacy` plugin +3. `region_map` plugin +4. `tile_map` plugin +5. `vis_type_table` plugin +6. `vis_type_tagcloud` plugin +7. `vis_type_timeseries` plugin +8. `vis_type_vislib` plugin +9. `vis_builder` plugin + +#### `SwitchOption` component + +1. `maps_legacy` plugin +2. `region_map` plugin +3. `tile_map` plugin +4. `vis_type_markdown` plugin +5. `vis_type_metric` plugin +6. `vis_type_table` plugin +7. `vis_type_tagcloud` plugin +8. `vis_type_vislib` plugin +9. `vis_builder` plugin + +#### `TextInputOption` component + +1. `maps_legacy` plugin +2. `vis_type_vislib` plugin + +# OUI chart colors + +An alternative to using color schemas and maps provided by the `charts` plugin is to use [color palettes from OUI](https://github.com/opensearch-project/oui/blob/main/src/services/color/oui_palettes.ts). + +## `ouiPaletteColorBlind()` + +1. `index_pattern_management` plugin +2. `vis_type_vega` plugin +3. `vis_type_vislib` plugin + +## Other quantitative palettes + +Not currently used + +## `colorPalette` + +Not currently used diff --git a/docs/docker-dev/docker-compose.yml b/docs/docker-dev/docker-compose.yml new file mode 100644 index 00000000000..f2f0ffb64cf --- /dev/null +++ b/docs/docker-dev/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3' +services: + opensearch-node: + image: opensearchproject/opensearch:latest + container_name: opensearch-node + environment: + - cluster.name=opensearch-cluster # Name the cluster + - node.name=opensearch-node # Name the node that will run in this container + - discovery.seed_hosts=opensearch-node # Nodes to look for when discovering the cluster + - cluster.initial_cluster_manager_nodes=opensearch-node # Nodes eligibile to serve as cluster manager + - bootstrap.memory_lock=true # Disable JVM heap memory swapping + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM + - "DISABLE_INSTALL_DEMO_CONFIG=true" # Prevents execution of bundled demo script which installs demo certificates and security configurations to OpenSearch + - "DISABLE_SECURITY_PLUGIN=true" # Disables security plugin + ulimits: + memlock: + soft: -1 # Set memlock to unlimited (no soft or hard limit) + hard: -1 + nofile: + soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536 + hard: 65536 + volumes: + - opensearch-data:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container + ports: + - 9200:9200 # REST API + - 9600:9600 # Performance Analyzer + networks: + - opensearch-net # All of the containers will join the same Docker bridge network + dev-env: + image: abbyhu/opensearch-dashboards-dev:latest + container_name: dev-env + ports: + - 5601:5601 + - 5603:5603 + stdin_open: true + tty: true + expose: + - "5601" + - "5603" + environment: + - 'OPENSEARCH_HOSTS=["http://opensearch-node:9200"]' + - 'SERVER_HOST="0.0.0.0"' + - 'REPO_URL=${REPO_URL}' + volumes: + - osd-dev:/home/osd-dev + - ./entrypoint.sh:/entrypoint.sh + networks: + - opensearch-net + entrypoint: + - /bin/bash + - /entrypoint.sh +volumes: + opensearch-data: + osd-dev: +networks: + opensearch-net: + \ No newline at end of file diff --git a/docs/docker-dev/docker-dev-setup-manual.md b/docs/docker-dev/docker-dev-setup-manual.md new file mode 100644 index 00000000000..53b80e9ff63 --- /dev/null +++ b/docs/docker-dev/docker-dev-setup-manual.md @@ -0,0 +1,59 @@ +# Docker Development Environment Setup +The following instructions demonstrate how to set up a development environment for OpenSearch Dashboards using Docker. It utilizes tools such as `Docker` and `VS Code`, and users should be familiar with the basic usages of them. Users will be able to develop and run the application inside VS Code without additional configurations. + +1. Install [Docker](https://docs.docker.com/get-docker/) if not already installed. + * Make sure that Docker daemon is running. (For windows and macos,you need to have [Docker Desktop](https://docs.docker.com/desktop/), or its alternatives, such as [Finch](https://github.com/runfinch/finch)) + +2. In the terminal, run the command below. + * This should create a folder named `opensearch-dashboards-docker-dev` and it should contain two files: `docker-compose.yml` and `entrypoint.sh`. + * Here is the link to the installer script: `https://raw.githubusercontent.com/opensearch-project/OpenSearch-Dashboards/main/dev-tools/install-docker-dev.sh` if needed. + +```bash +curl -o- https://raw.githubusercontent.com/opensearch-project/OpenSearch-Dashboards/main/dev-tools/install-docker-dev.sh | bash +``` + +3. Open VS Code or [install it](https://code.visualstudio.com/download), if it's not already installed. + * Make sure VS Code has the extensions `Dev Containers` and `Docker` installed. If not, go to `Extensions` tab, search and install them. + +4. Under the Discover tab, click `Open Folder`, and open the `opensearch-dashboards-docker-dev` folder that we just created. + +5. Open the `opensearch-dashboards-docker-dev` folder in VS Code integrated terminal, set environment variable for the fork repository URL by running the command below. + * If fork repo has not been created: Go to [OpenSearch Dashboards github page](https://github.com/opensearch-project/OpenSearch-Dashboards) and under fork, select create a new fork, and then copy the https link of the fork url and use it in the above command. The command needs to be re-run every time it re-start the docker compose file in a new terminal. +```bash +export REPO_URL=[insert your fork repo url here] +``` + +6. Run the `docker-compose.yml` file in the background by typing: +```bash +docker compose up -d --build +``` + +7. Under the `Docker` tab in VS Code, verify that there are two containers running: `opensearchproject/opensearch:latest` and `abbyhu/opensearch-dashboards-dev:latest`. + * This can also be verified by using the command line: + ```bash + docker ps + ``` + +8. Right-click `abbyhu/opensearch-dashboards-dev:latest`, and select `Attach Visual Studio Code`. + * This will ssh into the container and you will be able to view and edit the files using VS Code as the code editor. + * If you do not wish to use VS Code as the code editor, the alternative way of ssh into the container is by using the command below: + ```bash + docker exec -it dev-env /bin/bash + ``` + +9. For the new VS Code window, if it is not showing the repository code, then select `Open Folder`. Then open `/workspace-docker/OpenSearch-Dashboards`. + +10. In the terminal, start the OpenSearch Dashboards application by typing: +```bash +yarn start:docker +``` + +11. Now that OpenSearch Dashboards is running, you should be able to see a log line similar to `[info][server][OpenSearchDashboards][http] http server running at http://0.0.0.0:5603/dog`. + * The last three letters `dog` are randomly generated every time we start dashboards. + +12. Wait for the optimizer to run, which takes about 100s - 200s. Once the optimizer is finished running, it will show a line such as `[success][@osd/optimizer] 48 bundles compiled successfully after 204.9 sec, watching for changes`. + +13. Then paste the link into a chrome browser and view dashboard running in browser, but change β€˜0.0.0.0’ to β€˜localhost’. So here the link should be `http://localhost:5603/dog`. + * Files are constantly watched, so when you make code changes, OpenSearch Dashboards will rebuild and restart automatically. Refresh the link in the browser and the new changes should be applied. + +14. `Git` is already configured in the `entrypoint.sh` file, and the remote is already tracking the fork repository. You can start contributing by creating your branch off the main, and commit your first PR! diff --git a/docs/docker-dev/entrypoint.sh b/docs/docker-dev/entrypoint.sh new file mode 100644 index 00000000000..ed5c4e720d6 --- /dev/null +++ b/docs/docker-dev/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo $REPO_URL +git remote set-url origin $REPO_URL +git fetch +tail -f /dev/null diff --git a/docs/multi-datasource/client_management_design.md b/docs/multi-datasource/client_management_design.md new file mode 100644 index 00000000000..c83b254fe28 --- /dev/null +++ b/docs/multi-datasource/client_management_design.md @@ -0,0 +1,226 @@ +# Multi Data Source Client Management + +## 1. Problem Statement + +This design is part of the OpenSearch Dashboards multi data source project [[RFC](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1388)], where we need to manage and expose datasource clients. Connections are established by creating clients that can then be used by a caller to interact with any data source (OpenSearch is the only data source type in scope at this phase). + +**Overall the critical problems we are solving are:** + +1. How to set up connection(clients) for different data sources? +2. How to expose data source clients to callers through clean interfaces? +3. How to maintain backwards compatibility if user turn off this feature? +4. How to manage multiple clients/connection efficiently, and not consume all the memory? + +## 2. Requirements + +1. **Accessibility**: + 1. Clients need to be accessible by other OpenSearch Dashboards plugins or modules through interfaces, in all stages of the plugin lifecycle. E.g β€œSetup”, and β€œStart” + 2. Clients should be accessible by plugin through request handler context. +2. **Client Management**: Clients needs to be reused in a resource-efficient way to not harm the performance. +3. **Backwards compatibility**: if user enables this feature and later disabled it. Any related logic should be able to take in this config change, and deal with any user cases. + 1. Either switching to connect to default OpenSearch cluster + 2. Or blocking the connection to data source, and throw error message +4. **Auditing:** Need to log different user query on different data sources, for troubleshooting, or log analysis + +## 3. Architecture/Dataflow + +- We are adding a new service in core to manage data source clients, and expose interface for plugins and modules to access data source client. +- Existing OpenSearch services and saved object services should not be affected by this change + +#### 3.1 Dataflow of plugin(use viz plugin as example) call sequence to retrieve data form any datasource. + +![img](./img/client_management_dataflow.png) + +#### 3.2 Architecture Diagram + +![img](./img/client_management_architecture.png) + +## 4. Detailed Design + +### 4.0 Answer some critical design questions + +**1.** **How to set up connection(clients) for different data sources?** +Similar to how current OpenSearch Dashboards talks to default OpenSearch by creating a client using [opensearch-js](https://github.com/opensearch-project/opensearch-js) library, for data sources we also create clients for each connection. Critical params that differentiate data sources are `url` and `auth` + +```ts +const { Client } = require('@opensearch-project/opensearch'); + +const dataSourceClient = new Client({ + node: url, + auth: { + username, + password, + }, + ...OtherClientOptions, +}); + +dataSourceClient.search(); +dataSourceClient.ping(); +``` + +**2. How to expose datasource clients to callers through clean interfaces?** +We create a `data source service`. Similar to existing `opensearch service` in core, which provides client of default OpenSearch cluster. This new service will be dedicated to provide clients for data sources. Following the same paradigm we can register this new service to `CoreStart`, `CoreRouteHandlerContext` , in order to expose data source client to plugins and modules. The interface is exposed from new service, and thus it doesn’t mess up with any existing services, and keeps the interface clean. + +``` +*// Existing* +*const defaultClient: OpenSearchClient = core.opensearch.client.asCurrentUser +* +// With openearch_data_services added +const dataSourceClient: OpenSearchClient = core.openearchData.client +``` + +**3.How to maintain backwards compatibility if user turns off this feature?** +The context is that user can only turn on/off multiple datasource feature by updating boolean config `data_source.enabled` in `opensearch_dashboards.yml` and reboot. + +1. **Browser side**, if datasource feature is turned off, browser should detect the config change and update UI not allowing request to be submitted to any datasource. Multiple datasource related UI shouldn't render. If the request is not submitted to a datasource, the logic won’t return a datasource client at all. +2. **Server side**, if user submits the request to datasource manually, on purpose. Or the plugin tries to access datasource client from server side. In the corresponding core service we’ll have a **flag** that maps to the **enable_multi_datasource** boolean config, and throw error if API is called while this feature is turned off. + +**4.How to manage multiple clients/connection efficiently, and not consume all the memory?** + +- For data sources with different endpoint, user client Pooling (E.g. LRU cache) +- For data sources with same endpoint, but different user, use connection pooling strategy (child client) provided by opensearch-js. + +**5.Where should we implement the core logic?** +Current `opensearch service` exists in core. The module we'll implement has similarity function wise, but we choose to implement `data source service` in plugin along with `crypto` service for the following reasons. + +1. Data source is a feature that can be turned on or off. Plugin is born for such pluggable use case. +2. We don't mess up with OpenSearch Dashboards core, since this is an experimental feature, the potential risk of breaking existing behavior will be lowered if we use plugin. Worst case, user could just uninstall the plugin. +3. Complexity wise, it's about the same amount of work. + +### 4.1 Data Source Plugin + +Create a data source plugin that only has server side code, to hold most core logic of data source feature. Including data service, crypto service, and client management. A plugin will have all setup, start and stop as lifecycle. + +**Functionality** + +- Setup plugin configuration such as `data_source.enabled` +- Define and register datasource as a new saved object type +- Initiate data source service and crypto service +- Register API to get datasource client to core route handler context +- Setup logging and auditing +- Stop all running services in plugin `stop()` phase + +### 4.1 Data Source Service + +We need to create a data source service in the data source plugin, to provide the main functionality and APIs for callers to `getDataSourceClient()`. A service in a plugin will have all setup, start and stop as lifecycle. + +**Functionality** + +- Initialize client pool as empty data structure but with size mapped to user config value. (`data_source.clientPool.size`) +- Configuring a data source client and expose as `getDataSourceClient()` from service level. + +### 4.2. Data source client + +We need to configure the data source client by either creating a new one, or looking up the client pool. + +**Functionality** + +- Get data source meta info: Use saved object client to retrieve data source info from OpenSearch Dashboards system index by id, and parse results to `DataSource` object. + + ```ts + { + title: ds-sample; + description?: data source; + endpoint: http://opensearch.com; + auth: { + type: "Basic Auth" + username: "user name" + password: "encrypted content" + }; + } + ``` + +- Get root client: Look up the client pool by **endpoint** and return the client if it exists. If a client was not found, a new client instance is created and loaded into pool. At this step, the client won't have any auth info. + +- Get credentials: Call crypto service utilities to **decrypt** user credentials from `DataSource` Object. +- Assemble the actual query client: With auth info and root client, we’ll leverage the `opensearch-js` connection pooling strategy to create the actual query client from root client by `client.child()`. + +#### 4.2.1 Legacy Client + +OpenSearch Dashboards had two types of clients available for use when created. One was the "new client" which has since been separated into `opensearch-js`, and the other was the legacy client named `elasticsearch-js`. Legacy clients are still used by some core features like visualization and index pattern management. + +```ts +// legacy client +context.core.opensearch.legacy.client.callAsCurrentUser; +// new client +context.core.opensearch.client.asCurrentUser; +``` + +Since deprecating legacy client could be a bigger scope of project, multiple data source feature still need to implement a substitute for it as for now. Implementation should be done in a way that's decoupled with data source client as much as possible, for easier deprecation. Similar to [opensearch legacy service](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/core/server/opensearch/legacy) in core. + +```ts +context.dataSource.opensearch.legacy.getClient(dataSourceId); +``` + +### 4.3 Register datasource client to core context + +This is for plugin to access data source client via request handler. For example, by `core.client.search(params)`. It’s a very common use case for plugin to access cluster while handling request. In fact data plugin uses it in its search module to get client, and I’ll talk about it in details in next section. + +- **param** + - **dataSourceId**: need it to retrieve **datasource info** for either creating new client, or look up the client pool +- **return type:** OpenSearchClient + ```ts + core.http.registerRouteHandlerContext( + 'dataSource', + { + opensearch: { + getClient: (dataSourceId: string) = { + ... + return dataSourceService.getDataSourceClient() + } + } + } + ``` + +### 4.4 Refactor data plugin search module to call core API to get datasource client + +`Search strategy` is the low level API of data plugin search module. It retrieves clients and queries OpenSearch. It needs to be refactored to switch between the default client and the datasource client, depending on whether or not a request is sent to the datasource. + +Currently default client is retrieved by search module of data plugin to interact with OpenSearch by this API call. Ref: [opensearch-search-strategy.ts](https://github.com/opensearch-project/opensearch-dashboards/blob/e3b34df1dea59a253884f6da4e49c3e717d362c9/src/plugins/data/server/search/opensearch_search/opensearch_search_strategy.ts#L75) + +```ts +const client: OpenSearchClient = core.opensearch.client.asCurrentUser; +// use API provided by opensearch-js lib to interact with OpenSearch +client.search(params); +``` + +Similarly we’ll have the following for datasource use case. `AsCurrentUser` doesn't really apply to a datasource because it’s always the β€œcurrent” user's credentials, defined in the β€œdatasource”, that gets used to initialize the client or lookup the client pool. + +```ts +if (request.dataSource) { + await client: OpenSearchClient = + core.opensearchData.getClient() +} else { +// existing logic to retrieve default client + client: OpenSearchClient = core.opensearch.client.asCurrentUser +} + +// use API provided by opensearch-js lib to interact with OpenSearch +client.ping() +client.search(params) +``` + +### 4.5 Client Management + +When loading a dashboard with visualizations, each visualization sends at least 1 request to server side to retrieve data. With multiple data source feature enabled, multiple requests are being sent to multiple datasources, that requires multiple clients. If we return a new client **per request**, it will soon fill up the memory and sockets with idle clients hanging there. Of course we can close a client anytime. But the connection is supposed to be kept alive for easy reload and periodic pulling data. Therefore, we should come up with better solution to manage clients efficiently. + +#### Client pooling by LRU cache + +- Key: data source endpoint +- Value: OpenSearch client object +- Configurable pool size: `data_source.clientPool.size`, default to 5 +- Use existing js `lru-cache` lib in OpenSearch Dashboards, that enables easy initialization, look up, and dumping outdated client. +- While stopping the service, we can close all the connections by looping the LRU cache and calling `client.close()` for each. +- For data sources with same endpoint, but different user, use connection pooling strategy (child client) provided by opensearch-js. + +```ts +import LRUCache from 'lru-cache'; + +export class OpenSearchClientPool { + private cache?: LRUCache + ... +``` + +## 5. Audit & Logging + +[#1986](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1986) diff --git a/docs/multi-datasource/high_level_design.md b/docs/multi-datasource/high_level_design.md new file mode 100644 index 00000000000..f89bd3d0ff3 --- /dev/null +++ b/docs/multi-datasource/high_level_design.md @@ -0,0 +1,146 @@ +# Multiple Data Source Support High Level Design + +OpenSearch Dashboards is designed and implemented to only work with one single OpenSearch cluster. This documents discusses the design to enable OpenSearch Dashboards to work with multiple OpenSearch endpoints, which can be a centralized data visualization and analytics application. + +For more context, see RFC [Enable OpenSearch Dashboards to support multiple OpenSearch clusters](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1388) + +## User Stories + +[OpenSearch Dashboards Multiple OpenSearch Data Source Support User Stories](user_stories.md) + +From a very high level, we introduce `data-source` as a new OpenSearch Dashboards saved object type. + +## Terminologies + +- **Dashboards metadata**: refers to data documents saved in the `.kibana` index. Equivalent to Dashboards **saved objects**. +- **User data**: in this document, user data refers to the log, metrics or search catalog data that saved in OpenSearch, users run analysis against these user data with OpenSearch Dashboards. +- **Data source**: an OpenSearch endpoint, it could be a on-prem cluster, or AWS managed OpenSearch domain or a serverless collection, which stores the user log/metrics data for visualization and analytics purpose. + - in this document, we may also refer data source as a new type of OpenSearch Dashboards saved objects, which is a data model to describe a data source, including endpoint, auth info, capabilities etc. + +## Scope + +We are targeting to release the multiple data source support in OpenSearch 2.4 preview as an experimental feature, and make it GA over a few minor version throughout 2.x versions. + +### Preview Scope + +- data source only support basic authentication with OpenSearch + - API key, JWT, Sigv4 and other auth types are out of scope +- data source will only work with visualizations, and discover + - plugins like AD/Alerting/ISM doesn’t work with data source + - DevTool console maybe in scope depending on the progress and resource + - Observability visualizations are out of scope +- data source support can be enabled/disable based on config in OpenSearch Dashboards yml config file +- multiple data source project doesn’t change existing security experience + - e.g. if a user have access to a security tenant, they will be able to use the data sources defined in that tenant + +### GA Scope + +- Support all Elasticsearch 7.10 DSL/API compatible data sources, including customer self managed Elasticsearch 7.10, OpenSearch 2.x clusters, AWS managed OpenSearch and Elasticsearch 7.10 domains. OpenSearch Serverless collections. + - Support Basic auth, AWS SigV4 signing with Data sources +- OpenSearch Dashboards plugins such as Alerting/AD etc. can work with each data source depending on the data source capability +- Observability visualizations are out of scope +- Support of different (major) versions of ES/OpenSearch data sources is out of scope + +## Requirements + +### Functional requirements + +- OpenSearch Dashboards users should be able to dynamically add/view/update/remove OpenSearch data sources using UI and API +- OpenSearch Dashboards users should be able to save/update/remove credentials( username/password in preview, and AWS Sigv4 in GA) +- OpenSearch Dashboards users can create index pattern with specific data source +- Data source credentials should be handled securely +- OpenSearch Dashboards users can put data visualizations of different data sources into one dashboard +- OpenSearch analytics and management functions (such as AD, ISM and security) can work with specific data source to manage those functions in corresponding data source + - such as user can choose a data source and then edit/view Anomaly detectors and security roles with OpenSearch Dashboards +- OpenSearch Dashboards should be able to work with self managed and AWS managed + +### Limitations + +- One index pattern can only work with one data source +- One visualization will still only work with one index pattern +- Plugins like AD and alerting will only work with one data source at any point of time + +## Design + +### Introducing data source saved object model + +Generally, OpenSearch Dashboards works with 2 kinds of data: + +1. User data, such as application logs, metrics, and search catalog data in data indices. +2. OpenSearch Dashboards metadata, which are the saved objects in `.kibana` index + +Currently both OpenSearch Dashboards metadata and user data indices are saved in the same OpenSearch cluster. However in the case to support OpenSearch Dashboards to work with multiple OpenSearch data sources, OpenSearch Dashboards metadata index will be stored in one OpenSearch cluster, and user data indices will be saved in other OpenSearch clusters. Thus we will need to differentiate OpenSearch Dashboards metadata operations and user data access. + +OpenSearch Dashboards admin will still define an OpenSearch cluster in the `opensearch.host` config in `opensearch_dashboards.yml` file. It will be used as the OpenSearch Dashboards metadata store, and OpenSearch Dashboards metadata will still be saved in the `.kibana` index in this OpenSearch cluster. + +Regarding the user data access, we propose to add a new β€œdata-source” saved objects type, which describes a data source connection, such as + +- cluster endpoint +- auth info, like auth types and credentials to use when accessing the data source +- data source capabilities, such as if the data source supports AD/ISM etc. + +Users can dynamically add data source in OpenSearch Dashboards using UI or API, OpenSearch Dashboards will save the data source saved objects in its metadata index. And then users can do as they want with their data sources. For example, when OpenSearch Dashboards needs to access user data on behalf of the customer, customer will need to specify a data source id, then OpenSearch Dashboards can fetch the data source info from its metadata store, then send the request to the corresponding data source endpoint. + +So the Dashboards and OpenSearch setup may look like:![img](./img/hld_setup_diagram.png) + +Refer to the proposed solution in [#1388](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1388) for the data modeling of data source + +### Data source integration + +[opensearch_service](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/core/server/opensearch) is one of the core modules of OpenSearch Dashboards, it is a singleton instance in OpenSearch Dashboards which manages OpenSearch Dashboards connection with the backend OpenSearch endpoint. It makes another level of abstraction of OpenSearch client, and provide a set of interfaces for other OpenSearch Dashboards modules and plugins to interact with OpenSearch for example running DSL queries, or calling arbitrary OpenSearch APIs. + +Currently, OpenSearch Dashboards only works with one OpenSearch cluster, OpenSearch Dashboards metadata index and user data indices are stored in the same OpenSearch cluster. So the OpenSearch Dashboards [saved object service](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/core/server/saved_objects), which the core OpenSearch Dashboards module handles all OpenSearch Dashboards metadata operations, also relies on `opensearch_service` interfaces to work with OpenSearch. + +With multi-datasource, we will need to diverge the `opensearch_service` for these 2 use cases. We propose to fork a new `metadata_client` from existing `opensearch_service` to manage the metadata store connection, so that `saved_objects_service` can use `metadata_client` to perform saved objects operations. And then we repurpose the `opensearch_service` to serve the user data access use cases. The new `opensearch_service` needs will expose following interface to allow other OpenSearch Dashboards components to interact with a specific data source cluster. + +``` +core.opensearch.withDataSource().callAsCurrentUser(searchParams) +``` + +OpenSearch Dashboards plugins like data plugin, alerting plugin will need to introduce the data source concept into their use case, letting users to specify a data source when using their functions, and then switch to this new opensearch interface when calling OpenSearch APIs or executing queries. + +### Visualization solution with support of multiple datasource + +Current OpenSearch Dashboards visualization solution replies on 3 major saved object types: index-pattern, visualization and dashboard. + +- Index pattern is a level of data abstraction. Index pattern describes a set of data indices, and their data schema. +- Visualization works starts with index pattern. OpenSearch Dashboards users can create data visualizations against an index pattern. A visualization includes the OpenSearch DSL query, aggregation and a reference to an index pattern, as well as graph metadata such as legend and labels. When rendering a visualization graph, the visualization executes the query & aggregation against that specific index pattern, and draw the graph according to graph settings. +- Dashboard references visualizations. OpenSearch Dashboards users can place a set of visualizations into a dashboard. A OpenSearch Dashboards dashboards describes the layout and control (time picker, field filters) of all visualizations on the dashboard. + +To support multiple data source in OpenSearch Dashboards, we will add β€œdata source” saved object as a reference to the index pattern model. One index pattern will have one data source reference. An index pattern can only refer to one data source, one data source can be used by multiple index patterns. + +With this new β€œdata source” reference in index pattern, OpenSearch Dashboards users will need to first create data sources in OpenSearch Dashboards, then select a data source when creating index patterns. Then the visualization and dashboard creation experience will remain the same. Also for any other save object types, if they reference index-pattern, or reference any save object that references index-pattern. Retrieving data from data source will be supported out of the box. + +- For OpenSearch Dashboards multiple data source user experience, refer to [OpenSearch Dashboards Multiple OpenSearch Data Source Support User Stories](https://quip-amazon.com/VXQ0AhpPs3gU) + +- The OpenSearch Dashboards visualization rendering flow will look like following with multi-datasource support: ![image](./img/hld_vis_flow.png) + +### Backward Compatibility + +We plan to release this multi-datasource support as an experimental feature with OpenSearch 2.4. OpenSearch Dashboards admins will be able to enable or disable the multi-datasource feature using configurations in `opensearch_dashboards.yml` . + +If multi-datasource is enabled, OpenSearch Dashboards users will be able to see all data source related feature and APIs, that they can manage their data sources, and build visualization and dashboards with data sources. While if multi-datasource is disabled, users will not see anything related to data sources, and their OpenSearch Dashboards experience will remain the same as single data source. + +If OpenSearch Dashboards admin enables multi-datasource for an existing OpenSearch Dashboards service, users will still able to use their existing index patterns and visualizations, which will by default fetch data from the same endpoint as their metadata store. + +If an OpenSearch Dashboards service has enabled multi-datasource, and it already has index pattern with remote data source created, admin will not able to disable multi-datasource feature. OpenSearch Dashboards will fail to start if it detected data source in the saved object while multi-datasource is disabled. + +### Security + +#### Data source access control + +Multi-datasource project doesn’t plan to change the security (authN & authZ) controls for OpenSearch Dashboards. The `data-source` is a new type of saved objects, so the access control of `data source` will follow the same way as other saved objects such as index patterns and visualizations. + +Based on existing OpenSearch and OpenSearch Dashboards security implementations, OpenSearch Dashboards saved objects access control is implemented via `security tenants`. OpenSearch users are mapped to a set of roles, and each role has corresponding permission to access certain tenants. If a user has permission to access a tenant, they will be able to access all saved objects in that tenant. With this mechanism, if a user created a data source in a shared tenant, other users who has access to that shared tenant will be able to see the data source object and see/create visualizations with the data source. + +#### Data source credential handling + +Credentials is part of the data source object, and will be saved in OpenSearch Dashboards metadata index. OpenSearch Dashboards will use that credentials to authenticate with the data source when executing queries. This credentials will need to be encrypted regardless OpenSearch Dashboards has access control or not. + +We will use a symmetric key to encrypt the credentials before saving data source into OpenSearch Dashboards metadata index, and use the same key to decrypt it when OpenSearch Dashboards needs to authenticate with corresponding data source. For open source release, we will allow admins to configure the encryption key in the `opensearch_dashboards.yml` file. + +For more about credential encryption/decryption strategy, refer to [#1756](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1756) + +#### Auditing + +As part of the security effort, OpenSearch Dashboards needs to support the logging for all use of data sources, so that admins can have a clear view of which OpenSearch Dashboards user accessed data source, and queried data from that data source. The audit log could be saved in the metadata store, or local logs for potential auditing work. diff --git a/docs/multi-datasource/img/client_management_architecture.png b/docs/multi-datasource/img/client_management_architecture.png new file mode 100644 index 00000000000..548741bfc5c Binary files /dev/null and b/docs/multi-datasource/img/client_management_architecture.png differ diff --git a/docs/multi-datasource/img/client_management_dataflow.png b/docs/multi-datasource/img/client_management_dataflow.png new file mode 100644 index 00000000000..f0937167d69 Binary files /dev/null and b/docs/multi-datasource/img/client_management_dataflow.png differ diff --git a/docs/multi-datasource/img/dsm_flow.png b/docs/multi-datasource/img/dsm_flow.png new file mode 100644 index 00000000000..3dc4d3f72f4 Binary files /dev/null and b/docs/multi-datasource/img/dsm_flow.png differ diff --git a/docs/multi-datasource/img/hld_setup_diagram.png b/docs/multi-datasource/img/hld_setup_diagram.png new file mode 100644 index 00000000000..15854999b39 Binary files /dev/null and b/docs/multi-datasource/img/hld_setup_diagram.png differ diff --git a/docs/multi-datasource/img/hld_vis_flow.png b/docs/multi-datasource/img/hld_vis_flow.png new file mode 100644 index 00000000000..08bf027ffc1 Binary files /dev/null and b/docs/multi-datasource/img/hld_vis_flow.png differ diff --git a/docs/multi-datasource/resources/client_management_architecture.puml b/docs/multi-datasource/resources/client_management_architecture.puml new file mode 100644 index 00000000000..21cd6e9fa76 --- /dev/null +++ b/docs/multi-datasource/resources/client_management_architecture.puml @@ -0,0 +1,117 @@ +@startuml +hide stereotype +skinparam nodesep 6 +skinparam ranksep 10 + +skinparam component { +backgroundColor<> Green +backgroundColor<> Orange +backgroundColor<> LightYellow +backgroundColor<> LightGrey +backgroundColor<> Khaki +backgroundColor<> Grey +backgroundColor<> LightGrey +} +skinparam rectangle { +backgroundColor<> Green +backgroundColor<> Orange +backgroundColor<> LightYellow +backgroundColor<> LightGrey +backgroundColor<> Khaki +backgroundColor<> Grey +backgroundColor<> LightGrey +} +skinparam node { +backgroundColor<> Green +backgroundColor<> Orange +backgroundColor<> LightYellow +backgroundColor<> LightGrey +backgroundColor<> Khaki +backgroundColor<> Grey +backgroundColor<> LightGrey +} + +title ** OSD Multi Data Source Client Management Architecture ** + +node "Dashboards" as cluster { +rectangle "Legend" { + rectangle "New" as new <> + rectangle "Modified" as modify <> + rectangle "Existing" as existing <> + rectangle "External" as external <> + new -[hidden]right- modify + modify -[hidden]right- existing + existing -[hidden]right- external +} + + rectangle "Other Plugins" <> { + rectangle "visualization" as viz <> { + + } + rectangle "Alerting or other" as a <> { + + } + } + rectangle "Data Plugin" <> as dp { + rectangle "Search Module" as sm <> { + rectangle "Search Source" <> as source { + + } + rectangle "Search Strategy" as strategy <> { + + } + + } + interface "DataPluginStart" as dps + } + + rectangle "OpenSearch Data Source Plugin" as ods <> { + component "DataSource Service" as ds + interface "PluginSetUp" as dsps + component "Crypto Service" as cs + } + + rectangle "Core" <> as core { + + rectangle "opensearch service" as os_service <> { + component "internal/scoped client" as ic <> + interface "ServiceStart" as osss + + } + interface "CoreStart" as core_start + rectangle "saved object service" as sos <> { + interface "ServiceStart" as soss + } + + interface "CoreRouteHandlerContext" as cc <> + + + } + + ds --> es: query + source -> strategy: call + strategy --> cc: get datasource client + viz --> dps: speical viz types + viz --> source + dps --> sm + sos --> os: get saved objects + core_start --> cc + core_start <.. a: get client + a ..> dps + a ...> cc: get client + core_start <-- osss: register + osss <-- soss: depends + ic --> os: query + ds -> cs: decrypt credential + dsps ---> cc: register + dp --[hidden]-- ods + + rectangle "Default OpenSearch" <> as os { + } + rectangle "Datasource(OpenSearch)" <> as es { + + } +} + + +@enduml \ No newline at end of file diff --git a/docs/multi-datasource/resources/client_management_dataflow.puml b/docs/multi-datasource/resources/client_management_dataflow.puml new file mode 100644 index 00000000000..fa778f8b0ce --- /dev/null +++ b/docs/multi-datasource/resources/client_management_dataflow.puml @@ -0,0 +1,75 @@ +@startuml +autoactivate on + +title ** Multiple Datasource Visualization call sequence ** + +box "OSD Browser" +participant "visualization" as viz +' participant "Timeline/Vega/TSVB" as viz_s +participant "expression" as e +end box + + +box "OSD Server-Data Plugin" #LightBlue +participant "SearchSource\n(High Level API)" as s +participant "Search Strategy\n(Low Level API)" as ss +end box + +box "OSD Server-Data Source Plugin" #LightBlue +participant "OpenSearch Data Service" as ods #LightGreen +end box + +box "OSD Server-Core" #LightBlue +participant "OpenSearch Service" as os +participant "Saved Object Service" as sos +end box + +box "OpenSearch" +database "OSD metadata" as oi +database "data index" as default_di +end box + +box "DataSource[OS]" +database "data index" as datasource_di +end box + +sos --> os: depends on +ods --> sos: depends on + +viz -> e: execute expression pipeline +e -> s: create SearchSource +s -> ss: call .search() +alt viz_type = Timelion/TSVB/Vega +viz -> ss: call .search() +end + +ss -> os: get client +alt if (datasource == true) +ss -> ods: get datasource client +alt if exists in datasource client pool +ods -> ods: find client +end + +ods -> sos: call saved_obj_client +sos -> oi: get datasource metadata +oi --> sos: +sos --> ods: datasource metadata +ods --> ods: create datasource client \n and add to pool +ods --> ss: return client + +end +os --> ss: return client +alt if (client is datasource Client) +ss -> datasource_di: query +datasource_di --> ss: data +end + +ss -> default_di: query +default_di --> ss: data + +ss --> s: data +s --> e: data +e --> viz: render data + +skinparam BoxPadding 15 +@enduml \ No newline at end of file diff --git a/docs/multi-datasource/resources/dsm_flow.puml b/docs/multi-datasource/resources/dsm_flow.puml new file mode 100644 index 00000000000..a4da756cc7a --- /dev/null +++ b/docs/multi-datasource/resources/dsm_flow.puml @@ -0,0 +1,45 @@ +@startuml +title DataSource Management in Stack Management + + +:DataSource Owner: as DSO +:DataSource User: as DSU + +(Stack Management Page) as (Page-Stack) +(DataSource Management Page) as (Page-DM) +(Add new DataSource Page) as (Page-AND) +(DataSource Grid View) as (Component-DSG) +(DataSource Edit Page) as (Page-DSEP) +(Delete DataSource Button) as (Component-DelDS) +(Export DataSource Button) as (Component-ExDS) +(Import DataSource Button) as (Component-ImDS) + + +(DataSource Name) as (Component-DSName) +(DataSource Type) as (Component-DSType) +(DataSource Endpoint) as (Component-DSEndpoint) +(DataSource Credential) as (Component-DSCredential) + +DSO -> (Page-Stack) +(Page-Stack) -> (Page-DM) +(Page-DM) -> (Page-AND) : Add +(Page-AND) -> (Page-DM) : Save +(Page-DM) ...> (Component-DSG): View +(Page-DM) <..> (Component-DelDS) : Delete +(Page-DM) <..> (Component-ExDS) : Export +(Page-DM) <..> (Component-ImDS) : Import + +(Component-DSG) -> (Page-DSEP): Edit + +(Page-DSEP) .....> (Component-DSName) : Edit +(Page-DSEP) .....> (Component-DSType): Edit +(Page-DSEP) .....> (Component-DSEndpoint): Edit +(Page-DSEP) .....> (Component-DSCredential): Select + +note top of DSO + DataSource Owner who has access to manage + all DataSources. + When security enabled, user could only see + DataSources added by them +end note +@enduml \ No newline at end of file diff --git a/docs/multi-datasource/user_stories.md b/docs/multi-datasource/user_stories.md new file mode 100644 index 00000000000..2c4dd97721a --- /dev/null +++ b/docs/multi-datasource/user_stories.md @@ -0,0 +1,65 @@ +# OpenSearch Dashboards Multiple OpenSearch Data Source Support User Stories + +Today, OpenSearch Dashboards (OpenSearch Dashboards) can only connect to one single OpenSearch cluster by configuring the cluster endpoint in the `opensearch_dashboards.yml` config file. We want to allow OpenSearch Dashboards users to dynamically add/update/remove OpenSearch compatible endpoints, and then do their analytics work with data in those OpenSearch data stores. + +RFC: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1388 + +This document discusses the user experience of the OpenSearch Dashboards multiple data source support. + +## User Story + +### Current user experience + +- OpenSearch Dashboards admin setup the OpenSearch Dashboards service and configure the OpenSearch endpoint in `opensearch_dashboards.yml` + - Both the OpenSearch Dashboards metadata index (`opensearch_dashboards` index) and data indices are saved in the same OpenSearch cluster +- OpenSearch Dashboards users can work with visualizations, usually they will + - Create/update index patterns + - Create/update visualization, each visualization is built on top of one index pattern + - Create/update dashboard using a group of visualizations + - Run adhoc queries against an index pattern using discover feature + - View index patterns/visualization/dashboards +- OpenSearch Dashboards users can work with analytics functions, such as Alerting/AD etc + +### Expected user experience with multiple data source + +We are planning to introduce a new `data-source` model, to describe an OpenSearch data source, and letting index pattern to refer to a `data-source`. + +- OpenSearch Dashboards admin setup the OpenSearch Dashboards service and configure the OpenSearch **metadata store endpoint** in `opensearch_dashboards.yml` + - the metadata store OpenSearch cluster only saves the `.kibana` index, data indices can be saved in other OpenSearch stores +- Users will need to have a data-source before they can do any visualization or analytics work with OpenSearch Dashboards + - Users can create/update/view data sources + - Users need to specify a data source when creating new index patterns, data source is not mutable after index pattern is created + - Create/update visualization and dashboards experience remains the same as is today. + - View index patterns/visualization/dashboards experience remains the same as is today. +- When users want to work with analytics features like AD and alerting. they need to specify a data source to work with. (We may consider to add default data source concept) + +## UI Change + +This multiple data source support and introduction of data source model requires several UI changes on OpenSearch Dashboards + +### Data source management + +![img](./img/dsm_flow.png) + +Data source, as a new saved object type, should have a management page, like index pattern. + +We will need to + +- add a new data source entry in the stack management Nav app, with a data source list table +- a data source detail page, to show detailed information of a specific data source, such as URL, auth type, endpoint capabilities etc. + +### Index Pattern + +- Index pattern creation flow: With the data sources, users will need to specify which data source to use when creating a new index pattern. +- Index pattern detail page: On the index pattern detail page, we will need to show which data source this index pattern uses +- Data source selector for plugins: when OpenSearch Dashboards users working with analytics functions like Alerting and AD, we will want to allow users to switch between data sources + +## Appendix + +### Data source security + +For the initial launch with OpenSearch 2.4 preview, we do not plan to change security design of OpenSearch. + +When creating a data source, users need to provide endpoint URL, username and password(if using basic authentication). OpenSearch Dashboards service will encrypt the username and password when storing it into metadata store. + +Data source is a new type of OpenSearch Dashboards saved objects. In current OpenSearch security model, access control on data source document is the same as other saved objects documents. Basically data source docs will be accessible by any user who has access to the tenant. diff --git a/docs/plugins/data_persistence.md b/docs/plugins/data_persistence.md new file mode 100644 index 00000000000..b4141e8a57a --- /dev/null +++ b/docs/plugins/data_persistence.md @@ -0,0 +1,235 @@ +# Data persistence +There are currently five plugins that have the ability to persist user data and configurations: `dashboard`, `discover`, `timeline`, `visualize`, and `vis-builder`. Data will be persisted globally when users navigate between them in OpenSearch Dashboards; data will also be persisted locally across the action of refreshes. To achieve this, they use services and mechanisms from `opensearch_dashboard_utils` plugin. + +State syncing utils are a set of helpers to sync application state with URL or browser storage (when setting state: `storeInSessionStore` to `true` in advanced setting, or in the case of an overflowed URL): +1. `syncState()`: subscribe to state changes and push them to state storage; subscribe to state storage and push them to state container +2. storages that are compatible with `syncState()` + 1. `OsdUrlStateStorage`: serialize state and persist it to URL's query param in [Rison](https://github.com/w33ble/rison-node) format; listen for state change in URL and update them back to state + 2. `SessionStorageStateStorage`: serialize state and persist it to URL's query param in session storage +3. state containers: redux-store like objects to help manage states and provide a central place to store state + +# Two types of persistence +There are two types for data persistence: +1. App state (example from visualization plugin) + 1. App state storage key: '_a' + 2. App state is persistent only within the specific app, values will persist when we refresh the page, values will not be persist when we navigate away from the app + 3. For visualize app, the params are: + 1. Query + + ![img](./img/app_query.png) + + 2. App filters + + ![img](./img/app_filter.png) + + 3. Vis & UI state + + ![img](./img/visualization.png) +2. Global query state + 1. Global state storage key: '_g' + 2. Global query state is persistent across the entire OpenSearch Dashboards application, values will persist when we refresh the page, or when we navigate across visualize, discover, timeline or dashboard page. For example, if we set time range to last 24 hours, and refresh intervals to every 30 min, the same time range and refresh intervals will be applied if we navigate to any of the other pages. + 3. Params: + 1. global filters (Select `pin filter` to make the filters global) + + ![img](./img/global_filter.png) + + 2. refresh intervals + + ![img](./img/refresh_interval.png) + + 3. time range + + ![img](./img/time_range.png) + +# URL breakdown & example + +![img](./img/URL_example.png) + +# Global state persistence + +1. In plugin.ts, during plugin setup, call `createOsdUrlTracker()`, listen to history changes and global state changes, then update the nav link URL. This also returns function such as `onMountApp()`, `onUnmountedApp()` + ```ts + const { + appMounted, + appUnMounted, + ... + } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/visualize'), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:visualize`, + navLinkUpdater$: this.appStateUpdater, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + .... + ``` + + * When we enter the app and app is mounted, it initializes nav link by getting previously stored URL from storage instance: `const storedUrl = storageInstance.getItem(storageKey)`. (Storage instance is a browser wide session storage instance.) Then it unsubscribes to global `state$` and subscribes to `URL$`. The current app actively listens to history location changes. If there are changes, set the updated URL as the active URL + + ```ts + function onMountApp() { + unsubscribe(); + ... + // track current hash when within app + unsubscribeURLHistory = historyInstance.listen((location) => { + ... + setActiveUrl(location.hash.substr(1)); + } + }); + } + ``` + + * When we are leaving the app and app is unmounted, unsubscribe `URL$` and subscribe to global `state$`. If the global states are changed in another app, the global state listener will still get triggered in this app even though it is unmounted, it will set the updated URL in storage instance, so next time when we enter the app, it gets the URL from the storage instance thus the global state will persist. + + ```ts + function onUnmountApp() { + unsubscribe(); + // propagate state updates when in other apps + unsubscribeGlobalState = stateParams.map(({ stateUpdate$, osdUrlKey }) => + stateUpdate$.subscribe((state) => { + ... + const updatedUrl = setStateToOsdUrl( ... ); + ... + storageInstance.setItem(storageKey, activeUrl); + }) + ); + } + ``` + +2. In `app.tsx`, call `syncQueryStateWithUrl(query, osdUrlStateStorage)` to sync `_g` portion of url with global state params + * When we first enter the app, there is no initial state in the URL, then we initialize and put the _g key into url + + ```ts + if (!initialStateFromUrl) { + osdUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { + replace: true, + }); + } + ``` + + * When we enter the app, if there is some initial state in the URL(the previous saved URL in `storageInstance`), so we retrieve global state from `_g` URL + + ```ts + // retrieve current state from `_g` url + const initialStateFromUrl = osdUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); + // remember whether there was info in the URL + const hasInheritedQueryFromUrl = Boolean( + initialStateFromUrl && Object.keys(initialStateFromUrl).length + ); + // prepare initial state, whatever was in URL takes precedences over current state in services + const initialState: QueryState = { + ...defaultState, + ...initialStateFromUrl, + }; + ``` + + * If we make some changes to the global state: 1. `stateUpdate$` get triggered for all other unmounted app(if we made the change in visualize plugin, then the `stateUpdate$` will get triggered for dashboard, discover, timeline), then it will call `setStateToOsdUrl()` to set `updatedURL` in `storageInstance` so global state get updated for all unmounted app. 2. `updateStorage()` get triggered for `currentApp` to update current URL state storage, then global query state container will also be in sync with URL state storage + + ```ts + const { start, stop: stopSyncingWithUrl } = syncState({ + stateStorage: osdUrlStateStorage, + stateContainer: { + ...globalQueryStateContainer, + set: (state) => { + if (state) { + // syncState utils requires to handle incoming "null" value + globalQueryStateContainer.set(state); + } + }, + }, + storageKey: GLOBAL_STATE_STORAGE_KEY, + }); + start(); + ``` + +# App state persistence + +1. We use `useVisualizeAppState()` hook to instantiate the visualize app state container, which is in sync with '_a' URL + +```ts + const { stateContainer, stopStateSync } = createVisualizeAppState({ + stateDefaults, + osdUrlStateStorage: services.osdUrlStateStorage, + byValue, + }); +``` +2. When we first enter the app, there is no app state in the URL, so we set the default states into URL in `createDefaultVisualizeAppState()`: `osdUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true });` + +3. When we make changes to the app state, the `dirtyStateChange` event emitter will get triggered, then osd state container will call `updateStorage()` to update the URL state storage, then state container(appState) will also be in sync with URL state storage + +```ts + const onDirtyStateChange = ({ isDirty }: { isDirty: boolean }) => { + if (!isDirty) { + // it is important to update vis state with fresh data + stateContainer.transitions.updateVisState(visStateToEditorState(instance, services).vis); + } + setHasUnappliedChanges(isDirty); + }; + eventEmitter.on('dirtyStateChange', onDirtyStateChange); + ... + const { start, stop: stopSyncingWithUrl } = syncState({ + stateStorage: osdUrlStateStorage, + stateContainer: { + ...globalQueryStateContainer, + set: (state) => { + if (state) { + globalQueryStateContainer.set(state); + } + }, + }, + storageKey: GLOBAL_STATE_STORAGE_KEY, + }); + // start syncing the appState with the ('_a') url + startStateSync(); +``` + +4. In `useEditorUpdates()`, we use the saved appState to load the visualize editor + +5. We can also choose to do state sync without using state container by directing hooking up the state managers with the URL data storage. For example, we implemented state management in Vis Builder with redux store, while the global state persistence implementation is the same, we implemented app state persistence without using any state containers or state syncing utils. For the actual visual data, we directly hook up redux store with `OsdUrlStateStorage` to sync the values by using `saveReduxState()` and `loadReduxState()`. For app filter and query, since they are part of the app states but not part of the redux store, we directly hook up their state managers from data plugin with `OsdUrlStateStorage` and `connectStorageToQueryState()`. + +# Refresh +When we refresh the page, both app state and global state should persist: + +1. `appMounted()` gets triggered for the current app, so current app subscribe to URL$ +2. `syncQueryStateWithUrl()` gets called within app.tsx for the current app, and we are getting the global states from URL '_g', and then `connectToQueryState()` gets called to sync global states and state container for the current app so the current app load the saved global states in top nav +3. `stateUpdate$` will get triggered for every other unmounted app, so the global states are updated for their URL in storage instance as well by calling `setStateOsdUrl()` +4. When we load the visualize editor, `createDefaultVisualizeAppState()` gets called, and it gets app state from URL '_a', and it updates appState based on URL +5. In `useEditorUpdates()`, it uses the updated appState to load the visualization with previous saved states +# Navigate to another app + +When we navigate to another app from the current app, global state should persist: + +1. `appUnmounted()` triggered for the current app, unsubscribe to `URLHistory$`, and subscribe to stateUpdate$ +2. `appMounted()` triggered for the app that we navigated to, so it unsubscribe its `stateUpdate$`, and subscribe to `URLHistory$` +3. `syncQueryStateWithUrl` is triggered, it then gets the saved global state from the `osdurlstatestorage` and set the top nav global states by using `globalQueryStateContainer` + +# Diagrams + +1. When first navigate to the visualize app, initialize and sync state storage and state containers +![img](./img/initialization.png) + +2. When we navigate to another app, the browser wide storage instance stores the last active URL for each app and also updates the URL if there are any new global state values. This ensure global data persistence across different apps. For example, if we navigate from visualize app to discover app: +![img](./img/navigate.png) + +Here is an example of the storage instance: +![img](./img/storage_instance.png) + +3. When we made some changes to the global params, the global query state container will receive updates and push the updates to osd url state storage. Other unmounted app will update their last active URL in instance storage as well. +![img](./img/global_persistence.png) + +4. When we made some changes to the app params, the app state container will receive updates and also push the updates to osd url state storage. +![img](./img/app_persistence.png) + +5. When we refresh the page, we parse the information from the URL(_g for global state, _a for app state). We use the saved information to create new state containers and set up synchronization between state containers and state storage. +![img](./img/refresh.png) diff --git a/docs/plugins/img/URL_example.png b/docs/plugins/img/URL_example.png new file mode 100644 index 00000000000..d7a42937f85 Binary files /dev/null and b/docs/plugins/img/URL_example.png differ diff --git a/docs/plugins/img/app_filter.png b/docs/plugins/img/app_filter.png new file mode 100644 index 00000000000..9f909fdd25d Binary files /dev/null and b/docs/plugins/img/app_filter.png differ diff --git a/docs/plugins/img/app_persistence.png b/docs/plugins/img/app_persistence.png new file mode 100644 index 00000000000..e6ba4608fc4 Binary files /dev/null and b/docs/plugins/img/app_persistence.png differ diff --git a/docs/plugins/img/app_query.png b/docs/plugins/img/app_query.png new file mode 100644 index 00000000000..287b735a409 Binary files /dev/null and b/docs/plugins/img/app_query.png differ diff --git a/docs/plugins/img/global_filter.png b/docs/plugins/img/global_filter.png new file mode 100644 index 00000000000..f741d708f33 Binary files /dev/null and b/docs/plugins/img/global_filter.png differ diff --git a/docs/plugins/img/global_persistence.png b/docs/plugins/img/global_persistence.png new file mode 100644 index 00000000000..72df26ab2ec Binary files /dev/null and b/docs/plugins/img/global_persistence.png differ diff --git a/docs/plugins/img/initialization.png b/docs/plugins/img/initialization.png new file mode 100644 index 00000000000..ba5034694a4 Binary files /dev/null and b/docs/plugins/img/initialization.png differ diff --git a/docs/plugins/img/navigate.png b/docs/plugins/img/navigate.png new file mode 100644 index 00000000000..d76c9b4e35a Binary files /dev/null and b/docs/plugins/img/navigate.png differ diff --git a/docs/plugins/img/refresh.png b/docs/plugins/img/refresh.png new file mode 100644 index 00000000000..acc901edc03 Binary files /dev/null and b/docs/plugins/img/refresh.png differ diff --git a/docs/plugins/img/refresh_interval.png b/docs/plugins/img/refresh_interval.png new file mode 100644 index 00000000000..4e2324219c4 Binary files /dev/null and b/docs/plugins/img/refresh_interval.png differ diff --git a/docs/plugins/img/storage_instance.png b/docs/plugins/img/storage_instance.png new file mode 100644 index 00000000000..71dc532e642 Binary files /dev/null and b/docs/plugins/img/storage_instance.png differ diff --git a/docs/plugins/img/time_range.png b/docs/plugins/img/time_range.png new file mode 100644 index 00000000000..0bdc709c08d Binary files /dev/null and b/docs/plugins/img/time_range.png differ diff --git a/docs/plugins/img/visualization.png b/docs/plugins/img/visualization.png new file mode 100644 index 00000000000..8c87a7dee4b Binary files /dev/null and b/docs/plugins/img/visualization.png differ diff --git a/examples/developer_examples/public/app.tsx b/examples/developer_examples/public/app.tsx index 90c28e3be8c..974c114f526 100644 --- a/examples/developer_examples/public/app.tsx +++ b/examples/developer_examples/public/app.tsx @@ -36,13 +36,12 @@ import { EuiPageContent, EuiCard, EuiPageContentHeader, - EuiFlexGroup, - EuiFlexItem, EuiFieldSearch, EuiListGroup, EuiHighlight, EuiLink, EuiButtonIcon, + EuiPage, } from '@elastic/eui'; import { AppMountParameters } from '../../../src/core/public'; import { ExampleDefinition } from './types'; @@ -66,12 +65,14 @@ function DeveloperExamples({ examples, navigateToApp, getUrlForApp }: Props) { }); return ( - - - -

Developer examples

-

- The following examples showcase services and APIs that are available to developers. + + + + +

Developer examples

+

+ The following examples showcase services and APIs that are available to developers. +

-

-
-
- - {filteredExamples.map((def) => ( - + + +
+ {filteredExamples.map((def) => ( {def.description} @@ -114,11 +120,13 @@ function DeveloperExamples({ examples, navigateToApp, getUrlForApp }: Props) { } image={def.image} footer={def.links ? : undefined} + titleSize="xs" + textAlign="left" /> - - ))} - - + ))} +
+
+ ); } diff --git a/examples/expressions_example/public/components/explorer_tab.tsx b/examples/expressions_example/public/components/explorer_tab.tsx index 14030751e5b..2104d103d2e 100644 --- a/examples/expressions_example/public/components/explorer_tab.tsx +++ b/examples/expressions_example/public/components/explorer_tab.tsx @@ -43,7 +43,6 @@ export function ExplorerTab() { const allTypes = new Set(Object.values(functions).map((fn) => fn.type)); // Catch all filter and remove - allTypes.delete(undefined); allTypes.add('all'); return [...allTypes].map((type) => ({ text: type })); diff --git a/examples/state_containers_examples/public/plugin.ts b/examples/state_containers_examples/public/plugin.ts index 2da5d36b3b9..e404c48ccd8 100644 --- a/examples/state_containers_examples/public/plugin.ts +++ b/examples/state_containers_examples/public/plugin.ts @@ -82,7 +82,7 @@ export class StateContainersExamplesPlugin implements Plugin { developerExamples.register({ appId: 'stateContainersExampleBrowserHistory', - title: 'State containers using browser history', + title: 'State containers: browser history', description: `An example todo app that uses browser history and state container utilities like createStateContainerReactHelpers, createStateContainer, createOsdUrlStateStorage, createSessionStorageStateStorage, syncStates and getStateFromOsdUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the @@ -101,7 +101,7 @@ export class StateContainersExamplesPlugin implements Plugin { developerExamples.register({ appId: 'stateContainersExampleHashHistory', - title: 'State containers using hash history', + title: 'State containers: hash history', description: `An example todo app that uses hash history and state container utilities like createStateContainerReactHelpers, createStateContainer, createOsdUrlStateStorage, createSessionStorageStateStorage, syncStates and getStateFromOsdUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the @@ -120,7 +120,7 @@ export class StateContainersExamplesPlugin implements Plugin { developerExamples.register({ appId: PLUGIN_ID, - title: 'Sync state from a query bar with the url', + title: 'State containers: Sync with the url', description: `Shows how to use data.syncQueryStateWitUrl in combination with state container utilities from opensearch_dashboards_utils to show a query bar that stores state in the url and is kept in sync. `, diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index 2afc32f3484..4de6e928a95 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -28,114 +28,96 @@ * under the License. */ -import React, { useState } from 'react'; +import React, { useMemo } from 'react'; import ReactDOM from 'react-dom'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; -import { EuiPage } from '@elastic/eui'; +import { + EuiPage, + EuiTitle, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiTabbedContent, +} from '@elastic/eui'; +import { AppMountParameters, CoreStart } from '../../../src/core/public'; +import { UiActionsExplorerServices, UiActionsExplorerStartDependencies } from './types'; +import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; -import { EuiButton } from '@elastic/eui'; -import { EuiPageBody } from '@elastic/eui'; -import { EuiPageContent } from '@elastic/eui'; -import { EuiPageContentBody } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiFieldText } from '@elastic/eui'; -import { EuiCallOut } from '@elastic/eui'; -import { EuiPageHeader } from '@elastic/eui'; -import { EuiModalBody } from '@elastic/eui'; -import { toMountPoint } from '../../../src/plugins/opensearch_dashboards_react/public'; -import { UiActionsStart, createAction } from '../../../src/plugins/ui_actions/public'; -import { AppMountParameters, OverlayStart } from '../../../src/core/public'; -import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '../../ui_action_examples/public'; -import { TriggerContextExample } from './trigger_context_example'; -import { ContextMenuExamples } from './context_menu_examples'; +import { BasicTab } from './basic_tab'; +import { ExplorerTab } from './explorer_tab'; -interface Props { - uiActionsApi: UiActionsStart; - openModal: OverlayStart['openModal']; -} +const ActionsExplorer = () => { + const tabs = useMemo( + () => [ + { + id: 'demo-basic', + name: ( + + ), + content: , + }, + { + id: 'demo-explorer', + name: ( + + ), + content: , + }, + ], + [] + ); -const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { - const [name, setName] = useState('Waldo'); - const [confirmationText, setConfirmationText] = useState(''); return ( - - - Ui Actions Explorer - - - -

- By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking - this button will cause it to be executed immediately. -

-
- uiActionsApi.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} - > - Say hello world! - - - -

- Lets dynamically add new actions to this trigger. After you click this button, click - the above button again. This time it should offer you multiple options to choose - from. Using the UI Action and Trigger API makes your plugin extensible by other - plugins. Any actions attached to the `HELLO_WORLD_TRIGGER_ID` will show up here! -

- setName(e.target.value)} /> - { - const dynamicAction = createAction({ - id: `${ACTION_HELLO_WORLD}-${name}`, - type: ACTION_HELLO_WORLD, - getDisplayName: () => `Say hello to ${name}`, - execute: async () => { - const overlay = openModal( - toMountPoint( - - - {`Hello ${name}`} - {' '} - overlay.close()}> - Close - - - ) - ); - }, - }); - uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); - setConfirmationText( - `You've successfully added a new action: ${dynamicAction.getDisplayName({ - trigger: uiActionsApi.getTrigger(HELLO_WORLD_TRIGGER_ID), - })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` - ); - }} - > - Say hello to me! - - {confirmationText !== '' ? {confirmationText} : undefined} -
- - - - - - - - -
-
-
-
+ + + + + +

+ +

+
+
+ + + + + +
+
+
); }; -export const renderApp = (props: Props, { element }: AppMountParameters) => { - ReactDOM.render(, element); +export const renderApp = ( + coreStart: CoreStart, + { uiActions }: UiActionsExplorerStartDependencies, + { element }: AppMountParameters +) => { + const services: UiActionsExplorerServices = { + ...coreStart, + uiActions, + }; + ReactDOM.render( + + + , + element + ); return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/examples/ui_actions_explorer/public/basic_tab.tsx b/examples/ui_actions_explorer/public/basic_tab.tsx new file mode 100644 index 00000000000..21f6f647df7 --- /dev/null +++ b/examples/ui_actions_explorer/public/basic_tab.tsx @@ -0,0 +1,109 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; + +import { + EuiButton, + EuiCallOut, + EuiSpacer, + EuiText, + EuiFieldText, + EuiModalBody, +} from '@elastic/eui'; + +import { TriggerContextExample } from './trigger_context_example'; +import { ContextMenuExamples } from './context_menu_examples'; +import { UiActionsExplorerServices } from './types'; +import { HELLO_WORLD_TRIGGER_ID, ACTION_HELLO_WORLD } from '../../ui_action_examples/public'; +import { + toMountPoint, + useOpenSearchDashboards, +} from '../../../src/plugins/opensearch_dashboards_react/public'; +import { createAction } from '../../../src/plugins/ui_actions/public'; + +export const BasicTab = () => { + const [name, setName] = useState('Waldo'); + const [confirmationText, setConfirmationText] = useState(''); + const { + services: { + uiActions, + overlays: { openModal }, + }, + } = useOpenSearchDashboards(); + + return ( + <> + + +

+ By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking this + button will cause it to be executed immediately. +

+
+ + uiActions.executeTriggerActions(HELLO_WORLD_TRIGGER_ID, {})} + > + Say hello world! + + + + +

+ Lets dynamically add new actions to this trigger. After you click this button, click the + above button again. This time it should offer you multiple options to choose from. Using + the UI Action and Trigger API makes your plugin extensible by other plugins. Any actions + attached to the `HELLO_WORLD_TRIGGER_ID` will show up here! +

+ setName(e.target.value)} /> + + + { + const dynamicAction = createAction({ + id: `${ACTION_HELLO_WORLD}-${name}`, + type: ACTION_HELLO_WORLD, + getDisplayName: () => `Say hello to ${name}`, + execute: async () => { + const overlay = openModal( + toMountPoint( + + + {`Hello ${name}`} + {' '} + overlay.close()}> + Close + + + ) + ); + }, + }); + uiActions.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); + setConfirmationText( + `You've successfully added a new action: ${dynamicAction.getDisplayName({ + trigger: uiActions.getTrigger(HELLO_WORLD_TRIGGER_ID), + })}. Refresh the page to reset state. It's up to the user of the system to persist state like this.` + ); + }} + > + Say hello to me! + + {confirmationText !== '' ? {confirmationText} : undefined} +
+ + + + + + + + + + ); +}; diff --git a/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx b/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx index d38d80d0fac..b01d04c1608 100644 --- a/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx +++ b/examples/ui_actions_explorer/public/context_menu_examples/context_menu_examples.tsx @@ -44,6 +44,8 @@ export const ContextMenuExamples: React.FC = () => {

Below examples show how context menu panels look with varying number of actions and how the actions can be grouped into different panels using grouping field. + Grouping can only be one layer deep. A group needs to have at least two items for grouping + to work. A separator is automatically added between groups.

diff --git a/examples/ui_actions_explorer/public/explorer_tab.tsx b/examples/ui_actions_explorer/public/explorer_tab.tsx new file mode 100644 index 00000000000..dc9a6355e6c --- /dev/null +++ b/examples/ui_actions_explorer/public/explorer_tab.tsx @@ -0,0 +1,87 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiCallOut, EuiTitle, EuiSpacer, EuiBasicTable } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import React, { useMemo } from 'react'; +import { UiActionsExplorerServices } from './types'; +import { useOpenSearchDashboards } from '../../../src/plugins/opensearch_dashboards_react/public'; +import {} from '../../../src/plugins/ui_actions/public'; + +interface TriggerItem { + actions: string[]; + id: any; + title?: string | undefined; + description?: string | undefined; +} + +export const ExplorerTab = () => { + const { + services: { uiActions }, + } = useOpenSearchDashboards(); + const triggers: TriggerItem[] = useMemo( + () => + Array.from(uiActions.getTriggers().values()).map(({ trigger }) => { + return { + ...trigger, + actions: uiActions.getTriggerActions(trigger.id).map((action) => action.id), + }; + }), + [uiActions] + ); + + return ( + <> + + + + + + + +

Triggers

+
+ + ( +
    + {actions.map((action) => ( +
  • {action}
  • + ))} +
+ ), + }, + ]} + items={triggers} + /> + + ); +}; diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index 819ddf5feb4..a0eaf1d8e0d 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -28,7 +28,6 @@ * under the License. */ -import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public'; import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; import { PHONE_TRIGGER, @@ -50,17 +49,12 @@ import { ACTION_TRIGGER_PHONE_USER, createTriggerPhoneTriggerAction, } from './actions/actions'; -import { DeveloperExamplesSetup } from '../../developer_examples/public'; import image from './ui_actions.png'; - -interface StartDeps { - uiActions: UiActionsStart; -} - -interface SetupDeps { - uiActions: UiActionsSetup; - developerExamples: DeveloperExamplesSetup; -} +import { + UiActionsExplorerPluginSetup, + UiActionsExplorerPluginStart, + UiActionsExplorerStartDependencies, +} from './types'; declare module '../../../src/plugins/ui_actions/public' { export interface TriggerContextMapping { @@ -79,8 +73,12 @@ declare module '../../../src/plugins/ui_actions/public' { } } -export class UiActionsExplorerPlugin implements Plugin { - public setup(core: CoreSetup, deps: SetupDeps) { +export class UiActionsExplorerPlugin + implements Plugin { + public setup( + core: CoreSetup, + deps: UiActionsExplorerPluginSetup + ) { deps.uiActions.registerTrigger({ id: COUNTRY_TRIGGER, }); @@ -116,10 +114,7 @@ export class UiActionsExplorerPlugin implements Plugin(); + const columns = [ { id: 'name', @@ -118,12 +120,12 @@ export function TriggerContextExample({ uiActionsApi }: Props) { const updateUser = (newUser: User, oldName: string) => { const index = rows.findIndex((u) => u.name === oldName); const newRows = [...rows]; - newRows.splice(index, 1, createRowData(newUser, uiActionsApi, updateUser)); + newRows.splice(index, 1, createRowData(newUser, uiActions, updateUser)); setRows(newRows); }; const initialRows: UserRowData[] = rawData.map((user: User) => - createRowData(user, uiActionsApi, updateUser) + createRowData(user, uiActions, updateUser) ); const [rows, setRows] = useState(initialRows); diff --git a/examples/ui_actions_explorer/public/types.ts b/examples/ui_actions_explorer/public/types.ts new file mode 100644 index 00000000000..d2e009dbd9f --- /dev/null +++ b/examples/ui_actions_explorer/public/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { CoreStart } from '../../../src/core/public'; +import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; + +export interface UiActionsExplorerPluginStart { + uiActions: UiActionsStart; +} + +export interface UiActionsExplorerPluginSetup { + uiActions: UiActionsSetup; + developerExamples: DeveloperExamplesSetup; +} + +export interface UiActionsExplorerStartDependencies { + uiActions: UiActionsStart; +} + +export interface UiActionsExplorerServices extends CoreStart { + uiActions: UiActionsStart; +} diff --git a/package.json b/package.json index 43741261732..e791a29ca5d 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,12 @@ "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", "checkLicenses": "node scripts/check_licenses --dev", + "notice:validate": "node scripts/notice --validate", + "notice:generate": "node scripts/notice", "build-platform": "node scripts/build", "build": "node scripts/build --all-platforms", "start": "node scripts/opensearch_dashboards --dev", + "start:docker": "node scripts/opensearch_dashboards --dev --opensearch.hosts=$OPENSEARCH_HOSTS --opensearch.ignoreVersionMismatch=true --server.host=$SERVER_HOST", "debug": "node --nolazy --inspect scripts/opensearch_dashboards --dev", "debug-break": "node --nolazy --inspect-brk scripts/opensearch_dashboards --dev", "lint": "yarn run lint:es && yarn run lint:style", @@ -71,7 +74,7 @@ "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", "osd:bootstrap": "node scripts/build_ts_refs && node scripts/register_git_hook", "spec_to_console": "node scripts/spec_to_console", - "pkg-version": "./dev-tools/get-version.sh" + "pkg-version": "node -e \"console.log(require('./package.json').version)\"" }, "repository": { "type": "git", @@ -81,20 +84,19 @@ "**/@types/node": "^14.17.32", "**/ansi-regex": "^5.0.1", "**/async": "^3.2.3", - "**/axios": "^0.27.2", "**/d3-color": "^3.1.0", "**/glob-parent": "^6.0.0", "**/hoist-non-react-statics": "^3.3.2", "**/json-schema": "^0.4.0", "**/kind-of": ">=6.0.3", - "**/loader-utils": "^2.0.3", - "**/node-jose": "^2.1.0", + "**/loader-utils": "^2.0.4", + "**/node-jose": "^2.2.0", "**/nth-check": "^2.0.1", - "**/qs": "^6.10.3", "**/trim": "^0.0.3", "**/typescript": "4.0.2", "**/unset-value": "^2.0.1", - "**/minimatch": "^3.0.5" + "**/jest-config": "npm:@amoo-miki/jest-config@27.5.1", + "**/jest-jasmine2": "npm:@amoo-miki/jest-jasmine2@27.5.1" }, "workspaces": { "packages": [ @@ -132,7 +134,7 @@ "@hapi/podium": "^4.1.3", "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", - "@opensearch-project/opensearch": "^1.1.0", + "@opensearch-project/opensearch": "^2.1.0", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", "@osd/apm-config-loader": "1.0.0", @@ -161,10 +163,11 @@ "commander": "^3.0.2", "core-js": "^3.6.5", "deep-freeze-strict": "^1.1.1", - "del": "^5.1.0", + "del": "^6.1.1", "dns-sync": "^0.2.1", "elastic-apm-node": "^3.7.0", "elasticsearch": "^16.7.0", + "http-aws-es": "6.0.0", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", @@ -195,7 +198,7 @@ "pegjs": "0.10.0", "proxy-from-env": "1.0.0", "query-string": "^6.13.2", - "re2": "^1.15.4", + "re2": "1.17.4", "react": "^16.14.0", "react-dom": "^16.12.0", "react-input-range": "^1.3.0", @@ -229,7 +232,6 @@ "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/filesaver": "1.1.2", - "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.1.0", "@microsoft/api-documenter": "^7.13.78", "@microsoft/api-extractor": "^7.19.3", @@ -260,12 +262,12 @@ "@types/bluebird": "^3.1.1", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.31", - "@types/chromedriver": "^81.0.0", "@types/color": "^3.0.0", "@types/d3": "^3.5.43", "@types/dedent": "^0.7.0", "@types/deep-freeze-strict": "^1.1.0", "@types/delete-empty": "^2.0.0", + "@types/dompurify": "^2.3.3", "@types/elasticsearch": "^5.0.33", "@types/enzyme": "^3.10.7", "@types/eslint": "^6.1.3", @@ -319,7 +321,7 @@ "@types/sinon": "^7.0.13", "@types/strip-ansi": "^5.2.1", "@types/styled-components": "^5.1.19", - "@types/supertest": "^2.0.11", + "@types/supertest": "^2.0.12", "@types/supertest-as-promised": "^2.0.38", "@types/tapable": "^1.0.6", "@types/tar": "^4.0.3", @@ -334,6 +336,7 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", + "@types/http-aws-es": "6.0.2", "angular-aria": "^1.8.0", "angular-mocks": "^1.8.2", "angular-recursion": "^1.0.5", @@ -347,13 +350,14 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^100.0.0", + "chromedriver": "^107.0.3", "classnames": "2.3.1", "compare-versions": "3.5.1", "d3": "3.5.17", "d3-cloud": "1.2.5", "dedent": "^0.7.0", "delete-empty": "^2.0.0", + "dompurify": "^2.4.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", "enzyme-to-json": "^3.5.0", @@ -377,7 +381,7 @@ "exit-hook": "^2.2.0", "fetch-mock": "^7.3.9", "fp-ts": "^2.3.1", - "geckodriver": "^3.0.1", + "geckodriver": "^3.0.2", "getopts": "^2.2.5", "grunt": "^1.5.2", "grunt-available-tasks": "^0.6.3", @@ -400,7 +404,7 @@ "leaflet": "1.5.1", "leaflet-draw": "0.4.14", "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "npm:@amoo-miki/leaflet-vega@0.8.8", + "leaflet-vega": "^0.9.0", "leaflet.heat": "0.2.0", "less": "^4.1.2", "license-checker": "^16.0.0", @@ -408,7 +412,7 @@ "load-grunt-config": "^4.0.1", "load-json-file": "^6.2.0", "markdown-it": "^12.3.2", - "mocha": "^7.2.0", + "mocha": "10.1.0", "mock-fs": "^4.12.0", "monaco-editor": "~0.17.0", "ms-chromium-edge-driver": "^0.4.3", @@ -439,12 +443,12 @@ "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.1", "selenium-webdriver": "^4.0.0-alpha.7", - "simple-git": "^3.4.0", + "simple-git": "^3.16.0", "sinon": "^7.4.2", "strip-ansi": "^6.0.0", "stylelint": "^14.5.2", "stylelint-config-standard-scss": "^3.0.0", - "supertest": "^6.2.2", + "supertest": "^6.3.3", "supertest-as-promised": "^4.0.2", "tape": "^5.0.1", "topojson-client": "3.0.0", @@ -452,18 +456,18 @@ "tree-kill": "^1.2.2", "typescript": "4.0.2", "ui-select": "0.19.8", - "vega": "^5.17.3", + "vega": "^5.23.0", "vega-interpreter": "npm:@amoo-miki/vega-forced-csp-compliant-interpreter@1.0.6", - "vega-lite": "^4.16.8", + "vega-lite": "^5.6.0", "vega-schema-url-parser": "^2.1.0", - "vega-tooltip": "^0.24.2", + "vega-tooltip": "^0.30.0", "vinyl-fs": "^3.0.3", "xml2js": "^0.4.22", "xmlbuilder": "13.0.2", "zlib": "^1.0.5" }, "engines": { - "node": "14.20.0", - "yarn": "^1.21.1" + "node": "^14.20.1", + "yarn": "^1.22.10" } } diff --git a/packages/osd-babel-preset/common_preset.js b/packages/osd-babel-preset/common_preset.js index 3905aab6604..3b0bdf22fec 100644 --- a/packages/osd-babel-preset/common_preset.js +++ b/packages/osd-babel-preset/common_preset.js @@ -32,27 +32,27 @@ const plugins = [ require.resolve('babel-plugin-add-module-exports'), // The class properties proposal was merged with the private fields proposal - // into the "class fields" proposal. Babel doesn't support this combined - // proposal yet, which includes private field, so this transform is - // TECHNICALLY stage 2, but for all intents and purposes it's stage 3 - // + // into the "class fields" proposal which is stage 3. // See https://github.com/babel/proposals/issues/12 for progress require.resolve('@babel/plugin-proposal-class-properties'), - // Optional Chaining proposal is stage 3 (https://github.com/tc39/proposal-optional-chaining) + // Optional Chaining proposal is stage 4 (https://github.com/tc39/proposal-optional-chaining) // Need this since we are using TypeScript 3.7+ require.resolve('@babel/plugin-proposal-optional-chaining'), - // Nullish coalescing proposal is stage 3 (https://github.com/tc39/proposal-nullish-coalescing) + // Nullish coalescing proposal is stage 4 (https://github.com/tc39/proposal-nullish-coalescing) // Need this since we are using TypeScript 3.7+ require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), - // Proposal is on stage 4 (https://github.com/tc39/proposal-export-ns-from) + // Proposal is merged into ECMA-262 (https://github.com/tc39/proposal-export-ns-from) // Need this since we are using TypeScript 3.8+ require.resolve('@babel/plugin-proposal-export-namespace-from'), - // Proposal is on stage 4 (https://github.com/tc39/proposal-export-ns-from) + // Proposal is merged into ECMA-262 (https://github.com/tc39/proposal-export-ns-from) // Need this since we are using TypeScript 3.9+ require.resolve('@babel/plugin-proposal-private-methods'), + + // Proposal is on stage 4 (https://github.com/tc39/proposal-logical-assignment) + require.resolve('@babel/plugin-proposal-logical-assignment-operators'), ]; module.exports = { diff --git a/packages/osd-babel-preset/package.json b/packages/osd-babel-preset/package.json index 67ef872075e..32666631479 100644 --- a/packages/osd-babel-preset/package.json +++ b/packages/osd-babel-preset/package.json @@ -9,6 +9,7 @@ "dependencies": { "@babel/plugin-proposal-class-properties": "^7.16.5", "@babel/plugin-proposal-export-namespace-from": "^7.16.5", + "@babel/plugin-proposal-logical-assignment-operators": "^7.16.5", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.5", "@babel/plugin-proposal-optional-chaining": "^7.16.5", "@babel/plugin-proposal-private-methods": "^7.16.5", diff --git a/packages/osd-config-schema/src/errors/schema_error.test.ts b/packages/osd-config-schema/src/errors/schema_error.test.ts index 345304c955e..c134c888d51 100644 --- a/packages/osd-config-schema/src/errors/schema_error.test.ts +++ b/packages/osd-config-schema/src/errors/schema_error.test.ts @@ -31,7 +31,7 @@ import { relative, sep } from 'path'; import { SchemaError } from '.'; -import { standardize, PROCESS_WORKING_DIR } from '@osd/cross-platform'; +import { standardize, getRepoRoot } from '@osd/cross-platform'; /** * Make all paths in stacktrace relative. @@ -48,7 +48,7 @@ export const cleanStack = (stack: string) => } const path = parts[1]; - const relativePath = standardize(relative(PROCESS_WORKING_DIR, path)); + const relativePath = standardize(relative(getRepoRoot(path) || '.', path)); return line.replace(path, relativePath); }) diff --git a/packages/osd-cross-platform/README.md b/packages/osd-cross-platform/README.md index a61a2f184f0..0583b295c5f 100644 --- a/packages/osd-cross-platform/README.md +++ b/packages/osd-cross-platform/README.md @@ -1,3 +1,28 @@ # `@osd/cross-platform` β€” OpenSearch Dashboards cross-platform helpers -This package contains the helper functions to work around the differences of platforms +This package contains the helpers to work around the differences across platforms, such as the difference in the path segment separator and the possibility of referencing a path using the short 8.3 name (SFN), a long name, and a long UNC on Windows. + +Some helpers are functions that `standardize` the reference to a path or help `getRepoRoot`, and some are constants referencing the `PROCESS_WORKING_DIR` or `REPO_ROOT`. + +### Example + +When the relative reference of `path` to the working directory is needed, using the code below would produce different results on Linux that it would on Windows and if the process was started in a Windows shell that used short paths, the results differ from a Windows shell that used long paths. +```js +import { relative } from 'path'; + +const relativePath = relative(process.cwd(), path); + +// Output on Linux: relative-path/to/a/file +// Windows: relative-path\to\a\file +// Windows SFN: RELATI~1\to\a\file +``` + +To avoid those differences, helper functions and constants can be used: +```js +import { relative } from 'path'; +import { standardize, PROCESS_WORKING_DIR } from '@osd/cross-platform'; + +const relativePath = standardize(relative(PROCESS_WORKING_DIR, path)); + +// Output: relative-path/to/a/file +``` \ No newline at end of file diff --git a/packages/osd-cross-platform/src/__snapshots__/path.test.ts.snap b/packages/osd-cross-platform/src/__snapshots__/path.test.ts.snap new file mode 100644 index 00000000000..20c44ec1c2d --- /dev/null +++ b/packages/osd-cross-platform/src/__snapshots__/path.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cross Platform path standardize on POSIX-compatible platforms ignores additional parameters 1`] = `"/a/b/c"`; + +exports[`Cross Platform path standardize on POSIX-compatible platforms produces a path in POSIX format 1`] = `"/a/b/c"`; + +exports[`Cross Platform path standardize on Windows produces a path in POSIX format 1`] = `"C:/a/b/c"`; + +exports[`Cross Platform path standardize on Windows produces a path in native format 1`] = `"C:\\\\a\\\\b\\\\c"`; + +exports[`Cross Platform path standardize on Windows produces a path in native format even for POSIX input 1`] = `"C:\\\\a\\\\b\\\\c"`; + +exports[`Cross Platform path standardize on Windows produces a path in native format with escaped backslashes 1`] = `"C:\\\\\\\\a\\\\\\\\b\\\\\\\\c"`; diff --git a/packages/osd-cross-platform/src/index.ts b/packages/osd-cross-platform/src/index.ts index 343d7e9257a..bc05aa9a955 100644 --- a/packages/osd-cross-platform/src/index.ts +++ b/packages/osd-cross-platform/src/index.ts @@ -5,3 +5,4 @@ export * from './path'; export * from './process'; +export * from './repo_root'; diff --git a/packages/osd-cross-platform/src/path.test.ts b/packages/osd-cross-platform/src/path.test.ts new file mode 100644 index 00000000000..ed405298ad1 --- /dev/null +++ b/packages/osd-cross-platform/src/path.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'path'; +import fs from 'fs'; +import { access, rmdir, mkdir, writeFile, symlink } from 'fs/promises'; + +import { + resolveToFullNameSync, + resolveToFullPathSync, + resolveToShortNameSync, + resolveToShortPathSync, + shortNamesSupportedSync, + realPathSync, + realShortPathSync, + standardize, +} from './path'; + +const tmpTestFolder = './__test_artifacts__'; +const longFolderName = '.long-folder-name'; +const longFileName = '.long-file-name.txt'; +const longSymlinkName = '.sym.link'; +const shortFolderName = 'LONG-F~1'; +const shortFileName = 'LONG-F~1.TXT'; +const dummyWindowsPath = 'C:\\a\\b\\c'; +const dummyWindowsPOSIXPath = 'C:/a/b/c'; +const dummyPOSIXPath = '/a/b/c'; + +const onWindows = process.platform === 'win32' ? describe : xdescribe; +const onWindowsWithShortNames = shortNamesSupportedSync() ? describe : xdescribe; + +// Save the real process.platform +const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!; + +describe('Cross Platform', () => { + describe('path', () => { + onWindows('on Windows', () => { + onWindowsWithShortNames('when 8.3 is supported', () => { + beforeAll(async () => { + // Cleanup + try { + // If leftover artifacts were found, get rid of them + await access(tmpTestFolder); + await rmdir(tmpTestFolder, { recursive: true }); + } catch (ex) { + // Do nothing; if `rmdir` failed, let the `mkdir` below throw the error + } + + await mkdir(tmpTestFolder); + await mkdir(path.resolve(tmpTestFolder, longFolderName)); + await writeFile(path.resolve(tmpTestFolder, longFolderName, longFileName), ''); + await symlink( + path.resolve(tmpTestFolder, longFolderName), + path.resolve(tmpTestFolder, longSymlinkName), + 'junction' + ); + }); + + afterAll(async () => { + try { + await rmdir(tmpTestFolder, { recursive: true }); + } catch (ex) { + // Do nothing + } + }); + + it('can synchronously extract full name of a folder', () => { + const name = path.basename( + resolveToFullPathSync(path.resolve(tmpTestFolder, shortFolderName)) + ); + expect(name).toBe(longFolderName); + }); + + it('can synchronously extract full name of a file', () => { + const name = path.basename( + resolveToFullNameSync(path.resolve(tmpTestFolder, shortFolderName, shortFileName)) + ); + expect(name).toBe(longFileName); + }); + + it('can synchronously extract short name of a folder', () => { + const name = path.basename( + resolveToShortPathSync(path.resolve(tmpTestFolder, longFolderName)) + ); + expect(name).toBe(shortFolderName); + }); + + it('can synchronously extract short name of a file', () => { + const name = path.basename( + resolveToShortNameSync(path.resolve(tmpTestFolder, longFolderName, longFileName)) + ); + expect(name).toBe(shortFileName); + }); + + it('can synchronously extract full name of a symbolic link', () => { + const name = path.basename(realPathSync(path.resolve(tmpTestFolder, longSymlinkName))); + expect(name).toBe(longFolderName); + }); + + it('can synchronously extract short name of a symbolic link', () => { + const name = path.basename( + realShortPathSync(path.resolve(tmpTestFolder, longSymlinkName)) + ); + expect(name).toBe(shortFolderName); + }); + }); + }); + + describe('on platforms other than Windows', () => { + let mockPathNormalize: jest.SpyInstance; + let mockPathResolve: jest.SpyInstance; + let mockFSRealPathSync: jest.SpyInstance; + + beforeAll(() => { + Object.defineProperty(process, 'platform', { + ...Object.getOwnPropertyDescriptor(process, 'property'), + value: 'linux', + }); + + mockPathNormalize = jest.spyOn(path, 'normalize').mockReturnValue(dummyPOSIXPath); + mockPathResolve = jest.spyOn(path, 'resolve').mockReturnValue(dummyPOSIXPath); + mockFSRealPathSync = jest + .spyOn(fs, 'realpathSync') + .mockReturnValue(dummyPOSIXPath) as jest.SpyInstance; + }); + + afterAll(() => { + // Restore the real property value after each test + Object.defineProperty(process, 'platform', realPlatform); + mockPathNormalize.mockRestore(); + mockPathResolve.mockRestore(); + mockFSRealPathSync.mockRestore(); + }); + + it('all short and full name methods return just the normalized paths', () => { + expect(shortNamesSupportedSync()).toBe(false); + expect(resolveToFullPathSync(dummyPOSIXPath)).toBe(dummyPOSIXPath); + expect(resolveToShortPathSync(dummyPOSIXPath)).toBe(dummyPOSIXPath); + }); + }); + + describe('standardize', () => { + describe('on Windows', () => { + let mockPathNormalize: jest.SpyInstance; + + beforeAll(() => { + Object.defineProperty(process, 'platform', { + ...Object.getOwnPropertyDescriptor(process, 'property'), + value: 'win32', + }); + + mockPathNormalize = jest.spyOn(path, 'normalize').mockReturnValue(dummyWindowsPath); + }); + + afterAll(() => { + // Restore the real property value after each test + Object.defineProperty(process, 'platform', realPlatform); + mockPathNormalize.mockRestore(); + }); + + it('produces a path in native format', () => { + expect(standardize(dummyWindowsPath, false, false)).toMatchSnapshot(); + }); + + it('produces a path in native format even for POSIX input', () => { + expect(standardize(dummyWindowsPOSIXPath, false, false)).toMatchSnapshot(); + }); + + it('produces a path in native format with escaped backslashes', () => { + expect(standardize(dummyWindowsPath, false, true)).toMatchSnapshot(); + }); + + it('produces a path in POSIX format', () => { + expect(standardize(dummyWindowsPath)).toMatchSnapshot(); + }); + }); + + describe('on POSIX-compatible platforms', () => { + let mockPathNormalize: jest.SpyInstance; + + beforeAll(() => { + Object.defineProperty(process, 'platform', { + ...Object.getOwnPropertyDescriptor(process, 'property'), + value: 'linux', + }); + + mockPathNormalize = jest.spyOn(path, 'normalize').mockReturnValue(dummyPOSIXPath); + }); + + afterAll(() => { + // Restore the real property value after each test + Object.defineProperty(process, 'platform', realPlatform); + mockPathNormalize.mockRestore(); + }); + + it('produces a path in POSIX format', () => { + expect(standardize(dummyPOSIXPath)).toMatchSnapshot(); + }); + + it('ignores additional parameters', () => { + expect(standardize(dummyPOSIXPath, false, true)).toMatchSnapshot(); + }); + }); + }); + }); +}); diff --git a/packages/osd-cross-platform/src/path.ts b/packages/osd-cross-platform/src/path.ts index 7e37443f1bc..d5b2befb56c 100644 --- a/packages/osd-cross-platform/src/path.ts +++ b/packages/osd-cross-platform/src/path.ts @@ -3,28 +3,158 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { normalize } from 'path'; +import { execSync } from 'child_process'; +import { basename, normalize, resolve } from 'path'; +import { + realpathSync as nativeRealpathSync, + openSync, + closeSync, + existsSync, + unlinkSync, +} from 'fs'; + +export const NAMESPACE_PREFIX = process.platform === 'win32' ? '\\\\?\\' : ''; /** * Get a standardized reference to a path * @param {string} path - the path to standardize * @param {boolean} [usePosix=true] - produce a posix reference * @param {boolean} [escapedBackslashes=true] - on Windows, double-backslash the reference - * @internal + * @param {boolean} [returnUNC=false] - produce an extended reference */ export const standardize = ( path: string, usePosix: boolean = true, - escapedBackslashes: boolean = true + escapedBackslashes: boolean = true, + returnUNC: boolean = false ) => { - /* Force os-dependant separators - * path.posix.normalize doesn't convert backslashes to slashes on Windows so we manually force it afterwards - */ + // Force os-dependant separators const normal = normalize(path); // Filter out in-browser executions as well as non-windows ones if (process?.platform !== 'win32') return normal; if (usePosix) return normal.replace(/\\/g, '/'); - return escapedBackslashes ? normal.replace(/\\/g, '\\\\') : normal; + else if (escapedBackslashes) return normal.replace(/\\/g, '\\\\'); + else if (returnUNC) return '\\\\?\\' + normal; + return normal; +}; + +/** + * Windows-only function that uses PowerShell to calculate the full path + * @param {string} path + * @private + */ +const getFullPathSync = (path: string) => { + if (process.platform !== 'win32') return path; + + try { + const fullName = execSync(`powershell "(Get-Item -LiteralPath '${path}').FullName"`, { + encoding: 'utf8', + })?.trim?.(); + + // Make sure we got something back + if (fullName?.length > 2) return fullName; + } catch (ex) { + // Do nothing + } + + return path; +}; + +/** + * Windows-only function that uses PowerShell and Com Object to calculate the 8.3 path + * @param {string} path + * @private + */ +const getShortPathSync = (path: string) => { + if (process.platform !== 'win32') return path; + + try { + const shortPath = execSync( + `powershell "$FSO = New-Object -ComObject Scripting.FileSystemObject; $O = (Get-Item -LiteralPath '${path}'); if ($O.PSIsContainer) { $FSO.GetFolder($O.FullName).ShortPath } else { $FSO.GetFile($O.FullName).ShortPath }"`, + { + encoding: 'utf8', + } + )?.trim?.(); + + // Make sure we got something back + if (shortPath?.length > 2) return shortPath; + } catch (ex) { + // Do nothing + } + + return path; }; + +/** + * Checks if Windows 8.3 short names are supported on the volume of the given path + * @param {string} [path='.'] - the path to examine + */ +export const shortNamesSupportedSync = (path: string = '.') => { + if (process.platform !== 'win32') return false; + + const testFileName = '.___osd-cross-platform-test.file'; + const file = resolve(path, testFileName); + + // Create a test file if it doesn't exist + if (!existsSync(file)) closeSync(openSync(file, 'w')); + + // If the returned value's basename is not the same as the requested file name, it must be a short name + const foundShortName = basename(getShortPathSync(file)) !== testFileName; + + // Cleanup + unlinkSync(file); + + return foundShortName; +}; + +/** + * @borrows shortNamesSupportedSync + */ +export const shortNameSupportedSync = shortNamesSupportedSync; + +/** + * Get the full pathname + * @param {string} path - the path to resolve + */ +export const resolveToFullPathSync = (path: string) => getFullPathSync(resolve(path)); + +/** + * @borrows resolveToFullPathSync + */ +export const resolveToFullNameSync = resolveToFullPathSync; + +/** + * Get the short pathname + * @param {string} path - the path to resolve + */ +export const resolveToShortPathSync = (path: string) => getShortPathSync(resolve(path)); + +/** + * @borrows resolveToShortPathSync + */ +export const resolveToShortNameSync = resolveToShortPathSync; + +/** + * Get the canonical pathname + * @param {string} path - the path to resolve + */ +export const realPathSync = (path: string) => getFullPathSync(nativeRealpathSync(path, 'utf8')); + +/** + * @borrows realPathSync + */ +export const realpathSync = realPathSync; + +/** + * Get the canonical pathname + * @param {string} path - the path to resolve + */ +export const realShortPathSync = (path: string) => + getShortPathSync(nativeRealpathSync(path, 'utf8')); + +/** + * @borrows realShortPathSync + */ +export const realshortpathSync = realShortPathSync; diff --git a/packages/osd-cross-platform/src/process.ts b/packages/osd-cross-platform/src/process.ts index fa593c94368..ade19cca561 100644 --- a/packages/osd-cross-platform/src/process.ts +++ b/packages/osd-cross-platform/src/process.ts @@ -3,20 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { execSync } from 'child_process'; +import { resolveToFullPathSync, standardize } from './path'; -let workingDir = process.cwd(); - -if (process.platform === 'win32') { - try { - const pathFullName = execSync('powershell "(Get-Item -LiteralPath $pwd).FullName"', { - cwd: workingDir, - encoding: 'utf8', - })?.trim?.(); - if (pathFullName?.length > 2) workingDir = pathFullName; - } catch (ex) { - // Do nothing - } -} +/** + * The full pathname of the working directory of the process + * @constant + * @type {string} + */ +export const PROCESS_WORKING_DIR: string = resolveToFullPathSync(process.cwd()); -export const PROCESS_WORKING_DIR = workingDir; +/** + * The full pathname of the working directory of the process, in POSIX format + * @constant + * @type {string} + */ +export const PROCESS_POSIX_WORKING_DIR: string = standardize(PROCESS_WORKING_DIR); diff --git a/packages/osd-utils/src/repo_root.ts b/packages/osd-cross-platform/src/repo_root.ts similarity index 64% rename from packages/osd-utils/src/repo_root.ts rename to packages/osd-cross-platform/src/repo_root.ts index e46f62746b0..a7ffc19a7f7 100644 --- a/packages/osd-utils/src/repo_root.ts +++ b/packages/osd-cross-platform/src/repo_root.ts @@ -28,21 +28,16 @@ * under the License. */ -import Path from 'path'; -import Fs from 'fs'; +import { resolve, parse, dirname, isAbsolute, relative } from 'path'; import loadJsonFile from 'load-json-file'; +import { resolveToFullPathSync, resolveToShortPathSync, realPathSync } from './path'; const readOpenSearchDashboardsPkgJson = (dir: string) => { try { - const path = Path.resolve(dir, 'package.json'); - const json = loadJsonFile.sync(path); - if ( - json && - typeof json === 'object' && - 'name' in json && - json.name === 'opensearch-dashboards' - ) { + const path = resolve(dir, 'package.json'); + const json = loadJsonFile.sync(path) as { [key: string]: any }; + if (json?.name === 'opensearch-dashboards') { return json; } } catch (error) { @@ -58,8 +53,8 @@ const findOpenSearchDashboardsPackageJson = () => { // search for the opensearch-dashboards directory, since this file is moved around it might // not be where we think but should always be a relatively close parent // of this directory - const startDir = Fs.realpathSync(__dirname); - const { root: rootDir } = Path.parse(startDir); + const startDir = realPathSync(__dirname); + const { root: rootDir } = parse(startDir); let cursor = startDir; while (true) { const opensearchDashboardsPkgJson = readOpenSearchDashboardsPkgJson(cursor); @@ -73,7 +68,7 @@ const findOpenSearchDashboardsPackageJson = () => { }; } - const parent = Path.dirname(cursor); + const parent = dirname(cursor); if (parent === rootDir) { throw new Error(`unable to find opensearch-dashboards directory from ${startDir}`); } @@ -86,5 +81,25 @@ const { opensearchDashboardsPkgJson, } = findOpenSearchDashboardsPackageJson(); -export const REPO_ROOT = opensearchDashboardsDir; +export const REPO_ROOT = resolveToFullPathSync(opensearchDashboardsDir); +export const REPO_ROOT_8_3 = resolveToShortPathSync(opensearchDashboardsDir); export const UPSTREAM_BRANCH = opensearchDashboardsPkgJson.branch; + +export const getMatchingRoot = (path: string, rootPaths: string | string[]) => { + const rootPathsArray = Array.isArray(rootPaths) ? rootPaths : [rootPaths]; + + // We can only find the appropriate root if an absolute path was given + if (path && isAbsolute(path)) { + // Return the matching root if one is found or return `undefined` + return rootPathsArray.find((root) => path.startsWith(root)); + } + + return undefined; +}; + +export const getRepoRoot = (path: string) => getMatchingRoot(path, [REPO_ROOT, REPO_ROOT_8_3]); + +export const relativeToRepoRoot = (path: string) => { + const repoRoot = getRepoRoot(path); + return repoRoot ? relative(repoRoot, path) : null; +}; diff --git a/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts index dd7757c594d..939b6e9924d 100644 --- a/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/osd-dev-utils/src/serializers/absolute_path_serializer.ts @@ -28,14 +28,25 @@ * under the License. */ -import { REPO_ROOT } from '@osd/utils'; +// Not importing from @osd/cross-platform to allow some complicated tests to run: suite_tracker.test.ts +import { REPO_ROOT, REPO_ROOT_8_3 } from '@osd/utils'; export function createAbsolutePathSerializer( - rootPath: string = REPO_ROOT, + rootPath: string | string[] = [REPO_ROOT, REPO_ROOT_8_3], replacement = '' ) { + const rootPaths = Array.isArray(rootPath) ? rootPath : [rootPath]; + return { - test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), - serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), + test: (value: any) => + typeof value === 'string' && rootPaths.some((path) => value.startsWith(path)), + serialize: (value: string) => + rootPaths + // Replace all instances of `rootPaths` found at the beginning of the `value` + .reduce( + (result, path) => (result.startsWith(path) ? result.replace(path, replacement) : result), + value + ) + .replace(/\\/g, '/'), }; } diff --git a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js index 6343af30028..52b1b27ba36 100644 --- a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js +++ b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js @@ -58,7 +58,7 @@ function traverseToTopFolder(src, pattern) { const srcIdx = src.lastIndexOf(path.sep); src = src.slice(0, srcIdx); } - return src; + return src.replace(/\\/g, '/').replace(/\/$/, ''); } function isSameFolderOrDescendent(src, imported, pattern) { diff --git a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js index 06e7a9c3941..96251e8fe55 100644 --- a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js +++ b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js @@ -97,6 +97,22 @@ ruleTester.run('@osd/eslint/no-restricted-paths', rule, { }, ], }, + { + code: 'import b from "testfiles/no_restricted_paths/server/deep/deeper/e.js"', + filename: path.join(__dirname, 'testfiles/no_restricted_paths/server/deep/d.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'testfiles/**/server/**/*', + from: 'testfiles/**/server/**/*', + allowSameFolder: true, + }, + ], + }, + ], + }, // irrelevant function calls { diff --git a/packages/osd-i18n/package.json b/packages/osd-i18n/package.json index e3acaba5860..98b3f9bbde1 100644 --- a/packages/osd-i18n/package.json +++ b/packages/osd-i18n/package.json @@ -18,7 +18,7 @@ "@osd/dev-utils": "1.0.0", "@types/intl-relativeformat": "^2.1.0", "@types/react-intl": "^2.3.15", - "del": "^5.1.0", + "del": "^6.1.1", "getopts": "^2.2.5", "supports-color": "^7.0.0", "typescript": "4.0.2" diff --git a/packages/osd-i18n/src/core/locales.js b/packages/osd-i18n/src/core/locales.js index f4ae7aa734a..a837462b740 100644 --- a/packages/osd-i18n/src/core/locales.js +++ b/packages/osd-i18n/src/core/locales.js @@ -34,5 +34,7 @@ addLocaleData({ locale: "ja", pluralRuleFunction: function (n,ord){if(ord)return addLocaleData({ locale: "ja-JP", parentLocale: "ja" }); addLocaleData({ locale: "ko", pluralRuleFunction: function (n,ord){if(ord)return"other";return"other"},"fields":{"year":{"displayName":"λ…„","relative":{"0":"μ˜¬ν•΄","1":"λ‚΄λ…„","-1":"μž‘λ…„"},"relativeTime":{"future":{"other":"{0}λ…„ ν›„"},"past":{"other":"{0}λ…„ μ „"}}},"year-short":{"displayName":"λ…„","relative":{"0":"μ˜¬ν•΄","1":"λ‚΄λ…„","-1":"μž‘λ…„"},"relativeTime":{"future":{"other":"{0}λ…„ ν›„"},"past":{"other":"{0}λ…„ μ „"}}},"month":{"displayName":"μ›”","relative":{"0":"이번 달","1":"λ‹€μŒ 달","-1":"μ§€λ‚œλ‹¬"},"relativeTime":{"future":{"other":"{0}κ°œμ›” ν›„"},"past":{"other":"{0}κ°œμ›” μ „"}}},"month-short":{"displayName":"μ›”","relative":{"0":"이번 달","1":"λ‹€μŒ 달","-1":"μ§€λ‚œλ‹¬"},"relativeTime":{"future":{"other":"{0}κ°œμ›” ν›„"},"past":{"other":"{0}κ°œμ›” μ „"}}},"day":{"displayName":"일","relative":{"0":"였늘","1":"내일","2":"λͺ¨λ ˆ","-2":"κ·Έμ €κ»˜","-1":"μ–΄μ œ"},"relativeTime":{"future":{"other":"{0}일 ν›„"},"past":{"other":"{0}일 μ „"}}},"day-short":{"displayName":"일","relative":{"0":"였늘","1":"내일","2":"λͺ¨λ ˆ","-2":"κ·Έμ €κ»˜","-1":"μ–΄μ œ"},"relativeTime":{"future":{"other":"{0}일 ν›„"},"past":{"other":"{0}일 μ „"}}},"hour":{"displayName":"μ‹œ","relative":{"0":"ν˜„μž¬ μ‹œκ°„"},"relativeTime":{"future":{"other":"{0}μ‹œκ°„ ν›„"},"past":{"other":"{0}μ‹œκ°„ μ „"}}},"hour-short":{"displayName":"μ‹œ","relative":{"0":"ν˜„μž¬ μ‹œκ°„"},"relativeTime":{"future":{"other":"{0}μ‹œκ°„ ν›„"},"past":{"other":"{0}μ‹œκ°„ μ „"}}},"minute":{"displayName":"λΆ„","relative":{"0":"ν˜„μž¬ λΆ„"},"relativeTime":{"future":{"other":"{0}λΆ„ ν›„"},"past":{"other":"{0}λΆ„ μ „"}}},"minute-short":{"displayName":"λΆ„","relative":{"0":"ν˜„μž¬ λΆ„"},"relativeTime":{"future":{"other":"{0}λΆ„ ν›„"},"past":{"other":"{0}λΆ„ μ „"}}},"second":{"displayName":"초","relative":{"0":"μ§€κΈˆ"},"relativeTime":{"future":{"other":"{0}초 ν›„"},"past":{"other":"{0}초 μ „"}}},"second-short":{"displayName":"초","relative":{"0":"μ§€κΈˆ"},"relativeTime":{"future":{"other":"{0}초 ν›„"},"past":{"other":"{0}초 μ „"}}}} }); addLocaleData({ locale: "ko-KR", parentLocale: "ko" }); +addLocaleData({ locale: 'ru', pluralRuleFunction: function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11?"one":v0&&i10>=2&&i10<=4&&(i100<12||i100>14)?"few":(v0&&i10==0)||(v0&&i10>=5&&i10<=9)||(v0&&i100>=11&&i100<=14)?"many":"other";},"fields":{"year":{"displayName":"Π³ΠΎΠ΄","relative":{"0":"Π² этом Π³ΠΎΠ΄Ρƒ","1":"Π² ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌ Π³ΠΎΠ΄Ρƒ","-1":"Π² ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΌ Π³ΠΎΠ΄Ρƒ"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} Π³ΠΎΠ΄","few":"Ρ‡Π΅Ρ€Π΅Π· {0} Π³ΠΎΠ΄Π°","many":"Ρ‡Π΅Ρ€Π΅Π· {0} Π»Π΅Ρ‚","other":"Ρ‡Π΅Ρ€Π΅Π· {0} Π³ΠΎΠ΄Π°"},"past":{"one":"{0} Π³ΠΎΠ΄ Π½Π°Π·Π°Π΄","few":"{0} Π³ΠΎΠ΄Π° Π½Π°Π·Π°Π΄","many":"{0} Π»Π΅Ρ‚ Π½Π°Π·Π°Π΄","other":"{0} Π³ΠΎΠ΄Π° Π½Π°Π·Π°Π΄"}}},"year-short":{"displayName":"Π³.","relative":{"0":"Π² этом Π³.","1":"Π² слСд. Π³.","-1":"Π² ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΌ Π³."},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} Π³.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} Π³.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} Π».","other":"Ρ‡Π΅Ρ€Π΅Π· {0} Π³."},"past":{"one":"{0} Π³. Π½Π°Π·Π°Π΄","few":"{0} Π³. Π½Π°Π·Π°Π΄","many":"{0} Π». Π½Π°Π·Π°Π΄","other":"{0} Π³. Π½Π°Π·Π°Π΄"}}},"month":{"displayName":"мСсяц","relative":{"0":"Π² этом мСсяцС","1":"Π² ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌ мСсяцС","-1":"Π² ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΌ мСсяцС"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} мСсяц","few":"Ρ‡Π΅Ρ€Π΅Π· {0} мСсяца","many":"Ρ‡Π΅Ρ€Π΅Π· {0} мСсяцСв","other":"Ρ‡Π΅Ρ€Π΅Π· {0} мСсяца"},"past":{"one":"{0} мСсяц Π½Π°Π·Π°Π΄","few":"{0} мСсяца Π½Π°Π·Π°Π΄","many":"{0} мСсяцСв Π½Π°Π·Π°Π΄","other":"{0} мСсяца Π½Π°Π·Π°Π΄"}}},"month-short":{"displayName":"мСс.","relative":{"0":"Π² этом мСс.","1":"Π² ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌ мСс.","-1":"Π² ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΌ мСс."},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} мСс.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} мСс.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} мСс.","other":"Ρ‡Π΅Ρ€Π΅Π· {0} мСс."},"past":{"one":"{0} мСс. Π½Π°Π·Π°Π΄","few":"{0} мСс. Π½Π°Π·Π°Π΄","many":"{0} мСс. Π½Π°Π·Π°Π΄","other":"{0} мСс. Π½Π°Π·Π°Π΄"}}},"week":{"displayName":"нСдСля","relativePeriod":"Π½Π° Π½Π΅Π΄Π΅Π»Π΅ {0}","relative":{"0":"Π½Π° этой Π½Π΅Π΄Π΅Π»Π΅","1":"Π½Π° ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ Π½Π΅Π΄Π΅Π»Π΅","-1":"Π½Π° ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΉ Π½Π΅Π΄Π΅Π»Π΅"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} нСдСлю","few":"Ρ‡Π΅Ρ€Π΅Π· {0} Π½Π΅Π΄Π΅Π»ΠΈ","many":"Ρ‡Π΅Ρ€Π΅Π· {0} нСдСль","other":"Ρ‡Π΅Ρ€Π΅Π· {0} Π½Π΅Π΄Π΅Π»ΠΈ"},"past":{"one":"{0} нСдСлю Π½Π°Π·Π°Π΄","few":"{0} Π½Π΅Π΄Π΅Π»ΠΈ Π½Π°Π·Π°Π΄","many":"{0} нСдСль Π½Π°Π·Π°Π΄","other":"{0} Π½Π΅Π΄Π΅Π»ΠΈ Π½Π°Π·Π°Π΄"}}},"week-short":{"displayName":"Π½Π΅Π΄.","relativePeriod":"Π½Π° Π½Π΅Π΄. {0}","relative":{"0":"Π½Π° этой Π½Π΅Π΄.","1":"Π½Π° ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ Π½Π΅Π΄.","-1":"Π½Π° ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΉ Π½Π΅Π΄."},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} Π½Π΅Π΄.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} Π½Π΅Π΄.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} Π½Π΅Π΄.","other":"Ρ‡Π΅Ρ€Π΅Π· {0} Π½Π΅Π΄."},"past":{"one":"{0} Π½Π΅Π΄. Π½Π°Π·Π°Π΄","few":"{0} Π½Π΅Π΄. Π½Π°Π·Π°Π΄","many":"{0} Π½Π΅Π΄. Π½Π°Π·Π°Π΄","other":"{0} Π½Π΅Π΄. Π½Π°Π·Π°Π΄"}}},"day":{"displayName":"дСнь","relative":{"0":"сСгодня","1":"Π·Π°Π²Ρ‚Ρ€Π°","2":"послСзавтра","-1":"Π²Ρ‡Π΅Ρ€Π°","-2":"ΠΏΠΎΠ·Π°Π²Ρ‡Π΅Ρ€Π°"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} дСнь","few":"Ρ‡Π΅Ρ€Π΅Π· {0} дня","many":"Ρ‡Π΅Ρ€Π΅Π· {0} Π΄Π½Π΅ΠΉ","other":"Ρ‡Π΅Ρ€Π΅Π· {0} дня"},"past":{"one":"{0} дСнь Π½Π°Π·Π°Π΄","few":"{0} дня Π½Π°Π·Π°Π΄","many":"{0} Π΄Π½Π΅ΠΉ Π½Π°Π·Π°Π΄","other":"{0} дня Π½Π°Π·Π°Π΄"}}},"day-short":{"displayName":"Π΄Π½.","relative":{"0":"сСгодня","1":"Π·Π°Π²Ρ‚Ρ€Π°","2":"послСзавтра","-1":"Π²Ρ‡Π΅Ρ€Π°","-2":"ΠΏΠΎΠ·Π°Π²Ρ‡Π΅Ρ€Π°"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} Π΄Π½.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} Π΄Π½.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} Π΄Π½.","other":"Ρ‡Π΅Ρ€Π΅Π· {0} Π΄Π½."},"past":{"one":"{0} Π΄Π½. Π½Π°Π·Π°Π΄","few":"{0} Π΄Π½. Π½Π°Π·Π°Π΄","many":"{0} Π΄Π½. Π½Π°Π·Π°Π΄","other":"{0} Π΄Π½. Π½Π°Π·Π°Π΄"}}},"hour":{"displayName":"час","relative":{"0":"Π² этот час"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} час","few":"Ρ‡Π΅Ρ€Π΅Π· {0} часа","many":"Ρ‡Π΅Ρ€Π΅Π· {0} часов","other":"Ρ‡Π΅Ρ€Π΅Π· {0} часа"},"past":{"one":"{0} час Π½Π°Π·Π°Π΄","few":"{0} часа Π½Π°Π·Π°Π΄","many":"{0} часов Π½Π°Π·Π°Π΄","other":"{0} часа Π½Π°Π·Π°Π΄"}}},"hour-short":{"displayName":"Ρ‡","relative":{"0":"Π² этот час"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} Ρ‡.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} Ρ‡.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} Ρ‡.","other":"Ρ‡Π΅Ρ€Π΅Π· {0} Ρ‡."},"past":{"one":"{0} Ρ‡. Π½Π°Π·Π°Π΄","few":"{0} Ρ‡. Π½Π°Π·Π°Π΄","many":"{0} Ρ‡. Π½Π°Π·Π°Π΄","other":"{0} Ρ‡. Π½Π°Π·Π°Π΄"}}},"minute":{"displayName":"ΠΌΠΈΠ½ΡƒΡ‚Π°","relative":{"0":"Π² эту ΠΌΠΈΠ½ΡƒΡ‚Ρƒ"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½ΡƒΡ‚Ρƒ","few":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½ΡƒΡ‚Ρ‹","many":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½ΡƒΡ‚","other":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½ΡƒΡ‚Ρ‹"},"past":{"one":"{0} ΠΌΠΈΠ½ΡƒΡ‚Ρƒ Π½Π°Π·Π°Π΄","few":"{0} ΠΌΠΈΠ½ΡƒΡ‚Ρ‹ Π½Π°Π·Π°Π΄","many":"{0} ΠΌΠΈΠ½ΡƒΡ‚ Π½Π°Π·Π°Π΄","other":"{0} ΠΌΠΈΠ½ΡƒΡ‚Ρ‹ Π½Π°Π·Π°Π΄"}}},"minute-short":{"displayName":"ΠΌΠΈΠ½.","relative":{"0":"Π² эту ΠΌΠΈΠ½ΡƒΡ‚Ρƒ"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½.","other":"Ρ‡Π΅Ρ€Π΅Π· {0} ΠΌΠΈΠ½."},"past":{"one":"{0} ΠΌΠΈΠ½. Π½Π°Π·Π°Π΄","few":"{0} ΠΌΠΈΠ½. Π½Π°Π·Π°Π΄","many":"{0} ΠΌΠΈΠ½. Π½Π°Π·Π°Π΄","other":"{0} ΠΌΠΈΠ½. Π½Π°Π·Π°Π΄"}}},"second":{"displayName":"сСкунда","relative":{"0":"сСйчас"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} сСкунду","few":"Ρ‡Π΅Ρ€Π΅Π· {0} сСкунды","many":"Ρ‡Π΅Ρ€Π΅Π· {0} сСкунд","other":"Ρ‡Π΅Ρ€Π΅Π· {0} сСкунды"},"past":{"one":"{0} сСкунду Π½Π°Π·Π°Π΄","few":"{0} сСкунды Π½Π°Π·Π°Π΄","many":"{0} сСкунд Π½Π°Π·Π°Π΄","other":"{0} сСкунды Π½Π°Π·Π°Π΄"}}},"second-short":{"displayName":"сСк.","relative":{"0":"сСйчас"},"relativeTime":{"future":{"one":"Ρ‡Π΅Ρ€Π΅Π· {0} сСк.","few":"Ρ‡Π΅Ρ€Π΅Π· {0} сСк.","many":"Ρ‡Π΅Ρ€Π΅Π· {0} сСк.","other":"Ρ‡Π΅Ρ€Π΅Π· {0} сСк."},"past":{"one":"{0} сСк. Π½Π°Π·Π°Π΄","few":"{0} сСк. Π½Π°Π·Π°Π΄","many":"{0} сСк. Π½Π°Π·Π°Π΄","other":"{0} сСк. Π½Π°Π·Π°Π΄"}}}} }); +addLocaleData({ locale: "ru-RU", parentLocale: "ru" }); addLocaleData({ locale: "zh", pluralRuleFunction: function (n,ord){if(ord)return"other";return"other"},"fields":{"year":{"displayName":"εΉ΄","relative":{"0":"今年","1":"明年","-1":"去年"},"relativeTime":{"future":{"other":"{0}年后"},"past":{"other":"{0}年前"}}},"year-short":{"displayName":"εΉ΄","relative":{"0":"今年","1":"明年","-1":"去年"},"relativeTime":{"future":{"other":"{0}年后"},"past":{"other":"{0}年前"}}},"month":{"displayName":"月","relative":{"0":"本月","1":"δΈ‹δΈͺ月","-1":"上δΈͺ月"},"relativeTime":{"future":{"other":"{0}δΈͺ月后"},"past":{"other":"{0}δΈͺζœˆε‰"}}},"month-short":{"displayName":"月","relative":{"0":"本月","1":"δΈ‹δΈͺ月","-1":"上δΈͺ月"},"relativeTime":{"future":{"other":"{0}δΈͺ月后"},"past":{"other":"{0}δΈͺζœˆε‰"}}},"day":{"displayName":"ζ—₯","relative":{"0":"今倩","1":"明倩","2":"后倩","-2":"前倩","-1":"昨倩"},"relativeTime":{"future":{"other":"{0}倩后"},"past":{"other":"{0}倩前"}}},"day-short":{"displayName":"ζ—₯","relative":{"0":"今倩","1":"明倩","2":"后倩","-2":"前倩","-1":"昨倩"},"relativeTime":{"future":{"other":"{0}倩后"},"past":{"other":"{0}倩前"}}},"hour":{"displayName":"小既","relative":{"0":"θΏ™δΈ€ζ—Άι—΄ \u002F ζ­€ζ—Ά"},"relativeTime":{"future":{"other":"{0}ε°ζ—ΆεŽ"},"past":{"other":"{0}小既前"}}},"hour-short":{"displayName":"小既","relative":{"0":"θΏ™δΈ€ζ—Άι—΄ \u002F ζ­€ζ—Ά"},"relativeTime":{"future":{"other":"{0}ε°ζ—ΆεŽ"},"past":{"other":"{0}小既前"}}},"minute":{"displayName":"εˆ†ι’Ÿ","relative":{"0":"歀刻"},"relativeTime":{"future":{"other":"{0}εˆ†ι’ŸεŽ"},"past":{"other":"{0}εˆ†ι’Ÿε‰"}}},"minute-short":{"displayName":"εˆ†","relative":{"0":"歀刻"},"relativeTime":{"future":{"other":"{0}εˆ†ι’ŸεŽ"},"past":{"other":"{0}εˆ†ι’Ÿε‰"}}},"second":{"displayName":"η§’","relative":{"0":"现在"},"relativeTime":{"future":{"other":"{0}η§’ι’ŸεŽ"},"past":{"other":"{0}η§’ι’Ÿε‰"}}},"second-short":{"displayName":"η§’","relative":{"0":"现在"},"relativeTime":{"future":{"other":"{0}η§’εŽ"},"past":{"other":"{0}秒前"}}}} }); addLocaleData({ locale: "zh-CN", parentLocale: "zh" }); diff --git a/packages/osd-interpreter/package.json b/packages/osd-interpreter/package.json index c8de5349c64..4654b73b186 100644 --- a/packages/osd-interpreter/package.json +++ b/packages/osd-interpreter/package.json @@ -24,7 +24,7 @@ "babel-loader": "^8.2.3", "copy-webpack-plugin": "^6.0.2", "css-loader": "^5.2.7", - "del": "^5.1.0", + "del": "^6.1.1", "getopts": "^2.2.5", "pegjs": "0.10.0", "sass-loader": "^10.2.0", diff --git a/packages/osd-monaco/README.md b/packages/osd-monaco/README.md index c9bdbae030c..b31ee4035e4 100644 --- a/packages/osd-monaco/README.md +++ b/packages/osd-monaco/README.md @@ -2,4 +2,6 @@ A customized version of monaco that is automatically configured the way we want it to be when imported as `@osd/monaco`. Additionally, imports to this package are automatically shared with all plugins using `@osd/ui-shared-deps`. -Includes custom xjson language support. The `opensearch_ui_shared` plugin has an example of how to use it, in the future we will likely expose helpers from this package to make using it everywhere a little more seamless. \ No newline at end of file +Includes custom xjson language support. The `opensearch_ui_shared` plugin has an example of how to use it, in the future we will likely expose helpers from this package to make using it everywhere a little more seamless. + +Includes json language worker support. \ No newline at end of file diff --git a/packages/osd-monaco/package.json b/packages/osd-monaco/package.json index 8754663f891..e937b1e8f60 100644 --- a/packages/osd-monaco/package.json +++ b/packages/osd-monaco/package.json @@ -17,7 +17,7 @@ "@osd/dev-utils": "1.0.0", "babel-loader": "^8.2.3", "css-loader": "^5.2.7", - "del": "^5.1.0", + "del": "^6.1.1", "raw-loader": "^4.0.2", "supports-color": "^7.0.0", "typescript": "4.0.2", diff --git a/packages/osd-monaco/src/index.ts b/packages/osd-monaco/src/index.ts index 440d62de0f1..e2002dfceb3 100644 --- a/packages/osd-monaco/src/index.ts +++ b/packages/osd-monaco/src/index.ts @@ -30,7 +30,10 @@ export { monaco } from './monaco'; export { XJsonLang } from './xjson'; +import './json'; /* eslint-disable-next-line @osd/eslint/module_migration */ import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api'; export { BarePluginApi }; +import './monaco_environment'; +export * from './worker_store'; diff --git a/packages/osd-monaco/src/json/index.ts b/packages/osd-monaco/src/json/index.ts new file mode 100644 index 00000000000..c27849986f1 --- /dev/null +++ b/packages/osd-monaco/src/json/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { registerWorker } from '../worker_store'; +// @ts-ignore +import jsonWorkerSrc from '!!raw-loader!../../target/public/json.editor.worker.js'; + +registerWorker('json', jsonWorkerSrc); diff --git a/packages/osd-monaco/src/json/worker/json.worker.ts b/packages/osd-monaco/src/json/worker/json.worker.ts new file mode 100644 index 00000000000..3df49d6355a --- /dev/null +++ b/packages/osd-monaco/src/json/worker/json.worker.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// @ts-ignore +/* eslint-disable-next-line @osd/eslint/module_migration */ +import 'monaco-editor/esm/vs/language/json/json.worker.js'; diff --git a/packages/osd-monaco/src/monaco_environment.ts b/packages/osd-monaco/src/monaco_environment.ts new file mode 100644 index 00000000000..ac5bc439e9a --- /dev/null +++ b/packages/osd-monaco/src/monaco_environment.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getWorker } from './worker_store'; + +// @ts-ignore +window.MonacoEnvironment = { + getWorker: (module: string, languageId: string) => { + const workerSrc = getWorker(languageId); + if (workerSrc) { + const blob = new Blob([workerSrc], { type: 'application/javascript' }); + return new Worker(URL.createObjectURL(blob)); + } + }, +}; diff --git a/packages/osd-monaco/src/worker_store.ts b/packages/osd-monaco/src/worker_store.ts new file mode 100644 index 00000000000..59eb6b3f369 --- /dev/null +++ b/packages/osd-monaco/src/worker_store.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const workerStoreMap: Record = {}; + +export const registerWorker = (workerId: string, worker: string) => { + if (!workerStoreMap[workerId]) { + workerStoreMap[workerId] = worker; + return true; + } + + return false; +}; + +export const getWorker = (workerId: string) => { + return workerStoreMap[workerId]; +}; diff --git a/packages/osd-monaco/src/xjson/language.ts b/packages/osd-monaco/src/xjson/language.ts index 084ecb84283..f9b317a39d3 100644 --- a/packages/osd-monaco/src/xjson/language.ts +++ b/packages/osd-monaco/src/xjson/language.ts @@ -34,6 +34,7 @@ import { monaco } from '../monaco'; import { WorkerProxyService } from './worker_proxy_service'; import { registerLexerRules } from './lexer_rules'; import { ID } from './constants'; +import { registerWorker } from '../worker_store'; // @ts-ignore import workerSrc from '!!raw-loader!../../target/public/xjson.editor.worker.js'; @@ -42,19 +43,8 @@ const wps = new WorkerProxyService(); // Register rules against shared monaco instance. registerLexerRules(monaco); -// In future we will need to make this map languages to workers using "id" and/or "label" values -// that get passed in. Also this should not live inside the "xjson" dir directly. We can update this -// once we have another worker. -// @ts-ignore -window.MonacoEnvironment = { - getWorker: (module: string, languageId: string) => { - if (languageId === ID) { - // In OpenSearch Dashboards we will probably build this once and then load with raw-loader - const blob = new Blob([workerSrc], { type: 'application/javascript' }); - return new Worker(URL.createObjectURL(blob)); - } - }, -}; +// register xjson worker to the worker map. +registerWorker(ID, workerSrc); monaco.languages.onLanguage(ID, async () => { return wps.setup(); diff --git a/packages/osd-monaco/webpack.config.js b/packages/osd-monaco/webpack.config.js index 696837d9a6f..1e3f108b95e 100644 --- a/packages/osd-monaco/webpack.config.js +++ b/packages/osd-monaco/webpack.config.js @@ -59,4 +59,4 @@ const createLangWorkerConfig = (lang) => ({ }, }); -module.exports = [createLangWorkerConfig('xjson')]; +module.exports = [createLangWorkerConfig('xjson'), createLangWorkerConfig('json')]; diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index d086033f728..4a9aafa875a 100644 --- a/packages/osd-opensearch/package.json +++ b/packages/osd-opensearch/package.json @@ -12,17 +12,17 @@ "osd:watch": "node scripts/build --watch" }, "dependencies": { - "@opensearch-project/opensearch": "^1.1.0", + "@opensearch-project/opensearch": "^2.1.0", "@osd/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", "dedent": "^0.7.0", - "del": "^5.1.0", + "del": "^6.1.1", "execa": "^4.0.2", "getopts": "^2.2.5", "glob": "^7.1.7", "node-fetch": "^2.6.7", - "simple-git": "^3.4.0", + "simple-git": "^3.16.0", "tar-fs": "^2.1.0", "tree-kill": "^1.2.2", "yauzl": "^2.10.0" @@ -30,6 +30,6 @@ "devDependencies": { "@osd/babel-preset": "1.0.0", "@babel/cli": "^7.16.0", - "del": "^5.1.0" + "del": "^6.1.1" } } diff --git a/packages/osd-opensearch/src/artifact.js b/packages/osd-opensearch/src/artifact.js index b7d8857ba09..541afa8c76f 100644 --- a/packages/osd-opensearch/src/artifact.js +++ b/packages/osd-opensearch/src/artifact.js @@ -38,6 +38,7 @@ const { createHash } = require('crypto'); const path = require('path'); const asyncPipeline = promisify(pipeline); +const SUPPORTED_PLATFORMS = ['linux', 'windows', 'darwin']; const DAILY_SNAPSHOTS_BASE_URL = 'https://artifacts.opensearch.org/snapshots/core/opensearch'; // TODO: [RENAMEME] currently do not have an existing replacement // issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/475 @@ -188,8 +189,8 @@ async function getArtifactSpecForSnapshotFromUrl(urlVersion, log) { const arch = process.arch === 'arm64' ? 'arm64' : 'x64'; const extension = process.platform === 'win32' ? 'zip' : 'tar.gz'; - if (platform !== 'linux' && platform !== 'windows') { - throw createCliError(`Snapshots are only available for Linux and Windows`); + if (!SUPPORTED_PLATFORMS.includes(platform)) { + throw createCliError(`Snapshots are only available for Linux, Windows, and Darwin`); } const latestUrl = `${DAILY_SNAPSHOTS_BASE_URL}/${desiredVersion}-SNAPSHOT`; diff --git a/packages/osd-opensearch/src/artifact.test.js b/packages/osd-opensearch/src/artifact.test.js index 1d5d0bdf330..89baa7b9e89 100644 --- a/packages/osd-opensearch/src/artifact.test.js +++ b/packages/osd-opensearch/src/artifact.test.js @@ -163,17 +163,17 @@ describe('Artifact', () => { }); }); - it('should throw when on a non-Linux or non-Windows platform', async () => { + it('should throw when on a non-Linux, non-Windows, non-Darwin platform', async () => { Object.defineProperties(process, { platform: { - value: 'darwin', + value: 'android', }, arch: { value: ORIGINAL_ARCHITECTURE, }, }); await expect(Artifact.getSnapshot('default', 'INVALID_PLATFORM', log)).rejects.toThrow( - 'Snapshots are only available for Linux' + 'Snapshots are only available for Linux, Windows, and Darwin' ); }); @@ -189,6 +189,42 @@ describe('Artifact', () => { mockFetch(MOCKS.multipleArch[0]); artifactTest(); }); + + it('should not throw when on a Linux platform', async () => { + Object.defineProperties(process, { + platform: { + value: 'linux', + }, + arch: { + value: 'x64', + }, + }); + artifactTest(); + }); + + it('should not throw when on a Windows platform', async () => { + Object.defineProperties(process, { + platform: { + value: 'win32', + }, + arch: { + value: 'x64', + }, + }); + artifactTest(); + }); + + it('should not throw when on a Darwin platform', async () => { + Object.defineProperties(process, { + platform: { + value: 'darwin', + }, + arch: { + value: 'x64', + }, + }); + artifactTest(); + }); }); describe('with custom snapshot manifest URL', () => { diff --git a/packages/osd-opensearch/src/cli_commands/snapshot.js b/packages/osd-opensearch/src/cli_commands/snapshot.js index 99b85b7439a..3cf8701856b 100644 --- a/packages/osd-opensearch/src/cli_commands/snapshot.js +++ b/packages/osd-opensearch/src/cli_commands/snapshot.js @@ -49,10 +49,13 @@ exports.help = (defaults = {}) => { -E Additional key=value settings to pass to OpenSearch --download-only Download the snapshot but don't actually start it --ssl Sets up SSL on OpenSearch + --P OpenSearch plugin artifact URL to install it on the cluster. We can use the flag multiple times + to install multiple plugins on the cluster snapshot. The argument value can be url to zip file, maven coordinates of the plugin + or for local zip files, use file:. Example: - opensearch snapshot --version 5.6.8 -E cluster.name=test -E path.data=/tmp/opensearch-data + opensearch snapshot --version 2.2.0 -E cluster.name=test -E path.data=/tmp/opensearch-data --P org.opensearch.plugin:test-plugin:2.2.0.0 --P file:/home/user/opensearch-test-plugin-2.2.0.0.zip `; }; @@ -64,6 +67,7 @@ exports.run = async (defaults = {}) => { installPath: 'install-path', dataArchive: 'data-archive', opensearchArgs: 'E', + opensearchPlugins: 'P', }, string: ['version'], @@ -83,6 +87,10 @@ exports.run = async (defaults = {}) => { await cluster.extractDataDirectory(installPath, options.dataArchive); } + if (options.opensearchPlugins) { + await cluster.installOpenSearchPlugins(installPath, options.opensearchPlugins); + } + options.bundledJDK = true; await cluster.run(installPath, options); diff --git a/packages/osd-opensearch/src/cluster.js b/packages/osd-opensearch/src/cluster.js index 7b11b4edbbc..3527668eed0 100644 --- a/packages/osd-opensearch/src/cluster.js +++ b/packages/osd-opensearch/src/cluster.js @@ -34,7 +34,7 @@ const execa = require('execa'); const chalk = require('chalk'); const path = require('path'); const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install'); -const { OPENSEARCH_BIN } = require('./paths'); +const { OPENSEARCH_BIN, OPENSEARCH_PLUGIN } = require('./paths'); const { log: defaultLog, parseOpenSearchLog, extractConfigFiles, decompress } = require('./utils'); const { createCliError } = require('./errors'); const { promisify } = require('util'); @@ -170,6 +170,29 @@ exports.Cluster = class Cluster { this._log.indent(-4); } + /** + * Unpacks a tar or zip file containing the OpenSearch plugin directory for an + * OpenSearch cluster. + * + * @param {string} installPath + * @param {Array|string} opensearchPlugins Array or string of OpenSearch plugin(s) artifact url + */ + async installOpenSearchPlugins(installPath, opensearchPluginsPath) { + if (opensearchPluginsPath) { + this._log.info(chalk.bold(`Downloading OpenSearch plugin(s) on the cluster snapshot`)); + this._log.indent(4); + opensearchPluginsPath = + typeof opensearchPluginsPath === 'string' ? [opensearchPluginsPath] : opensearchPluginsPath; + // Run opensearch-plugin tool script to download OpenSearch plugin artifacts + for (const pluginPath of opensearchPluginsPath) { + this._log.info(`Installing OpenSearch Plugin from the path: ${pluginPath}`); + await execa(OPENSEARCH_PLUGIN, [`install`, `--batch`, pluginPath], { cwd: installPath }); + } + this._log.info(`Plugin installation complete`); + this._log.indent(-4); + } + } + /** * Starts OpenSearch and returns resolved promise once started * @@ -229,7 +252,22 @@ exports.Cluster = class Cluster { throw new Error('OpenSearch has not been started'); } - await treeKillAsync(this._process.pid); + /* Temporary fix for https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2811 + * + * `tree-kill` behaves differently on Windows, where it throws if `pid` is already dead, when + * compared to other operating systems, where it silently returns. + */ + try { + await treeKillAsync(this._process.pid); + } catch (ex) { + console.log('ex.message', ex.message); + if ( + process.platform === 'win32' && + !ex.message?.includes(`The process "${this._process.pid}" not found`) + ) { + throw ex; + } + } await this._outcome; } diff --git a/packages/osd-opensearch/src/integration_tests/cluster.test.js b/packages/osd-opensearch/src/integration_tests/cluster.test.js index 43a8f0f63c7..c873f1e6be4 100644 --- a/packages/osd-opensearch/src/integration_tests/cluster.test.js +++ b/packages/osd-opensearch/src/integration_tests/cluster.test.js @@ -292,6 +292,36 @@ describe('#start(installPath)', () => { }); }); +describe('#installOpenSearchPlugins()', () => { + const pluginHelperCLI = + process.platform === 'win32' ? './bin/opensearch-plugin.bat' : './bin/opensearch-plugin'; + + it('install array of plugins on cluster snapshot', async () => { + const cluster = new Cluster({ log }); + await cluster.installOpenSearchPlugins('foo', ['foo1', 'foo2']); + expect(execa).toHaveBeenCalledTimes(2); + expect(execa).toHaveBeenCalledWith(pluginHelperCLI, ['install', '--batch', 'foo1'], { + cwd: 'foo', + }); + expect(execa).toHaveBeenCalledWith(pluginHelperCLI, ['install', '--batch', 'foo2'], { + cwd: 'foo', + }); + }); + it('installs single plugin on cluster snapshot', async () => { + const cluster = new Cluster({ log }); + await cluster.installOpenSearchPlugins('foo', 'foo1'); + expect(execa).toHaveBeenCalledTimes(1); + expect(execa).toHaveBeenCalledWith(pluginHelperCLI, ['install', '--batch', 'foo1'], { + cwd: 'foo', + }); + }); + it('do not execute plugin installation script when no plugins in the param list', async () => { + const cluster = new Cluster({ log }); + await cluster.installOpenSearchPlugins('foo'); + expect(execa).toHaveBeenCalledTimes(0); + }); +}); + describe('#run()', () => { it('resolves when bin/opensearch exists with 0', async () => { mockOpenSearchBin({ exitCode: 0 }); diff --git a/packages/osd-opensearch/src/paths.js b/packages/osd-opensearch/src/paths.js index e278f20fc5a..93bb80e97ff 100644 --- a/packages/osd-opensearch/src/paths.js +++ b/packages/osd-opensearch/src/paths.js @@ -44,3 +44,4 @@ exports.OPENSEARCH_BIN = maybeUseBat('bin/opensearch'); exports.OPENSEARCH_CONFIG = 'config/opensearch.yml'; exports.OPENSEARCH_KEYSTORE_BIN = maybeUseBat('./bin/opensearch-keystore'); +exports.OPENSEARCH_PLUGIN = maybeUseBat('./bin/opensearch-plugin'); diff --git a/packages/osd-optimizer/package.json b/packages/osd-optimizer/package.json index e3ffe9a4930..2aec5681ba9 100644 --- a/packages/osd-optimizer/package.json +++ b/packages/osd-optimizer/package.json @@ -23,7 +23,7 @@ "cpy": "^8.0.0", "core-js": "^3.6.5", "dedent": "^0.7.0", - "del": "^5.1.0", + "del": "^6.1.1", "execa": "^4.0.2", "fibers": "^5.0.3", "jest-diff": "^27.5.1", @@ -51,7 +51,7 @@ "babel-loader": "^8.2.3", "css-loader": "^5.2.7", "file-loader": "^6.2.0", - "loader-utils": "^1.2.3", + "loader-utils": "^2.0.4", "postcss-loader": "^4.2.0", "raw-loader": "^4.0.2", "sass-loader": "^10.2.0", diff --git a/packages/osd-optimizer/src/node/cache.ts b/packages/osd-optimizer/src/node/cache.ts index 225a0de79ee..e60038102fb 100644 --- a/packages/osd-optimizer/src/node/cache.ts +++ b/packages/osd-optimizer/src/node/cache.ts @@ -33,6 +33,7 @@ import { Writable } from 'stream'; import chalk from 'chalk'; import * as LmdbStore from 'lmdb-store'; +import { getMatchingRoot } from '@osd/cross-platform'; const GLOBAL_ATIME = `${Date.now()}`; const MINUTE = 1000 * 60; @@ -48,17 +49,24 @@ export class Cache { private readonly atimes: LmdbStore.Database; private readonly mtimes: LmdbStore.Database; private readonly sourceMaps: LmdbStore.Database; - private readonly pathRoot: string; + private readonly pathRoots: string[]; private readonly prefix: string; private readonly log?: Writable; private readonly timer: NodeJS.Timer; - constructor(config: { pathRoot: string; dir: string; prefix: string; log?: Writable }) { - if (!Path.isAbsolute(config.pathRoot)) { + constructor(config: { + pathRoot: string | string[]; + dir: string; + prefix: string; + log?: Writable; + }) { + const pathRoots = Array.isArray(config.pathRoot) ? config.pathRoot : [config.pathRoot]; + + if (!pathRoots.every((pathRoot) => Path.isAbsolute(pathRoot))) { throw new Error('cache requires an absolute path to resolve paths relative to'); } - this.pathRoot = config.pathRoot; + this.pathRoots = pathRoots; this.prefix = config.prefix; this.log = config.log; @@ -139,10 +147,17 @@ export class Cache { } private getKey(path: string) { + const resolvedPath = Path.resolve(path); + /* Try to find the root that is the parent to `path` so we can make a nimble + * and unique key based on the relative path. If A root was not found, just + * use any of the roots; the key would just be long. + */ + const pathRoot = getMatchingRoot(resolvedPath, this.pathRoots) || this.pathRoots[0]; + const normalizedPath = Path.sep !== '/' - ? Path.relative(this.pathRoot, path).split(Path.sep).join('/') - : Path.relative(this.pathRoot, path); + ? Path.relative(pathRoot, resolvedPath).split(Path.sep).join('/') + : Path.relative(pathRoot, resolvedPath); return `${this.prefix}${normalizedPath}`; } diff --git a/packages/osd-optimizer/src/node/node_auto_tranpilation.ts b/packages/osd-optimizer/src/node/node_auto_tranpilation.ts index 123f5afb3e7..87fcde1ce9d 100644 --- a/packages/osd-optimizer/src/node/node_auto_tranpilation.ts +++ b/packages/osd-optimizer/src/node/node_auto_tranpilation.ts @@ -50,7 +50,7 @@ import Crypto from 'crypto'; import * as babel from '@babel/core'; import { addHook } from 'pirates'; -import { REPO_ROOT, UPSTREAM_BRANCH } from '@osd/dev-utils'; +import { REPO_ROOT, REPO_ROOT_8_3, UPSTREAM_BRANCH } from '@osd/dev-utils'; import sourceMapSupport from 'source-map-support'; import { Cache } from './cache'; @@ -142,7 +142,7 @@ export function registerNodeAutoTranspilation() { installed = true; const cache = new Cache({ - pathRoot: REPO_ROOT, + pathRoot: [REPO_ROOT, REPO_ROOT_8_3], dir: Path.resolve(REPO_ROOT, 'data/node_auto_transpilation_cache_v1', UPSTREAM_BRANCH), prefix: determineCachePrefix(), log: process.env.DEBUG_NODE_TRANSPILER_CACHE diff --git a/packages/osd-optimizer/src/optimizer/cache_keys.test.ts b/packages/osd-optimizer/src/optimizer/cache_keys.test.ts index c9745213255..b01228a0957 100644 --- a/packages/osd-optimizer/src/optimizer/cache_keys.test.ts +++ b/packages/osd-optimizer/src/optimizer/cache_keys.test.ts @@ -28,8 +28,7 @@ * under the License. */ -import Path from 'path'; - +import fs from 'fs/promises'; import { diff } from 'jest-diff'; import { REPO_ROOT } from '@osd/utils'; import { createAbsolutePathSerializer } from '@osd/dev-utils'; @@ -52,14 +51,6 @@ jest.mock('./get_mtimes.ts', () => ({ jest.mock('execa'); -jest.mock('fs', () => { - const realFs = jest.requireActual('fs'); - return { - ...realFs, - readFile: jest.fn(realFs.readFile), - }; -}); - expect.addSnapshotSerializer(createAbsolutePathSerializer()); jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { @@ -83,17 +74,9 @@ jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], describe('getOptimizerCacheKey()', () => { it('uses latest commit, bootstrap cache, and changed files to create unique value', async () => { - jest - .requireMock('fs') - .readFile.mockImplementation( - (path: string, enc: string, cb: (err: null, file: string) => void) => { - expect(path).toBe( - Path.resolve(REPO_ROOT, 'packages/osd-optimizer/target/.bootstrap-cache') - ); - expect(enc).toBe('utf8'); - cb(null, ''); - } - ); + const mockFSReadFileAsync = jest + .spyOn(fs, 'readFile') + .mockReturnValue(Promise.resolve('')); const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, @@ -122,6 +105,8 @@ describe('getOptimizerCacheKey()', () => { }, } `); + + mockFSReadFileAsync.mockRestore(); }); }); diff --git a/packages/osd-optimizer/src/optimizer/cache_keys.ts b/packages/osd-optimizer/src/optimizer/cache_keys.ts index e31009f872c..16efdea4888 100644 --- a/packages/osd-optimizer/src/optimizer/cache_keys.ts +++ b/packages/osd-optimizer/src/optimizer/cache_keys.ts @@ -28,13 +28,12 @@ * under the License. */ -import Path from 'path'; -import Fs from 'fs'; -import { promisify } from 'util'; +import { dirname, resolve } from 'path'; +import { readFile } from 'fs/promises'; import Chalk from 'chalk'; import execa from 'execa'; -import { REPO_ROOT } from '@osd/utils'; +import { relativeToRepoRoot, REPO_ROOT } from '@osd/cross-platform'; import stripAnsi from 'strip-ansi'; import { diff } from 'jest-diff'; @@ -45,8 +44,8 @@ import { getMtimes } from './get_mtimes'; import { getChanges } from './get_changes'; import { OptimizerConfig } from './optimizer_config'; -const OPTIMIZER_DIR = Path.dirname(require.resolve('../../package.json')); -const RELATIVE_DIR = Path.relative(REPO_ROOT, OPTIMIZER_DIR); +const OPTIMIZER_DIR = dirname(require.resolve('../../package.json')); +const RELATIVE_DIR = relativeToRepoRoot(OPTIMIZER_DIR)!; export function diffCacheKey(expected?: unknown, actual?: unknown) { const expectedJson = jsonStable(expected, { @@ -156,10 +155,7 @@ async function getLastCommit() { async function getBootstrapCacheKey() { try { - return await promisify(Fs.readFile)( - Path.resolve(OPTIMIZER_DIR, 'target/.bootstrap-cache'), - 'utf8' - ); + return await readFile(resolve(OPTIMIZER_DIR, 'target/.bootstrap-cache'), { encoding: 'utf8' }); } catch (error) { if (error?.code !== 'ENOENT') { throw error; diff --git a/packages/osd-optimizer/src/worker/run_worker.ts b/packages/osd-optimizer/src/worker/run_worker.ts index e41c917a883..2997444eb0e 100644 --- a/packages/osd-optimizer/src/worker/run_worker.ts +++ b/packages/osd-optimizer/src/worker/run_worker.ts @@ -78,11 +78,13 @@ const exit = (code: number) => { setTimeout(() => { send( workerMsgs.error( - new Error('process did not automatically exit within 5 seconds, forcing exit') + new Error( + `process did not automatically exit within 15 seconds (previous code: ${code}); forcing exit...` + ) ) ); process.exit(1); - }, 5000).unref(); + }, 15000).unref(); }; // check for connected parent on an unref'd timer rather than listening diff --git a/packages/osd-optimizer/src/worker/webpack.config.ts b/packages/osd-optimizer/src/worker/webpack.config.ts index 1eb6c579675..caeed72a3bb 100644 --- a/packages/osd-optimizer/src/worker/webpack.config.ts +++ b/packages/osd-optimizer/src/worker/webpack.config.ts @@ -89,7 +89,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: // already bundled with all its necessary depedencies noParse: [ /[\/\\]node_modules[\/\\]lodash[\/\\]index\.js$/, - /[\/\\]node_modules[\/\\]vega[\/\\]build[\/\\]vega\.js$/, + /[\/\\]node_modules[\/\\]vega[\/\\]build-es5[\/\\]vega\.js$/, ], rules: [ @@ -205,7 +205,9 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: }, { test: /\.(js|tsx?)$/, - exclude: /node_modules/, + // vega-lite and some of its dependencies don't have es5 builds + // so we need to build from source and transpile for webpack v4 + exclude: /[\/\\]node_modules[\/\\](?!vega-(lite|label|functions)[\/\\])/, use: { loader: 'babel-loader', options: { diff --git a/packages/osd-plugin-generator/src/cli.ts b/packages/osd-plugin-generator/src/cli.ts index 596f55159ed..2d23cb8facf 100644 --- a/packages/osd-plugin-generator/src/cli.ts +++ b/packages/osd-plugin-generator/src/cli.ts @@ -32,7 +32,7 @@ import Path from 'path'; import Fs from 'fs'; import execa from 'execa'; -import { REPO_ROOT } from '@osd/utils'; +import { PROCESS_WORKING_DIR, REPO_ROOT } from '@osd/cross-platform'; import { run, createFailError, createFlagError } from '@osd/dev-utils'; import { snakeCase } from './casing'; @@ -78,7 +78,7 @@ export function runCli() { } log.success( - `πŸŽ‰\n\nYour plugin has been created in ${Path.relative(process.cwd(), outputDir)}\n` + `πŸŽ‰\n\nYour plugin has been created in ${Path.relative(PROCESS_WORKING_DIR, outputDir)}\n` ); }, { diff --git a/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts b/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts index c81f248369e..38ad0496c17 100644 --- a/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts +++ b/packages/osd-plugin-generator/src/integration_tests/generate_plugin.test.ts @@ -32,8 +32,8 @@ import Path from 'path'; import del from 'del'; import execa from 'execa'; -import { standardize } from '@osd/cross-platform'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@osd/dev-utils'; +import { standardize, REPO_ROOT } from '@osd/cross-platform'; +import { createAbsolutePathSerializer } from '@osd/dev-utils'; import globby from 'globby'; // Has to be a posix reference because it is used to generate glob patterns diff --git a/packages/osd-plugin-helpers/README.md b/packages/osd-plugin-helpers/README.md index 4a16bc9afe2..700999c26bf 100644 --- a/packages/osd-plugin-helpers/README.md +++ b/packages/osd-plugin-helpers/README.md @@ -49,7 +49,7 @@ $ plugin-helpers help Options: --skip-archive Don't create the zip file, just create the build/opensearch-dashboards directory - --opensearch-dashboards-version, -v OpenSearch Dashboards version that the built plugin will target + --opensearch-dashboards-version, -k OpenSearch Dashboards version that the built plugin will target version diff --git a/packages/osd-plugin-helpers/package.json b/packages/osd-plugin-helpers/package.json index 55e5e803fe4..7ee45578a63 100644 --- a/packages/osd-plugin-helpers/package.json +++ b/packages/osd-plugin-helpers/package.json @@ -12,14 +12,14 @@ "plugin-helpers": "bin/plugin-helpers.js" }, "scripts": { - "osd:bootstrap": "node ../../scripts/remove.js && tsc", + "osd:bootstrap": "node ../../scripts/remove.js target && tsc", "osd:watch": "tsc --watch" }, "dependencies": { "@osd/cross-platform": "1.0.0", "@osd/dev-utils": "1.0.0", "@osd/optimizer": "1.0.0", - "del": "^5.1.0", + "del": "^6.1.1", "execa": "^4.0.2", "gulp-zip": "^5.0.2", "inquirer": "^7.3.3", diff --git a/packages/osd-plugin-helpers/src/cli.ts b/packages/osd-plugin-helpers/src/cli.ts index f108f9a2f6c..c4b1a83feda 100644 --- a/packages/osd-plugin-helpers/src/cli.ts +++ b/packages/osd-plugin-helpers/src/cli.ts @@ -66,7 +66,7 @@ export function runCli() { }, help: ` --skip-archive Don't create the zip file, just create the build/opensearch-dashboards directory - --opensearch-dashboards-version, -v OpenSearch version that the + --opensearch-dashboards-version, -k OpenSearch Dashboards version that the built plugin will target `, }, async run({ log, flags }) { diff --git a/packages/osd-plugin-helpers/src/integration_tests/build.test.ts b/packages/osd-plugin-helpers/src/integration_tests/build.test.ts index 12a0b52ace2..780682d2765 100644 --- a/packages/osd-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/osd-plugin-helpers/src/integration_tests/build.test.ts @@ -32,8 +32,8 @@ import Path from 'path'; import Fs from 'fs'; import execa from 'execa'; -import { standardize } from '@osd/cross-platform'; -import { REPO_ROOT, createStripAnsiSerializer, createReplaceSerializer } from '@osd/dev-utils'; +import { REPO_ROOT, standardize } from '@osd/cross-platform'; +import { createStripAnsiSerializer, createReplaceSerializer } from '@osd/dev-utils'; import extract from 'extract-zip'; import del from 'del'; import globby from 'globby'; @@ -105,7 +105,8 @@ it('builds a generated plugin into a viable archive', async () => { info copying assets from \`public/assets\` to build info copying server source into the build and converting with babel info running yarn to install dependencies - info compressing plugin into [fooTestPlugin-1.0.0.zip]" + info compressing plugin into [fooTestPlugin-1.0.0.zip] + info cleaning up compression temporary artifacts" `); await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }, () => {}); @@ -192,7 +193,8 @@ it('builds a non-semver generated plugin into a viable archive', async () => { info copying assets from \`public/assets\` to build info copying server source into the build and converting with babel info running yarn to install dependencies - info compressing plugin into [fooTestPlugin-1.0.0.x.zip]" + info compressing plugin into [fooTestPlugin-1.0.0.x.zip] + info cleaning up compression temporary artifacts" `); await extract(PLUGIN_ARCHIVE_X, { dir: TMP_DIR }, () => {}); diff --git a/packages/osd-plugin-helpers/src/tasks/create_archive.ts b/packages/osd-plugin-helpers/src/tasks/create_archive.ts index 566fc83ff21..9d0ee1be278 100644 --- a/packages/osd-plugin-helpers/src/tasks/create_archive.ts +++ b/packages/osd-plugin-helpers/src/tasks/create_archive.ts @@ -62,6 +62,7 @@ export async function createArchive({ opensearchDashboardsVersion, plugin, log } vfs.dest(buildDir) ); + log.info(`cleaning up compression temporary artifacts`); // delete the files that were zipped - await del(Path.resolve(buildDir, 'opensearch-dashboards')); + await del(Path.resolve(buildDir, 'opensearch-dashboards'), { cwd: buildDir }); } diff --git a/packages/osd-pm/package.json b/packages/osd-pm/package.json index cda579e14fe..c10af975d93 100644 --- a/packages/osd-pm/package.json +++ b/packages/osd-pm/package.json @@ -42,7 +42,7 @@ "cmd-shim": "^2.1.0", "cpy": "^8.0.0", "dedent": "^0.7.0", - "del": "^5.1.0", + "del": "^6.1.1", "execa": "^4.0.2", "getopts": "^2.2.5", "glob": "^7.1.7", diff --git a/packages/osd-pm/src/utils/projects.test.ts b/packages/osd-pm/src/utils/projects.test.ts index 545b435a7e0..a4c945647f6 100644 --- a/packages/osd-pm/src/utils/projects.test.ts +++ b/packages/osd-pm/src/utils/projects.test.ts @@ -43,12 +43,16 @@ import { ProjectMap, topologicallyBatchProjects, } from './projects'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const rootPath = resolve(__dirname, '__fixtures__/opensearch-dashboards'); const rootPlugins = join(rootPath, 'plugins'); describe('#getProjects', () => { beforeAll(async () => { + // Make sure we start clean + await del(rootPlugins, { cwd: PROCESS_WORKING_DIR }); + await promisify(mkdir)(rootPlugins); await promisify(symlink)( @@ -58,7 +62,7 @@ describe('#getProjects', () => { ); }); - afterAll(async () => await del(rootPlugins)); + afterAll(async () => await del(rootPlugins, { cwd: PROCESS_WORKING_DIR })); test('find all packages in the packages directory', async () => { const projects = await getProjects(rootPath, ['packages/*']); diff --git a/packages/osd-pm/src/utils/validate_dependencies.ts b/packages/osd-pm/src/utils/validate_dependencies.ts index 51a1c0eada0..2264681b986 100644 --- a/packages/osd-pm/src/utils/validate_dependencies.ts +++ b/packages/osd-pm/src/utils/validate_dependencies.ts @@ -175,7 +175,7 @@ export async function validateDependencies(osd: OpenSearchDashboards, yarnLock: const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(osd, 'opensearch-dashboards'); if (devOnlyProjectsInProduction) { log.error(dedent` - Some of the packages in the production dependency chain for OpenSearch Dashboards and X-Pack are + Some of the packages in the production dependency chain for OpenSearch Dashboards are flagged with "opensearchDashboards.devOnly" in their package.json. Please check changes made to packages and their dependencies to ensure they don't end up in production. diff --git a/packages/osd-test/package.json b/packages/osd-test/package.json index 4016e4dcc05..7776afc1de5 100644 --- a/packages/osd-test/package.json +++ b/packages/osd-test/package.json @@ -27,7 +27,7 @@ "dependencies": { "chalk": "^4.1.0", "dedent": "^0.7.0", - "del": "^5.1.0", + "del": "^6.1.1", "exit-hook": "^2.2.0", "getopts": "^2.2.5", "glob": "^7.1.7", diff --git a/packages/osd-test/src/functional_test_runner/integration/failure_hooks.test.js b/packages/osd-test/src/functional_test_runner/integration/failure_hooks.test.js index 2047a6c3065..9e565671ba5 100644 --- a/packages/osd-test/src/functional_test_runner/integration/failure_hooks.test.js +++ b/packages/osd-test/src/functional_test_runner/integration/failure_hooks.test.js @@ -55,6 +55,7 @@ describe('failure hooks', function () { { flag: '$FAILING_TEST$', assert(lines) { + expect(lines.shift()).to.match(/\$FAILING_TEST\$/); expect(lines.shift()).to.match(/global before each/); expect(lines.shift()).to.match(/info\s+testFailure\s+\$FAILING_TEST_ERROR\$/); expect(lines.shift()).to.match(/info\s+testFailureAfterDelay\s+\$FAILING_TEST_ERROR\$/); diff --git a/packages/osd-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js b/packages/osd-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js index 5781a6fd785..bc27ff7e1a8 100644 --- a/packages/osd-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js +++ b/packages/osd-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js @@ -111,8 +111,8 @@ it('only runs hooks of parents and tests in level1a', async () => { "suite: ", "suite: level 1", "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1a\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1a\\"", "test: level 1 level 1a test 1a", ] `); @@ -130,8 +130,8 @@ it('only runs hooks of parents and tests in level1b', async () => { "suite: ", "suite: level 1", "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1b\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1b\\"", "test: level 1 level 1b test 1b", ] `); @@ -149,12 +149,12 @@ it('only runs hooks of parents and tests in level1a and level1b', async () => { "suite: ", "suite: level 1", "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1a\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1a\\"", "test: level 1 level 1a test 1a", "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1b\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1b\\"", "test: level 1 level 1b test 1b", ] `); @@ -173,8 +173,8 @@ it('only runs level1a if including level1 and excluding level1b', async () => { "suite: ", "suite: level 1", "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1a\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1a\\"", "test: level 1 level 1a test 1a", ] `); @@ -193,8 +193,8 @@ it('only runs level1b if including level1 and excluding level1a', async () => { "suite: ", "suite: level 1", "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1b\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1b\\"", "test: level 1 level 1b test 1b", ] `); @@ -212,7 +212,7 @@ it('only runs level2 if excluding level1', async () => { "suite: ", "suite: level 2", "suite: level 2 level 2a", - "hook: \\"before each\\" hook: rootBeforeEach", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 2a\\"", "test: level 2 level 2a test 2a", ] `); diff --git a/packages/osd-ui-shared-deps/package.json b/packages/osd-ui-shared-deps/package.json index b75c4bd2a5c..3df0f549e67 100644 --- a/packages/osd-ui-shared-deps/package.json +++ b/packages/osd-ui-shared-deps/package.json @@ -42,8 +42,8 @@ "@osd/dev-utils": "1.0.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "css-loader": "^5.2.7", - "del": "^5.1.0", - "loader-utils": "^1.2.3", + "del": "^6.1.1", + "loader-utils": "^2.0.4", "val-loader": "^2.1.2", "webpack": "^4.41.5" } diff --git a/packages/osd-utils/src/index.ts b/packages/osd-utils/src/index.ts index 7fd473a67b9..f5bf23b4164 100644 --- a/packages/osd-utils/src/index.ts +++ b/packages/osd-utils/src/index.ts @@ -30,4 +30,4 @@ export * from './package_json'; export * from './path'; -export * from './repo_root'; +export { REPO_ROOT, REPO_ROOT_8_3, UPSTREAM_BRANCH } from '@osd/cross-platform'; diff --git a/packages/osd-utils/src/package_json/index.ts b/packages/osd-utils/src/package_json/index.ts index 644554387ce..bcd22bb28ba 100644 --- a/packages/osd-utils/src/package_json/index.ts +++ b/packages/osd-utils/src/package_json/index.ts @@ -29,7 +29,7 @@ */ import { dirname, resolve } from 'path'; -import { REPO_ROOT } from '../repo_root'; +import { REPO_ROOT } from '@osd/cross-platform'; export const opensearchDashboardsPackageJSON = { __filename: resolve(REPO_ROOT, 'package.json'), diff --git a/packages/osd-utils/src/path/index.ts b/packages/osd-utils/src/path/index.ts index c661b04fb6a..ac4c40b0b60 100644 --- a/packages/osd-utils/src/path/index.ts +++ b/packages/osd-utils/src/path/index.ts @@ -31,7 +31,7 @@ import { join } from 'path'; import { accessSync, constants } from 'fs'; import { TypeOf, schema } from '@osd/config-schema'; -import { REPO_ROOT } from '../repo_root'; +import { REPO_ROOT } from '@osd/cross-platform'; const isString = (v: any): v is string => typeof v === 'string'; diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.7.md b/release-notes/opensearch-dashboards.release-notes-1.3.7.md new file mode 100644 index 00000000000..eb0e69f6c90 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.7.md @@ -0,0 +1,49 @@ +# Version 1.3.7 Release Notes + +### πŸ“ˆ Features/Enhancements + +* [Windows] Facilitate building and running OSD and plugins on Windows platforms ([#2601](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2601)) +* [Windows] Add helper functions to work around the differences of platforms ([#2681](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2681)) +* [Windows] Add `@osd/cross-platform` package to standardize path handling across platforms ([#2703](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2703)) + +### πŸ›‘ Security + +* [CVE-2022-0144] Bump shelljs from 0.8.4 to 0.8.5 ([#2511](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2511)) +* [Legacy Maps Plugin] Prevent reverse-tabnabbing ([#2540](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2540)) +* [CVE-2022-3517] Bump minimatch from 3.0.4 to 3.0.5 and [IBM X-Force ID: 220063] unset-value from 1.0.1 to 2.0.1 ([#2640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2640)) +* [CVE-2022-0155] Bump follow-redirects to 1.15.2 ([#2653](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2653)) +* [CVE-2022-0536] Bump follow-redirects to 1.15.2 ([#2653](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2653)) +* [CVE-2021-24033] Remove storybook package ([#2660](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2660)) +* [CVE-2021-42740] Remove storybook package ([#2660](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2660)) +* [CVE-2022-23647] Bump prismjs to 1.29.0 ([#2668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2668)) +* [CVE-2022-37599] Bump loader-utils to 2.0.4 ([#2995](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2995)) +* [CVE-2022-37603] Bump loader-utils to 2.0.4 ([#2995](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2995)) + +### πŸ“ Documentation + +* Add the release runbook to RELEASING.md ([#2533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2533)) +* Security-CVEs fixes guidelines [#2674](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2674) +* Correct README and help command of osd-plugin-helpers ([#2810](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2810)) + +### πŸ› Bug Fixes + +* [Chore] Visualize link fix [#2395](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2395) +* [BUG] Fix suggestion list cutoff issue ([#2607](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2607)) +* Remove Leftover X Pack references ([#2638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2638)) +* Bump `del` version to fix MacOS race condition ([#2847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2873)) +* Temporary workaround for task-kill exceptions on Windows when it is passed a pid for a process that is already dead ([#2842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2842)) +* [Build] Fixed "Last Access Time" not being set by `scanCopy` on Windows ([#2964](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2964)) +* Update `leaflet-vega` and fix its usage ([#3005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3005)) + +### 🚞 Infrastructure + +* Update backport custom branch name to utilize head template ([#2766](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2766)) + +### πŸ›  Maintenance + +* Increment version to 1.3.7 [#2528](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2528) + +### πŸ”© Tests + +* Bump `chromedriver` to 106 to fix function test fail issue [#2514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2514) +* Fix incorrect validation of time values in JUnit Reporter ([#2965](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2965)) diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.8.md b/release-notes/opensearch-dashboards.release-notes-1.3.8.md new file mode 100644 index 00000000000..b14e84018da --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.8.md @@ -0,0 +1,29 @@ +# Version 1.3.8 Release Notes + +### πŸ›‘ Security + +- [CVE-2022-25901] Bump supertest from 2.0.5 to 2.0.12 ([#3326](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3326)) +- [CVE-2022-25860] Bump simple-git from 3.15.1 to 3.16.0 ([#3345](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3345)) +- [CVE-2022-46175] Bump json5 version from 1.0.1 and 2.2.1 to 1.0.2 and 2.2.3 ([#3201](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3201)) +- [CVE-2022-25912] Bump simple-git from 3.4.0 to 3.15.0 ([#3036](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3036)) +- Bump decode-uri-component from 0.2.0 to 0.2.2 ([#3009](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3009)) + +### πŸ› Bug Fixes + +- [BUG] Fixes misleading embeddable plugin error message ([#3043](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3043)) +- [BUG] Trim trailing slashes before checking no-restricted-path rule ([#3020](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3020)) + +### 🚞 Infrastructure + +- Lock workflow tests to Chrome and ChromeDriver 107 as the last combination that run on Node.js v10 ([#3299](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3299)) +- Update yarn timeout for GitHub workflow on Windows ([#3118](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3118)) +- Add Windows CI to the GitHub workflow ([#2966](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2966)) + +### πŸ“ Documentation + +- Fix documentation link for date math ([#3207](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3207)) + +### πŸ”© Tests + +- [BWC] Updates to BWC tests ([#1190](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1190)) +- Automates chromedriver version selection for tests ([#2990](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2990)) \ No newline at end of file diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.9.md b/release-notes/opensearch-dashboards.release-notes-1.3.9.md new file mode 100644 index 00000000000..638a99ce11f --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.9.md @@ -0,0 +1,31 @@ +# Version 1.3.9 Release Notes + +### πŸ›‘ Security + +- [CVE-2022-2499] Resolve qs from 6.5.2 and 6.7.0 to 6.11.0 in 1.x ([#3451](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3451)) +- [CVE-2020-36632] [REQUIRES PLUGIN VALIDATION] Bump flat from 4.1.1 to 5.0.2 ([#3539](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3539)). To the best of our knowledge, this is a non-breaking change, but if your plugin relies on `mocha` tests, validate that they still work correctly (and plan to migrate them to `jest` [in preparation for `mocha` deprecation](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1572). +- [CVE-2023-25653] Bump node-jose to 2.2.0 ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445)) +- [CVE-2021-23807] Bump jsonpointer from 4.1.0 to 5.0.1 ([#3535](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3535)) +- [CVE-2021-23424] Bump ansi-html from 0.0.7 to 0.0.8 ([#3536](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3536)) +- [CVE-2022-24999] Bump express from 4.17.1 to 4.18.2 ([#3542](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3542)) + +### πŸ“ˆ Features/Enhancements + +- [I18n] Register ru, ru-RU locale ([#2817](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2817)) + +### πŸ› Bug Fixes + +- [TSVB] Fix the link to "serial differencing aggregation" documentation ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) + +### πŸ“ Documentation + +- [TSVB] Fix a spelling error in the README file ([#3518](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3518)) +- Simplify the in-code instructions for upgrading `re2` ([#3328](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3328)) +- [Doc] Improve DEVELOPER_GUIDE to make first time setup quicker and easier ([#3421](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3421)) + +### πŸ›  Maintenance + +- Update MAINTAINERS.md formatting and maintainer list ([#3338](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3338)) +- Remove `github-checks-reporter`, an unused dependency ([#3126](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3126)) +- [Version] Increment to 1.3.9 ([#3375](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3375)) +- Remove the unused `renovate.json5` file ([3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) diff --git a/release-notes/opensearch-dashboards.release-notes-2.4.0.md b/release-notes/opensearch-dashboards.release-notes-2.4.0.md new file mode 100644 index 00000000000..aa7128f27bc --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.4.0.md @@ -0,0 +1,115 @@ +## Version 2.4.0 Release Notes + +### πŸ›‘ Security + +- Bump percy-agent to use non-beta version ([#2415](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2415)) +- Use a forced CSP-compliant interpreter with Vega visualizations ([#2352](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2352)) +- [CVE-2022-33987] Bump makelogs to remove dependency on got ([#2801](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2801)) +- [CVE-2022-33987] Upgrade geckodriver to 3.0.2 ([#2166](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2166)) +- [CVE-2022-3517] Bump minimatch to 3.0.5 and [IBM X-Force ID: 220063] unset-value to 2.0.1 ([#2640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2640)) +- [CVE-2022-37601] Bump loader-utils to 2.0.3 ([#2706](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2706)) +- [GMS-2022-4708] Resolve sub-dependent d3-color version and potential security issue ([#2454](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2454)) +- [Legacy Maps] Prevent reverse-tabnabbing ([#2540](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2540)) +- [WS-2022-0284] [WS-2022-0280] Bump moment-timezone from 0.5.34 to 0.5.37 ([#2361](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2361)) +- [Multi DataSource] Prevent spell-checking the password fields ([#2818](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2818)) + +### πŸ“ˆ Features/Enhancements + +- Add extension point in saved object management to register namespaces and show filter ([#2656](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2656)) +- Add updated_at column to Saved Objects' tables ([#1218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1218)) +- Change the links in the visualize plugin to use `href` rather than `onClick` ([#2395](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2395)) +- Improve Discover field summaries ([#2391](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2391)) +- Remove Add Integration button ([#2723](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2723)) +- [Multi DataSource] Add data source column into index pattern table ([#2542](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2542)) +- [Multi DataSource] Add data source config to opensearch-dashboards-docker ([#2557](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2557)) +- [Multi DataSource] Add data source signing support ([#2510](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2510)) +- [Multi DataSource] Add experimental callout for index pattern section ([#2523](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2523)) +- [Multi DataSource] Address UX comments on Data source list and create page ([#2625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2625)) +- [Multi DataSource] Apply get indices error handling in step index pattern ([#2652](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2652)) +- [Multi DataSource] Display error toast for create index pattern with data source ([#2506](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2506)) +- [Multi DataSource] Make text content dynamically translated & update unit tests ([#2570](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2570)) +- [Multi DataSource] Support legacy client for data source ([#2204](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2204)) +- [Multi DataSource] UX enhancement on Data source management creation page ([#2051](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2501)) +- [Multi DataSource] UX enhancement on Data source management stack ([#2521](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2521)) +- [Multi DataSource] UX enhancement on Index Pattern management stack ([#2505](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2505)) +- [Multi DataSource] UX enhancement on Index Pattern management stack ([#2505](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2505))([#2527](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2527)) +- [Multi DataSource] UX enhancement on Update stored password modal for Data source management stack ([#2532](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2532)) +- [Plugin Helpers] Facilitate version changes ([#2398](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2398)) +- [Vis Builder] Add an experimental table visualization in vis builder ([#2705](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2705)) +- [Vis Builder] Add field summary popovers ([#2682](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2682)) +- [Vis Builder] Add index pattern info when loading embeddable ([#2363](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2363)) +- [Vis Builder] Add state validation before dispatching and loading ([#2351](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2351)) +- [Vis Builder] Change VisBuilder flag for docker config ([#2804](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2804)) +- [Vis Builder] Change classname prefix wiz to vb ([#2581](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2581/files)) +- [Vis Builder] Change save object type, wizard id and name to visBuilder ([#2673](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2673)) +- [Vis Builder] Change wizard to vis_builder in file names and paths ([#2587](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2587)) +- [Vis Builder] Create a new wizard directly on a dashboard ([#2384](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2384)) +- [Vis Builder] Edit wizard directly on dashboard ([#2508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2508)) +- [Vis Builder] Enable VisBuilder by default ([#2725](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2725)) +- [Vis Builder] Rename wizard on save modal and visualization table ([#2645](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2645)) +- [Vis Builder] Rename wizard to visBuilder in class name, type name and function name ([#2639](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2639)) +- [Vis Builder] Rename wizard to visBuilder in i18n id and formatted message id ([#2635](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2635)) +- [Windows] Add cross-platform helpers ([#2681](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2681)) +- [Windows] Consume `@osd/cross-platform` package to standardize path handling across platforms ([#2703](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2703)) +- [Windows] Facilitate building and running OSD and plugins on Windows platforms ([#2601](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2601)) + +### πŸ› Bug Fixes + +- Fix management app breadcrumb error ([#2344](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2344)) +- Fix suggestion list cutoff issue ([#2607](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2607)) +- Remove Leftover X Pack references ([#2638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2638)) +- [Multi DataSource] Add data source param to low-level search call in Discover ([#2431](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2431)) +- [Multi DataSource] Address UX comments on Edit Data source page ([#2629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2629)) +- [Multi DataSource] Address UX comments on index pattern management stack ([#2611](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2611)) +- [Multi DataSource] Enhance data source error handling ([#2661](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2661)) +- [Multi DataSource] Skip data source view in index pattern step when default is chosen ([#2574](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2574)) +- [Multi DataSource] Update default audit log path ([#2793](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2793)) +- [Save Object Aggregation View] Fix for export all after scroll count response changed ([#2696](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2696)) +- [Vis Builder] Add additional aggregation parameters to Vislib charts (Bar, Line and Area) ([2610](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2610)) +- [Vis Builder] Add missing test subject property of `DisabledVisualization` ([2610](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2610)) +- [Vis Builder] Fix Date Histogram auto bounds showing per 0 millisecond ([#2632](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2632)) +- [Vis Builder] Fix Histogram updating bounds when date range updates ([#2632](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2632)) +- [Vis Builder] Fix auto bounds for time-series bar chart visualization ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) +- [Vis Builder] Fix broken UX after switching index pattern while editing an aggregation ([#2632](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2632)) +- [Vis Builder] Fix rendering issues wuth time series for new chart types ([#2309](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2309)) +- [Vis Builder] Fix the missing `Last Updated` timestamp in visualization list ([#2628](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2628)) +- [Vis Builder] Fix visualization shift when editing an aggregation ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) +- [Vis Builder] Rename "Histogram" to "Bar" in visualization type picker ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) +- [Table Visualization] Fix an issue preventing sorting the first column ([#2828](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2828)) +- Temporary workaround for task-kill exceptions on Windows when it is passed a pid for a process that is already dead ([#2842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2842)) + +### 🚞 Infrastructure + +- Add CHANGELOG.md and related workflows ([#2414](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2414)) +- Update WhiteSource scans to ignore Backward Compatibility artifacts in `cypress` ([#2408](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2408)) +- [CI] Add Backward Compatibility tests for 2.4.0 ([#2393](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2393)) +- [CI] Add path ignore for markdown files ([#2312](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2312)) +- [CI] Prevent backport workflow from running on unmerged PRs ([#2746](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2746)) +- [CI] Run functional test repo as workflow ([#2503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2503)) +- [CI] Update backport custom branch name ([#2766](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2766)) +- [CI] Update backport workflow to ignore changelog conflicts ([#2729](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2729)) + + +### πŸ“ Documentation + +- Add CHANGELOG.md and Release Notes for 2.4.0 ([#2809](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2809)) +- Add README.md for `dataSource` and `dataSourceManagement` plugins ([#2448](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2448)) +- Add README.md for saving index pattern relationship ([#2276](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2276)) +- Remove a repeated "to" from the README.md file ([#2403](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2403)) +- Update functional testing information in TESTING.md ([#2492](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2492)) +- [Multi DataSource] Add design documents of multiple data source feature [#2538](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2538) +- [Multi DataSource] Add sample configuration for multi data source to the yml template ([#2428](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2428)) +- [Multi DataSource] Tweak multiple data source design doc ([#2724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2724)) +- [Multi DataSource] Update MD data source documentation link ([#2693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2693)) + +### πŸ”© Tests + +- Update caniuse to fix failed integration tests ([#2322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2322)) +- [Multi DataSource] Add unit test coverage for Update Data source management stack ([#2567](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2567)) +- [Vis Builder] Enable VisBuilder cypress tests ([#2728](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2728)) + +### πŸ›  Maintenance + +- Add @zengyan-amazon as a maintainer ([#2419](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2419)) +- Increment from 2.3 to 2.4. ([#2295](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2295)) +- Add CHANGELOG.md for 2.4.0 ([#2809](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2809)) diff --git a/release-notes/opensearch-dashboards.release-notes-2.4.1.md b/release-notes/opensearch-dashboards.release-notes-2.4.1.md new file mode 100644 index 00000000000..84e3a787e6d --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.4.1.md @@ -0,0 +1,9 @@ +## Version 2.4.1 Release Notes + +### πŸ› Bug Fixes + +* Update `leaflet-vega` and fix its usage ([#3005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3005)) + +### πŸ”© Tests + +* Correct the linting logic for `no-restricted-path` to ignore trailing slashes ([#3020](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3020)) diff --git a/release-notes/opensearch-dashboards.release-notes-2.5.0.md b/release-notes/opensearch-dashboards.release-notes-2.5.0.md new file mode 100644 index 00000000000..6974867085d --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.5.0.md @@ -0,0 +1,70 @@ +## Version 2.5.0 Release Notes + +### πŸ›‘ Security + +- Introduce guidelines for reporting vulnerable dependencies ([#2674](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2674)) +- Bump decode-uri-component from 0.2.0 to 0.2.2 ([3009](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3009)) +- [CVE-2022-25912] Bump simple-git from 3.4.0 to 3.15.0 ([#3036](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3036)) +- [CVE-2022-35256] Bump node version from 14.20.0 to 14.20.1 [#3166](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3166)) +- [CVE-2022-46175] Bump json5 version from 1.0.1 and 2.2.1 to 1.0.2 and 2.2.3 ([#3201](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3201)) + +### πŸ“ˆ Features/Enhancements + +- [CLI] Enhance `yarn opensearch snapshot` to facilitate installing plugins on an OpenSearch cluster ([#2734](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2734)) +- [I18n] Register ru, ru-RU locale ([#2817](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2817)) +- [Multi DataSource] Introduce validation of new or modified connections to external data sources ([#2973](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2973), [#3110](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3110)) +- [VisBuilder] Create global data persistence for VisBuilder #2896 ([#2896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2896)) +- [VisBuilder] Introduce Redux store persistence ([#3088](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3088)) +- [VisBuilder] Enable persistence for app filter and query without using state containers ([#3100](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3100)) +- [Data] Make the newly created configurations get added to beginning of the `aggConfig` array when using `createAggConfig` ([#3160](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3160)) +- [Optimizer] Increase the amount of time an optimizer worker is provided to exit before throwing an error ([#3193](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3193)) + +### 🚞 Infrastructure + +- Bump the version of the `2.x` branch to 2.5.0 ([#2884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2884)) +- [CI] Create workflows that test and build on Windows ([#2966](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2966)) +- [CI] Automate ChromeDriver installation for running functional tests ([#2990](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2990)) +- Create the Release Notes for the 1.3.7 release ([#3066](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3066)) +- [CI] Improve workflows by retaining Yarn's cache folder ([#3194](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3194)) + +### πŸ“ Documentation + +- Publish the release runbook ([#2533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2533)) +- Document the capabilities of the Charts plugin and its current usage ([#2695](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2695)) +- Document the correct version selection switch in `@osd/plugin-helpers` ([#2810](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2810)) +- Document the global query persistence ([#3001](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3001)) +- Document data persistence for plugins ([#3081](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3081)) + +### πŸͺ› Refactoring + +- [VisBuilder] Extend the use of i18n ([#2867](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2867)) +- [Console] Switch to using `core.http` when calling OSD APIs in console ([#3080]https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3080)) +- [Table Visualization] Refactor table visualization using React and DataGrid component ([#2863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2863)) + +### πŸ› Bug Fixes + +- Upgrade the `del` library to fix a race condition on macOS ([#2847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2873)) +- [Table Visualization] Fix a problem with table visualizations that prevented URLs from being rendered correctly ([#2918](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2918)) +- [Embaddable] Fix a misleading error message ([#3043](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3043)) +- Fix rendering issues when the obsolete `v8 (beta)` theme was carried over by an upgrade ([#3045](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3045)) +- [Multi Datasource] Replace the mock URL in tests ([#3099](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3099)) +- [CI] Increase Yarn's timeout for installing dependencies in workflows ([#3118](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3118)) +- [VisBuilder] Fix an issue that caused a crash when certain filters were added to a table visualization ([#3210](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3210)) +- [VisBuilder] Fix errors throws when pipeline aggregations, like cumulative sum, were used in VisBuilder ([#3137](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3137)) +- [Region Maps] Fix the problem of join fields being unusable ([#3213](Fix bug that prevents selected join field to be used)) +- [Multi DataSource] Update test connection button text([#3247](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3247)) + +### πŸ”© Tests + +- Enable retrying of flaky tests ([#2967](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2967)) +- Enhance cross-platform testing of plugin installation on cluster snapshots ([#2994](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2994)) +- [Tests] Bump `chromedriver` to v107 ([#3017](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3017)) +- [CI] Disable the execution of the Build and Test workflow when the changes are limited to the docs folder ([#3197](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3197)) +- Correct the linting logic for `no-restricted-path` to ignore trailing slashes ([#3020](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3020)) +- [VisBuilder] Create unit tests for field utilities ([#3211](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3211)) + +### πŸ›  Maintenance + +- Remove an unused dependency on `github-checks-reporter` ([#3126](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3126)) +- Introduce `vega-lite@5`, aliased as `vega-lite-next` ([#3151](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3151)) + diff --git a/release-notes/opensearch-dashboards.release-notes-2.6.0.md b/release-notes/opensearch-dashboards.release-notes-2.6.0.md new file mode 100644 index 00000000000..9600975e3ec --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.6.0.md @@ -0,0 +1,47 @@ +## Version 2.6.0 Release Notes + +### πŸ›‘ Security + +- [CVE-2022-37599] Bump loader-utils from `2.0.3` to `2.0.4` ([#3031](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3031)) +- [CVE-2022-37603] Bump loader-utils from `2.0.3` to `2.0.4` ([#3031](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3031)) +- [CVE-2022-25860] Bump simple-git from `3.15.1` to `3.16.0` ([#3345](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3345)) +- [CVE-2022-25881] Resolve http-cache-semantics from `4.1.0` to `4.1.1` ([#3409](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3409)) +- [Security] Bump hapi/statehood from `7.0.3` to `7.0.4` ([#3411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3411)) +- [CVE-2023-25166] Bump formula from `3.0.0` to `3.0.1` ([#3416](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3416)) +- [CVE-2020-36632] Bump flat from `4.1.1` to `5.0.2` ([#3419](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3419)) +- [CVE-2023-25653] Bump node-jose from `2.1.1` to `2.2.0` ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445)) +- [CVE-2022-24999] Resolve qs from `6.5.3` to `6.11.0` ([#3450](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3450)) +- [CVE-2022-25758] Bump node-sass from `6.0.1` to `7.0.3` and sass-loader from `10.2.1` to `10.4.1` to bump scss-tokenizer from `0.2.3` to `0.4.3` ([#3455](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3455)) +- [CVE-2020-24025] Bump node-sass from `6.0.1` to `7.0.3` ([#3455](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3455)) + +### πŸ“ˆ Features/Enhancements + +- Add disablePrototypePoisoningProtection configuration to prevent JS client from erroring when cluster utilizes JS reserved words ([#2992](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2992)) +- [Multiple DataSource] Add support for SigV4 authentication ([#3058](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3058)) +- [Multiple DataSource] Refactor test connection to support SigV4 auth type ([#3456](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3456)) + +### πŸ› Bug Fixes + +- [Search Telemetry] Fix search telemetry's observable object that won't be GC-ed([#3390](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3390)) +- [Region Maps] Add ui setting to configure custom vector map's size parameter([#3399](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3399)) + +### 🚞 Infrastructure + +- Fix detection of Chrome's version on Darwin during CI ([#3296](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3296)) + +### πŸ“ Documentation + +- [Docs] Fix documentation link for date math ([#3207](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3207)) + +### πŸ›  Maintenance + +- Bump `re2` and `supertest` ([#3018](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3018)) +- Upgrade vega-tooltip to `0.30.0` to support custom tooltips ([#3359](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3359)) +- Allow relaxing the Node.js runtime version requirement ([#3402](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3402)) +- Make build scripts find and use the latest version of Node.js that satisfies `engines.node` ([#3467](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3467)) +- Add `@opensearch-project/opensearch@^2.x` as dependency aliased as `@opensearch-project/opensearch-next` ([#3469](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3469)) + +### πŸ”© Tests + +- [BWC Tests] Add BWC tests for `2.6.0` ([#3356](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3356)) +- Prevent primitive linting limitations from being applied to unit tests found under `src/setup_node_env` ([#3403](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3403)) diff --git a/renovate.json5 b/renovate.json5 deleted file mode 100644 index 642ba9540cc..00000000000 --- a/renovate.json5 +++ /dev/null @@ -1,1123 +0,0 @@ -/** - * PLEASE DO NOT MODIFY - * - * This file is automatically generated by running `node scripts/build_renovate_config` - * - */ -{ - extends: [ - 'config:base', - ], - includePaths: [ - 'package.json', - 'packages/*/package.json', - 'examples/*/package.json', - 'test/plugin_functional/plugins/*/package.json', - 'test/interpreter_functional/plugins/*/package.json', - ], - baseBranches: [ - 'master', - ], - labels: [ - 'release_note:skip', - 'Team:Operations', - 'renovate', - 'v8.0.0', - 'v7.9.0', - ], - major: { - labels: [ - 'release_note:skip', - 'Team:Operations', - 'renovate', - 'v8.0.0', - 'v7.9.0', - 'renovate:major', - ], - }, - separateMajorMinor: false, - masterIssue: true, - masterIssueApproval: true, - rangeStrategy: 'bump', - npm: { - lockFileMaintenance: { - enabled: false, - }, - packageRules: [ - { - groupSlug: '@elastic/charts', - groupName: '@elastic/charts related packages', - packageNames: [ - '@elastic/charts', - '@types/elastic__charts', - ], - reviewers: [ - 'markov00', - ], - masterIssueApproval: false, - }, - { - groupSlug: '@reach/router', - groupName: '@reach/router related packages', - packageNames: [ - '@reach/router', - '@types/reach__router', - ], - }, - { - groupSlug: '@testing-library/dom', - groupName: '@testing-library/dom related packages', - packageNames: [ - '@testing-library/dom', - '@types/testing-library__dom', - ], - }, - { - groupSlug: 'angular', - groupName: 'angular related packages', - packagePatterns: [ - '(\\b|_)angular(\\b|_)', - ], - }, - { - groupSlug: 'api-documenter', - groupName: 'api-documenter related packages', - packageNames: [ - '@microsoft/api-documenter', - '@types/microsoft__api-documenter', - '@microsoft/api-extractor', - '@types/microsoft__api-extractor', - ], - enabled: false, - }, - { - groupSlug: 'archiver', - groupName: 'archiver related packages', - packageNames: [ - 'archiver', - '@types/archiver', - ], - }, - { - groupSlug: 'babel', - groupName: 'babel related packages', - packagePatterns: [ - '(\\b|_)babel(\\b|_)', - ], - packageNames: [ - 'core-js', - '@types/core-js', - '@babel/preset-react', - '@types/babel__preset-react', - '@babel/preset-typescript', - '@types/babel__preset-typescript', - ], - }, - { - groupSlug: 'base64-js', - groupName: 'base64-js related packages', - packageNames: [ - 'base64-js', - '@types/base64-js', - ], - }, - { - groupSlug: 'bluebird', - groupName: 'bluebird related packages', - packageNames: [ - 'bluebird', - '@types/bluebird', - ], - }, - { - groupSlug: 'browserslist-useragent', - groupName: 'browserslist-useragent related packages', - packageNames: [ - 'browserslist-useragent', - '@types/browserslist-useragent', - ], - }, - { - groupSlug: 'chance', - groupName: 'chance related packages', - packageNames: [ - 'chance', - '@types/chance', - ], - }, - { - groupSlug: 'cheerio', - groupName: 'cheerio related packages', - packageNames: [ - 'cheerio', - '@types/cheerio', - ], - }, - { - groupSlug: 'chroma-js', - groupName: 'chroma-js related packages', - packageNames: [ - 'chroma-js', - '@types/chroma-js', - ], - }, - { - groupSlug: 'chromedriver', - groupName: 'chromedriver related packages', - packageNames: [ - 'chromedriver', - '@types/chromedriver', - ], - }, - { - groupSlug: 'classnames', - groupName: 'classnames related packages', - packageNames: [ - 'classnames', - '@types/classnames', - ], - }, - { - groupSlug: 'cmd-shim', - groupName: 'cmd-shim related packages', - packageNames: [ - 'cmd-shim', - '@types/cmd-shim', - ], - }, - { - groupSlug: 'color', - groupName: 'color related packages', - packageNames: [ - 'color', - '@types/color', - ], - }, - { - groupSlug: 'cpy', - groupName: 'cpy related packages', - packageNames: [ - 'cpy', - '@types/cpy', - ], - }, - { - groupSlug: 'cytoscape', - groupName: 'cytoscape related packages', - packageNames: [ - 'cytoscape', - '@types/cytoscape', - ], - }, - { - groupSlug: 'd3', - groupName: 'd3 related packages', - packagePatterns: [ - '(\\b|_)d3(\\b|_)', - ], - }, - { - groupSlug: 'dedent', - groupName: 'dedent related packages', - packageNames: [ - 'dedent', - '@types/dedent', - ], - }, - { - groupSlug: 'deep-freeze-strict', - groupName: 'deep-freeze-strict related packages', - packageNames: [ - 'deep-freeze-strict', - '@types/deep-freeze-strict', - ], - }, - { - groupSlug: 'delete-empty', - groupName: 'delete-empty related packages', - packageNames: [ - 'delete-empty', - '@types/delete-empty', - ], - }, - { - groupSlug: 'dragselect', - groupName: 'dragselect related packages', - packageNames: [ - 'dragselect', - '@types/dragselect', - ], - labels: [ - 'release_note:skip', - 'Team:Operations', - 'renovate', - 'v8.0.0', - 'v7.9.0', - ':ml', - ], - }, - { - groupSlug: 'elasticsearch', - groupName: 'elasticsearch related packages', - packageNames: [ - 'elasticsearch', - '@types/elasticsearch', - ], - }, - { - groupSlug: 'eslint', - groupName: 'eslint related packages', - packagePatterns: [ - '(\\b|_)eslint(\\b|_)', - ], - }, - { - groupSlug: 'estree', - groupName: 'estree related packages', - packageNames: [ - 'estree', - '@types/estree', - ], - }, - { - groupSlug: 'fancy-log', - groupName: 'fancy-log related packages', - packageNames: [ - 'fancy-log', - '@types/fancy-log', - ], - }, - { - groupSlug: 'fetch-mock', - groupName: 'fetch-mock related packages', - packageNames: [ - 'fetch-mock', - '@types/fetch-mock', - ], - }, - { - groupSlug: 'file-saver', - groupName: 'file-saver related packages', - packageNames: [ - 'file-saver', - '@types/file-saver', - ], - }, - { - groupSlug: 'flot', - groupName: 'flot related packages', - packageNames: [ - 'flot', - '@types/flot', - ], - }, - { - groupSlug: 'geojson', - groupName: 'geojson related packages', - packageNames: [ - 'geojson', - '@types/geojson', - ], - }, - { - groupSlug: 'getopts', - groupName: 'getopts related packages', - packageNames: [ - 'getopts', - '@types/getopts', - ], - }, - { - groupSlug: 'getos', - groupName: 'getos related packages', - packageNames: [ - 'getos', - '@types/getos', - ], - }, - { - groupSlug: 'git-url-parse', - groupName: 'git-url-parse related packages', - packageNames: [ - 'git-url-parse', - '@types/git-url-parse', - ], - }, - { - groupSlug: 'glob', - groupName: 'glob related packages', - packageNames: [ - 'glob', - '@types/glob', - ], - }, - { - groupSlug: 'globby', - groupName: 'globby related packages', - packageNames: [ - 'globby', - '@types/globby', - ], - }, - { - groupSlug: 'graphql', - groupName: 'graphql related packages', - packagePatterns: [ - '(\\b|_)graphql(\\b|_)', - '(\\b|_)apollo(\\b|_)', - ], - }, - { - groupSlug: 'grunt', - groupName: 'grunt related packages', - packagePatterns: [ - '(\\b|_)grunt(\\b|_)', - ], - }, - { - groupSlug: 'gulp', - groupName: 'gulp related packages', - packagePatterns: [ - '(\\b|_)gulp(\\b|_)', - ], - }, - { - groupSlug: 'hapi', - groupName: 'hapi related packages', - packagePatterns: [ - '(\\b|_)hapi(\\b|_)', - ], - packageNames: [ - 'hapi', - '@types/hapi', - 'joi', - '@types/joi', - 'boom', - '@types/boom', - 'hoek', - '@types/hoek', - 'h2o2', - '@types/h2o2', - '@elastic/good', - '@types/elastic__good', - 'good-squeeze', - '@types/good-squeeze', - 'inert', - '@types/inert', - 'accept', - '@types/accept', - ], - }, - { - groupSlug: 'has-ansi', - groupName: 'has-ansi related packages', - packageNames: [ - 'has-ansi', - '@types/has-ansi', - ], - }, - { - groupSlug: 'he', - groupName: 'he related packages', - packageNames: [ - 'he', - '@types/he', - ], - }, - { - groupSlug: 'history', - groupName: 'history related packages', - packageNames: [ - 'history', - '@types/history', - ], - }, - { - groupSlug: 'hjson', - groupName: 'hjson related packages', - packageNames: [ - 'hjson', - '@types/hjson', - ], - }, - { - groupSlug: 'inquirer', - groupName: 'inquirer related packages', - packageNames: [ - 'inquirer', - '@types/inquirer', - ], - }, - { - groupSlug: 'intl-relativeformat', - groupName: 'intl-relativeformat related packages', - packageNames: [ - 'intl-relativeformat', - '@types/intl-relativeformat', - ], - }, - { - groupSlug: 'jest', - groupName: 'jest related packages', - packagePatterns: [ - '(\\b|_)jest(\\b|_)', - ], - }, - { - groupSlug: 'jquery', - groupName: 'jquery related packages', - packageNames: [ - 'jquery', - '@types/jquery', - ], - }, - { - groupSlug: 'js-search', - groupName: 'js-search related packages', - packageNames: [ - 'js-search', - '@types/js-search', - ], - }, - { - groupSlug: 'js-yaml', - groupName: 'js-yaml related packages', - packageNames: [ - 'js-yaml', - '@types/js-yaml', - ], - }, - { - groupSlug: 'jsdom', - groupName: 'jsdom related packages', - packageNames: [ - 'jsdom', - '@types/jsdom', - ], - }, - { - groupSlug: 'json-stable-stringify', - groupName: 'json-stable-stringify related packages', - packageNames: [ - 'json-stable-stringify', - '@types/json-stable-stringify', - ], - }, - { - groupSlug: 'json5', - groupName: 'json5 related packages', - packageNames: [ - 'json5', - '@types/json5', - ], - }, - { - groupSlug: 'jsonwebtoken', - groupName: 'jsonwebtoken related packages', - packageNames: [ - 'jsonwebtoken', - '@types/jsonwebtoken', - ], - }, - { - groupSlug: 'jsts', - groupName: 'jsts related packages', - packageNames: [ - 'jsts', - '@types/jsts', - ], - allowedVersions: '^1.6.2', - }, - { - groupSlug: 'karma', - groupName: 'karma related packages', - packagePatterns: [ - '(\\b|_)karma(\\b|_)', - ], - }, - { - groupSlug: 'language server', - groupName: 'language server related packages', - packageNames: [ - 'vscode-jsonrpc', - '@types/vscode-jsonrpc', - 'vscode-languageserver', - '@types/vscode-languageserver', - 'vscode-languageserver-types', - '@types/vscode-languageserver-types', - ], - }, - { - groupSlug: 'license-checker', - groupName: 'license-checker related packages', - packageNames: [ - 'license-checker', - '@types/license-checker', - ], - }, - { - groupSlug: 'listr', - groupName: 'listr related packages', - packageNames: [ - 'listr', - '@types/listr', - ], - }, - { - groupSlug: 'lodash', - groupName: 'lodash related packages', - packageNames: [ - 'lodash', - '@types/lodash', - ], - }, - { - groupSlug: 'log-symbols', - groupName: 'log-symbols related packages', - packageNames: [ - 'log-symbols', - '@types/log-symbols', - ], - }, - { - groupSlug: 'lru-cache', - groupName: 'lru-cache related packages', - packageNames: [ - 'lru-cache', - '@types/lru-cache', - ], - }, - { - groupSlug: 'mapbox-gl', - groupName: 'mapbox-gl related packages', - packageNames: [ - 'mapbox-gl', - '@types/mapbox-gl', - ], - }, - { - groupSlug: 'markdown-it', - groupName: 'markdown-it related packages', - packageNames: [ - 'markdown-it', - '@types/markdown-it', - ], - }, - { - groupSlug: 'memoize-one', - groupName: 'memoize-one related packages', - packageNames: [ - 'memoize-one', - '@types/memoize-one', - ], - }, - { - groupSlug: 'mime', - groupName: 'mime related packages', - packageNames: [ - 'mime', - '@types/mime', - ], - }, - { - groupSlug: 'minimatch', - groupName: 'minimatch related packages', - packageNames: [ - 'minimatch', - '@types/minimatch', - ], - }, - { - groupSlug: 'mocha', - groupName: 'mocha related packages', - packagePatterns: [ - '(\\b|_)mocha(\\b|_)', - ], - }, - { - groupSlug: 'mock-fs', - groupName: 'mock-fs related packages', - packageNames: [ - 'mock-fs', - '@types/mock-fs', - ], - }, - { - groupSlug: 'moment', - groupName: 'moment related packages', - packagePatterns: [ - '(\\b|_)moment(\\b|_)', - ], - }, - { - groupSlug: 'mustache', - groupName: 'mustache related packages', - packageNames: [ - 'mustache', - '@types/mustache', - ], - }, - { - groupSlug: 'ncp', - groupName: 'ncp related packages', - packageNames: [ - 'ncp', - '@types/ncp', - ], - }, - { - groupSlug: 'nock', - groupName: 'nock related packages', - packageNames: [ - 'nock', - '@types/nock', - ], - }, - { - groupSlug: 'node', - groupName: 'node related packages', - packageNames: [ - 'node', - '@types/node', - ], - }, - { - groupSlug: 'node-fetch', - groupName: 'node-fetch related packages', - packageNames: [ - 'node-fetch', - '@types/node-fetch', - ], - }, - { - groupSlug: 'node-forge', - groupName: 'node-forge related packages', - packageNames: [ - 'node-forge', - '@types/node-forge', - ], - }, - { - groupSlug: 'node-sass', - groupName: 'node-sass related packages', - packageNames: [ - 'node-sass', - '@types/node-sass', - ], - }, - { - groupSlug: 'nodemailer', - groupName: 'nodemailer related packages', - packageNames: [ - 'nodemailer', - '@types/nodemailer', - ], - }, - { - groupSlug: 'normalize-path', - groupName: 'normalize-path related packages', - packageNames: [ - 'normalize-path', - '@types/normalize-path', - ], - }, - { - groupSlug: 'object-hash', - groupName: 'object-hash related packages', - packageNames: [ - 'object-hash', - '@types/object-hash', - ], - }, - { - groupSlug: 'opn', - groupName: 'opn related packages', - packageNames: [ - 'opn', - '@types/opn', - ], - }, - { - groupSlug: 'ora', - groupName: 'ora related packages', - packageNames: [ - 'ora', - '@types/ora', - ], - }, - { - groupSlug: 'papaparse', - groupName: 'papaparse related packages', - packageNames: [ - 'papaparse', - '@types/papaparse', - ], - }, - { - groupSlug: 'parse-link-header', - groupName: 'parse-link-header related packages', - packageNames: [ - 'parse-link-header', - '@types/parse-link-header', - ], - }, - { - groupSlug: 'pegjs', - groupName: 'pegjs related packages', - packageNames: [ - 'pegjs', - '@types/pegjs', - ], - }, - { - groupSlug: 'pngjs', - groupName: 'pngjs related packages', - packageNames: [ - 'pngjs', - '@types/pngjs', - ], - }, - { - groupSlug: 'podium', - groupName: 'podium related packages', - packageNames: [ - 'podium', - '@types/podium', - ], - }, - { - groupSlug: 'pretty-ms', - groupName: 'pretty-ms related packages', - packageNames: [ - 'pretty-ms', - '@types/pretty-ms', - ], - }, - { - groupSlug: 'proper-lockfile', - groupName: 'proper-lockfile related packages', - packageNames: [ - 'proper-lockfile', - '@types/proper-lockfile', - ], - }, - { - groupSlug: 'puppeteer', - groupName: 'puppeteer related packages', - packageNames: [ - 'puppeteer', - '@types/puppeteer', - ], - }, - { - groupSlug: 'react', - groupName: 'react related packages', - packagePatterns: [ - '(\\b|_)react(\\b|_)', - '(\\b|_)redux(\\b|_)', - '(\\b|_)enzyme(\\b|_)', - ], - packageNames: [ - 'ngreact', - '@types/ngreact', - 'recompose', - '@types/recompose', - 'prop-types', - '@types/prop-types', - 'typescript-fsa-reducers', - '@types/typescript-fsa-reducers', - 'reselect', - '@types/reselect', - ], - }, - { - groupSlug: 'read-pkg', - groupName: 'read-pkg related packages', - packageNames: [ - 'read-pkg', - '@types/read-pkg', - ], - }, - { - groupSlug: 'reduce-reducers', - groupName: 'reduce-reducers related packages', - packageNames: [ - 'reduce-reducers', - '@types/reduce-reducers', - ], - }, - { - groupSlug: 'request', - groupName: 'request related packages', - packageNames: [ - 'request', - '@types/request', - ], - }, - { - groupSlug: 'selenium-webdriver', - groupName: 'selenium-webdriver related packages', - packageNames: [ - 'selenium-webdriver', - '@types/selenium-webdriver', - ], - }, - { - groupSlug: 'semver', - groupName: 'semver related packages', - packageNames: [ - 'semver', - '@types/semver', - ], - }, - { - groupSlug: 'set-value', - groupName: 'set-value related packages', - packageNames: [ - 'set-value', - '@types/set-value', - ], - }, - { - groupSlug: 'sinon', - groupName: 'sinon related packages', - packageNames: [ - 'sinon', - '@types/sinon', - ], - }, - { - groupSlug: 'stats-lite', - groupName: 'stats-lite related packages', - packageNames: [ - 'stats-lite', - '@types/stats-lite', - ], - }, - { - groupSlug: 'storybook', - groupName: 'storybook related packages', - packagePatterns: [ - '(\\b|_)storybook(\\b|_)', - ], - }, - { - groupSlug: 'strip-ansi', - groupName: 'strip-ansi related packages', - packageNames: [ - 'strip-ansi', - '@types/strip-ansi', - ], - }, - { - groupSlug: 'strong-log-transformer', - groupName: 'strong-log-transformer related packages', - packageNames: [ - 'strong-log-transformer', - '@types/strong-log-transformer', - ], - }, - { - groupSlug: 'styled-components', - groupName: 'styled-components related packages', - packageNames: [ - 'styled-components', - '@types/styled-components', - ], - }, - { - groupSlug: 'supertest', - groupName: 'supertest related packages', - packageNames: [ - 'supertest', - '@types/supertest', - ], - }, - { - groupSlug: 'supertest-as-promised', - groupName: 'supertest-as-promised related packages', - packageNames: [ - 'supertest-as-promised', - '@types/supertest-as-promised', - ], - }, - { - groupSlug: 'tar', - groupName: 'tar related packages', - packageNames: [ - 'tar', - '@types/tar', - ], - }, - { - groupSlug: 'tar-fs', - groupName: 'tar-fs related packages', - packageNames: [ - 'tar-fs', - '@types/tar-fs', - ], - }, - { - groupSlug: 'tempy', - groupName: 'tempy related packages', - packageNames: [ - 'tempy', - '@types/tempy', - ], - }, - { - groupSlug: 'through2', - groupName: 'through2 related packages', - packageNames: [ - 'through2', - '@types/through2', - ], - }, - { - groupSlug: 'through2-map', - groupName: 'through2-map related packages', - packageNames: [ - 'through2-map', - '@types/through2-map', - ], - }, - { - groupSlug: 'tinycolor2', - groupName: 'tinycolor2 related packages', - packageNames: [ - 'tinycolor2', - '@types/tinycolor2', - ], - }, - { - groupSlug: 'type-detect', - groupName: 'type-detect related packages', - packageNames: [ - 'type-detect', - '@types/type-detect', - ], - }, - { - groupSlug: 'typescript', - groupName: 'typescript related packages', - packagePatterns: [ - '(\\b|_)ts(\\b|_)', - '(\\b|_)typescript(\\b|_)', - ], - packageNames: [ - 'tslib', - '@types/tslib', - ], - }, - { - groupSlug: 'use-resize-observer', - groupName: 'use-resize-observer related packages', - packageNames: [ - 'use-resize-observer', - '@types/use-resize-observer', - ], - }, - { - groupSlug: 'uuid', - groupName: 'uuid related packages', - packageNames: [ - 'uuid', - '@types/uuid', - ], - }, - { - groupSlug: 'vega', - groupName: 'vega related packages', - packagePatterns: [ - '(\\b|_)vega(\\b|_)', - ], - enabled: false, - }, - { - groupSlug: 'vinyl', - groupName: 'vinyl related packages', - packageNames: [ - 'vinyl', - '@types/vinyl', - ], - }, - { - groupSlug: 'vinyl-fs', - groupName: 'vinyl-fs related packages', - packageNames: [ - 'vinyl-fs', - '@types/vinyl-fs', - ], - }, - { - groupSlug: 'watchpack', - groupName: 'watchpack related packages', - packageNames: [ - 'watchpack', - '@types/watchpack', - ], - }, - { - groupSlug: 'webpack', - groupName: 'webpack related packages', - packagePatterns: [ - '(\\b|_)webpack(\\b|_)', - '(\\b|_)loader(\\b|_)', - '(\\b|_)acorn(\\b|_)', - '(\\b|_)terser(\\b|_)', - ], - packageNames: [ - 'mini-css-extract-plugin', - '@types/mini-css-extract-plugin', - 'chokidar', - '@types/chokidar', - ], - }, - { - groupSlug: 'write-pkg', - groupName: 'write-pkg related packages', - packageNames: [ - 'write-pkg', - '@types/write-pkg', - ], - }, - { - groupSlug: 'xml-crypto', - groupName: 'xml-crypto related packages', - packageNames: [ - 'xml-crypto', - '@types/xml-crypto', - ], - }, - { - groupSlug: 'xml2js', - groupName: 'xml2js related packages', - packageNames: [ - 'xml2js', - '@types/xml2js', - ], - }, - { - groupSlug: 'zen-observable', - groupName: 'zen-observable related packages', - packageNames: [ - 'zen-observable', - '@types/zen-observable', - ], - }, - { - packagePatterns: [ - '^@osd/.*', - ], - enabled: false, - }, - ], - }, - prConcurrentLimit: 0, - vulnerabilityAlerts: { - enabled: false, - }, - rebaseStalePrs: false, - rebaseConflictedPrs: false, - semanticCommits: false, -} diff --git a/scripts/upgrade_chromedriver.js b/scripts/upgrade_chromedriver.js new file mode 100644 index 00000000000..3aa896fd1fa --- /dev/null +++ b/scripts/upgrade_chromedriver.js @@ -0,0 +1,122 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Upgrades the chromedriver dev-dependency to the one supported by the version of Google Chrome + * installed on the machine. + * + * Usage: node scripts/upgrade_chromedriver.js [--install] + */ + +/* eslint no-restricted-syntax: 0 */ +const { execSync, spawnSync } = require('child_process'); +const { createReadStream, createWriteStream, unlinkSync, renameSync, existsSync } = require('fs'); +const { createInterface } = require('readline'); + +if (!process.argv.includes(__filename)) { + console.error('Usage: node scripts/upgrade_chromedriver.js [--install]'); + process.exit(1); +} + +const versionCheckCommands = []; + +switch (process.platform) { + case 'win32': + versionCheckCommands.push( + 'powershell "(Get-Item \\"$Env:Programfiles/Google/Chrome/Application/chrome.exe\\").VersionInfo.FileVersion"' + ); + break; + + case 'darwin': + versionCheckCommands.push( + '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --version' + ); + break; + + default: + versionCheckCommands.push( + ...[ + '/usr/bin', + '/usr/local/bin', + '/usr/sbin', + '/usr/local/sbin', + '/opt/bin', + '/usr/bin/X11', + '/usr/X11R6/bin', + ].flatMap((loc) => + [ + 'google-chrome --version', + 'google-chrome-stable --version', + 'chromium --version', + 'chromium-browser --version', + ].map((cmd) => `${loc}/${cmd}`) + ) + ); +} + +let versionCheckOutput; +versionCheckCommands.some((cmd) => { + try { + console.log(cmd); + versionCheckOutput = execSync(cmd, { encoding: 'utf8' })?.trim?.(); + return true; + } catch (e) { + console.log('Failed to get version using', cmd); + } +}); + +// Versions 90+ +const majorVersion = versionCheckOutput?.match?.(/(?:^|\s)(9\d|\d{3})\./)?.[1]; + +if (majorVersion) { + if (process.argv.includes('--install')) { + console.log(`Installing chromedriver@^${majorVersion}`); + + spawnSync(`yarn add --dev chromedriver@^${majorVersion}`, { + stdio: 'inherit', + cwd: process.cwd(), + shell: true, + }); + } else { + console.log(`Upgrading to chromedriver@^${majorVersion}`); + + let upgraded = false; + const writeStream = createWriteStream('package.json.upgrading-chromedriver', { flags: 'w' }); + const rl = createInterface({ + input: createReadStream('package.json'), + crlfDelay: Infinity, + }); + rl.on('line', (line) => { + if (line.includes('"chromedriver": "')) { + line = line.replace( + /"chromedriver":\s*"[~^]?\d[\d.]*\d"/, + `"chromedriver": "^${majorVersion}"` + ); + upgraded = true; + } + writeStream.write(line + '\n', 'utf8'); + }); + rl.on('close', () => { + writeStream.end(); + if (upgraded) { + // Remove any previous backups + if (existsSync('package.json.bak')) unlinkSync('package.json.bak'); + + renameSync('package.json', 'package.json.bak'); + renameSync('package.json.upgrading-chromedriver', 'package.json'); + + console.log(`Backed up package.json and updated chromedriver to ${majorVersion}`); + } else { + unlinkSync('package.json.upgrading-chromedriver'); + console.error( + `Failed to update chromedriver to ${majorVersion}. Try adding the \`--install\` switch.` + ); + } + }); + } +} else { + console.debug(versionCheckOutput); + console.error(`Failed to extract the version of the installed Google Chrome.`); +} diff --git a/src/cli_plugin/install/download.test.js b/src/cli_plugin/install/download.test.js index a847a92733a..126af128b11 100644 --- a/src/cli_plugin/install/download.test.js +++ b/src/cli_plugin/install/download.test.js @@ -28,7 +28,7 @@ * under the License. */ -import Fs from 'fs'; +import { mkdir } from 'fs/promises'; import { join } from 'path'; import http from 'http'; @@ -40,6 +40,7 @@ import del from 'del'; import { Logger } from '../lib/logger'; import { UnsupportedProtocolError } from '../lib/errors'; import { download, _downloadSingle, _getFilePath, _checkFilePathDeprecation } from './download'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; describe('opensearchDashboards cli', function () { describe('plugin downloader', function () { @@ -70,17 +71,17 @@ describe('opensearchDashboards cli', function () { throw new Error('expected the promise to reject'); } - beforeEach(function () { + beforeEach(async () => { sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); - del.sync(testWorkingPath); - Fs.mkdirSync(testWorkingPath, { recursive: true }); + await del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); + await mkdir(testWorkingPath, { recursive: true }); }); - afterEach(function () { + afterEach(async () => { logger.log.restore(); logger.error.restore(); - del.sync(testWorkingPath); + del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); }); describe('_downloadSingle', function () { diff --git a/src/cli_plugin/install/opensearch_dashboards.test.js b/src/cli_plugin/install/opensearch_dashboards.test.js index 2a219c9ba6c..74d2a675fe9 100644 --- a/src/cli_plugin/install/opensearch_dashboards.test.js +++ b/src/cli_plugin/install/opensearch_dashboards.test.js @@ -30,12 +30,14 @@ import { join } from 'path'; import fs from 'fs'; +import { mkdir } from 'fs/promises'; import sinon from 'sinon'; import del from 'del'; import { existingInstall, assertVersion } from './opensearch_dashboards'; import { Logger } from '../lib/logger'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; jest.spyOn(fs, 'statSync'); @@ -62,17 +64,17 @@ describe('opensearchDashboards cli', function () { const logger = new Logger(settings); describe('assertVersion', function () { - beforeEach(function () { - del.sync(testWorkingPath); - fs.mkdirSync(testWorkingPath, { recursive: true }); + beforeEach(async () => { + await del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); + await mkdir(testWorkingPath, { recursive: true }); sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); }); - afterEach(function () { + afterEach(async () => { logger.log.restore(); logger.error.restore(); - del.sync(testWorkingPath); + await del(testWorkingPath, { cwd: PROCESS_WORKING_DIR }); }); it('should succeed with exact match', function () { diff --git a/src/cli_plugin/install/pack.test.js b/src/cli_plugin/install/pack.test.js index 783593c6d9f..fa8f51fa273 100644 --- a/src/cli_plugin/install/pack.test.js +++ b/src/cli_plugin/install/pack.test.js @@ -28,8 +28,8 @@ * under the License. */ -import Fs from 'fs'; import { join } from 'path'; +import { mkdir } from 'fs/promises'; import sinon from 'sinon'; import glob from 'glob-all'; @@ -38,6 +38,7 @@ import del from 'del'; import { Logger } from '../lib/logger'; import { extract, getPackData } from './pack'; import { _downloadSingle } from './download'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; describe('opensearchDashboards cli', function () { describe('pack', function () { @@ -49,7 +50,7 @@ describe('opensearchDashboards cli', function () { let logger; let settings; - beforeEach(function () { + beforeEach(async () => { //These tests are dependent on the file system, and I had some inconsistent //behavior with del.sync show up. Until these tests are re-written to not //depend on the file system, I make sure that each test uses a different @@ -69,14 +70,14 @@ describe('opensearchDashboards cli', function () { logger = new Logger(settings); sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); - Fs.mkdirSync(testWorkingPath, { recursive: true }); + await mkdir(testWorkingPath, { recursive: true }); }); afterEach(async () => { logger.log.restore(); logger.error.restore(); - await del(workingPathRoot); + await del(workingPathRoot, { cwd: PROCESS_WORKING_DIR }); }); function copyReplyFile(filename) { diff --git a/src/cli_plugin/list/list.test.js b/src/cli_plugin/list/list.test.js index 7ae9663f73f..724b7c4ec67 100644 --- a/src/cli_plugin/list/list.test.js +++ b/src/cli_plugin/list/list.test.js @@ -30,10 +30,12 @@ import { join } from 'path'; import { writeFileSync, mkdirSync } from 'fs'; +import { mkdir } from 'fs/promises'; import del from 'del'; import { list } from './list'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; function createPlugin(name, version, pluginBaseDir) { const pluginDir = join(pluginBaseDir, name); @@ -61,14 +63,14 @@ describe('opensearchDashboards cli', function () { describe('plugin lister', function () { const pluginDir = join(__dirname, '.test.data.list'); - beforeEach(function () { + beforeEach(async () => { logger.messages.length = 0; - del.sync(pluginDir); - mkdirSync(pluginDir, { recursive: true }); + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); + await mkdir(pluginDir, { recursive: true }); }); - afterEach(function () { - del.sync(pluginDir); + afterEach(async () => { + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); }); it('list all of the folders in the plugin folder, ignoring dot prefixed plugins and regular files', function () { diff --git a/src/cli_plugin/remove/remove.test.js b/src/cli_plugin/remove/remove.test.js index 8c963df2de5..c1c498c4b40 100644 --- a/src/cli_plugin/remove/remove.test.js +++ b/src/cli_plugin/remove/remove.test.js @@ -29,7 +29,7 @@ */ import { join } from 'path'; -import { writeFileSync, mkdirSync } from 'fs'; +import { mkdir, writeFile } from 'fs/promises'; import sinon from 'sinon'; import glob from 'glob-all'; @@ -37,6 +37,7 @@ import del from 'del'; import { Logger } from '../lib/logger'; import { remove } from './remove'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; describe('opensearchDashboards cli', function () { describe('plugin remover', function () { @@ -46,20 +47,20 @@ describe('opensearchDashboards cli', function () { const settings = { pluginDir }; - beforeEach(function () { + beforeEach(async () => { processExitStub = sinon.stub(process, 'exit'); logger = new Logger(settings); sinon.stub(logger, 'log'); sinon.stub(logger, 'error'); - del.sync(pluginDir); - mkdirSync(pluginDir, { recursive: true }); + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); + await mkdir(pluginDir, { recursive: true }); }); - afterEach(function () { + afterEach(async () => { processExitStub.restore(); logger.log.restore(); logger.error.restore(); - del.sync(pluginDir); + await del(pluginDir, { cwd: PROCESS_WORKING_DIR }); }); it('throw an error if the plugin is not installed.', function () { @@ -71,18 +72,18 @@ describe('opensearchDashboards cli', function () { expect(process.exit.called).toBe(true); }); - it('throw an error if the specified plugin is not a folder.', function () { - writeFileSync(join(pluginDir, 'foo'), 'This is a file, and not a folder.'); + it('throw an error if the specified plugin is not a folder.', async () => { + await writeFile(join(pluginDir, 'foo'), 'This is a file, and not a folder.'); remove(settings, logger); expect(logger.error.firstCall.args[0]).toMatch(/not a plugin/); expect(process.exit.called).toBe(true); }); - it('delete the specified folder.', function () { + it('delete the specified folder.', async () => { settings.pluginPath = join(pluginDir, 'foo'); - mkdirSync(join(pluginDir, 'foo'), { recursive: true }); - mkdirSync(join(pluginDir, 'bar'), { recursive: true }); + await mkdir(join(pluginDir, 'foo'), { recursive: true }); + await mkdir(join(pluginDir, 'bar'), { recursive: true }); remove(settings, logger); diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 5d24b54d992..7c3de41e575 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -171,7 +171,7 @@ import { MyAppRoot } from './components/app.ts'; */ export const renderApp = ( core: CoreStart, - deps: MyPluginDepsStart, + deps: MyPluginStartDeps, { element, history }: AppMountParameters ) => { ReactDOM.render(, element); @@ -182,10 +182,10 @@ export const renderApp = ( ```ts // my_plugin/public/plugin.ts -import { Plugin } from '../../src/core/public'; +import { Plugin, CoreSetup } from '../../src/core/public'; export class MyPlugin implements Plugin { - public setup(core) { + public setup(core: CoreSetup) { core.application.register({ id: 'my-app', async mount(params) { @@ -200,14 +200,14 @@ export class MyPlugin implements Plugin { } ``` -Prefer the pattern shown above, using `core.getStartServices()`, rather than store local references retrieved from `start`. +Prefer the pattern shown above, using `core.getStartServices()`, rather than store local references retrieved from `start`. **Bad:** ```ts export class MyPlugin implements Plugin { // Anti pattern private coreStart?: CoreStart; - private depsStart?: DepsStart; + private depsStart?: DepsStart; public setup(core) { core.application.register({ @@ -218,7 +218,7 @@ export class MyPlugin implements Plugin { return renderApp(this.coreStart, this.depsStart, params); } }); - } + } public start(core, deps) { // Anti pattern @@ -359,5 +359,5 @@ Migration example from the legacy format is available in `src/core/MIGRATION_EXA ### Naming conventions -Export start and setup contracts as `MyPluginStart` and `MyPluginSetup`. +Export start and setup contracts as `MyPluginStart` and `MyPluginSetup`. This avoids naming clashes, if everyone exported them simply as `Start` and `Setup`. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 52c72e3372b..109e3d9c177 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -338,6 +338,8 @@ export class DocLinksService { close: `${OPENSEARCH_VERSIONED_DOCS}rest-api/index-apis/close-index/`, }, }, + // https://opensearch.org/docs/latest/opensearch/supported-field-types/date/#date-math + dateMath: `${OPENSEARCH_VERSIONED_DOCS}supported-field-types/date/#date-math`, }, opensearchDashboards: { // https://opensearch.org/docs/latest/dashboards/index/ @@ -386,6 +388,7 @@ export class DocLinksService { // https://opensearch.org/docs/latest/dashboards/dql/#nested-field-query nested_query: `${OPENSEARCH_DASHBOARDS_VERSIONED_DOCS}dql/#nested-field-query`, }, + // https://opensearch.org/docs/latest/dashboards/browser-compatibility browser: `${OPENSEARCH_DASHBOARDS_VERSIONED_DOCS}browser-compatibility`, }, noDocumentation: { @@ -403,6 +406,9 @@ export class DocLinksService { loadingData: `${OPENSEARCH_WEBSITE_DOCS}`, introduction: `${OPENSEARCH_WEBSITE_DOCS}`, }, + dataSource: { + guide: `${OPENSEARCH_DASHBOARDS_VERSIONED_DOCS}discover/multi-data-sources/`, + }, management: { opensearchDashboardsGeneralSettings: `${OPENSEARCH_WEBSITE_DOCS}`, opensearchDashboardsSearchSettings: `${OPENSEARCH_WEBSITE_DOCS}`, @@ -422,7 +428,6 @@ export class DocLinksService { }, addData: `${OPENSEARCH_WEBSITE_DOCS}`, vega: `${OPENSEARCH_DASHBOARDS_VERSIONED_DOCS}`, - dateMath: `${OPENSEARCH_WEBSITE_DOCS}`, savedObject: { manageSavedObject: `https://opensearch.org/docs/latest/guide/en/kibana/current/managing-saved-objects.html#_export`, }, @@ -715,6 +720,7 @@ export interface DocLinksStart { readonly close: string; }; }; + readonly dateMath: string; }; readonly opensearchDashboards: { readonly introduction: string; @@ -759,6 +765,9 @@ export interface DocLinksStart { readonly loadingData: string; readonly introduction: string; }; + readonly dataSource: { + readonly guide: string; + }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; @@ -771,7 +780,6 @@ export interface DocLinksStart { readonly visualize: Record; readonly addData: string; readonly vega: string; - readonly dateMath: string; readonly savedObject: { readonly manageSavedObject: string; }; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 163e34b4cf1..e863d627c80 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -83,6 +83,7 @@ function createCoreSetupMock({ uiSettings: uiSettingsServiceMock.createSetupContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, + getBranding: injectedMetadataServiceMock.createSetupContract().getBranding, }, }; diff --git a/src/core/server/opensearch/client/client_config.test.ts b/src/core/server/opensearch/client/client_config.test.ts index d32508ec43a..b3d1ff95d7d 100644 --- a/src/core/server/opensearch/client/client_config.test.ts +++ b/src/core/server/opensearch/client/client_config.test.ts @@ -184,6 +184,24 @@ describe('parseClientOptions', () => { ] `); }); + + it('`disablePrototypePoisoningProtection` option', () => { + expect( + parseClientOptions(createConfig({ disablePrototypePoisoningProtection: false }), false) + .disablePrototypePoisoningProtection + ).toEqual(false); + expect( + parseClientOptions(createConfig({ disablePrototypePoisoningProtection: true }), false) + .disablePrototypePoisoningProtection + ).toEqual(true); + + expect( + parseClientOptions(createConfig({}), false).disablePrototypePoisoningProtection + ).toBeUndefined(); + expect( + parseClientOptions(createConfig({}), true).disablePrototypePoisoningProtection + ).toBeUndefined(); + }); }); describe('authorization', () => { diff --git a/src/core/server/opensearch/client/client_config.ts b/src/core/server/opensearch/client/client_config.ts index 6746ee5648e..0432bb8ddfd 100644 --- a/src/core/server/opensearch/client/client_config.ts +++ b/src/core/server/opensearch/client/client_config.ts @@ -52,6 +52,7 @@ export type OpenSearchClientConfig = Pick< | 'hosts' | 'username' | 'password' + | 'disablePrototypePoisoningProtection' > & { memoryCircuitBreaker?: | OpenSearchConfig['memoryCircuitBreaker'] @@ -115,6 +116,10 @@ export function parseClientOptions(config: OpenSearchClientConfig, scoped: boole ); } + if (config.disablePrototypePoisoningProtection != null) { + clientOptions.disablePrototypePoisoningProtection = config.disablePrototypePoisoningProtection; + } + return clientOptions; } diff --git a/src/core/server/opensearch/opensearch_config.test.ts b/src/core/server/opensearch/opensearch_config.test.ts index 25cbee718d0..d7a17c12d29 100644 --- a/src/core/server/opensearch/opensearch_config.test.ts +++ b/src/core/server/opensearch/opensearch_config.test.ts @@ -72,6 +72,7 @@ test('set correct defaults', () => { OpenSearchConfig { "apiVersion": "7.x", "customHeaders": Object {}, + "disablePrototypePoisoningProtection": undefined, "healthCheckDelay": "PT2.5S", "hosts": Array [ "http://localhost:9200", diff --git a/src/core/server/opensearch/opensearch_config.ts b/src/core/server/opensearch/opensearch_config.ts index 9b7bdff21cd..fee26c354fb 100644 --- a/src/core/server/opensearch/opensearch_config.ts +++ b/src/core/server/opensearch/opensearch_config.ts @@ -142,6 +142,7 @@ export const configSchema = schema.object({ }), schema.boolean({ defaultValue: false }) ), + disablePrototypePoisoningProtection: schema.maybe(schema.boolean({ defaultValue: false })), }); const deprecations: ConfigDeprecationProvider = ({ renameFromRoot, renameFromRootWithoutMap }) => [ @@ -318,6 +319,12 @@ export class OpenSearchConfig { */ public readonly customHeaders: OpenSearchConfigType['customHeaders']; + /** + * Specifies whether the client should attempt to protect against reserved words + * or not. + */ + public readonly disablePrototypePoisoningProtection?: boolean; + constructor(rawConfig: OpenSearchConfigType) { this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch; this.apiVersion = rawConfig.apiVersion; @@ -338,6 +345,7 @@ export class OpenSearchConfig { this.username = rawConfig.username; this.password = rawConfig.password; this.customHeaders = rawConfig.customHeaders; + this.disablePrototypePoisoningProtection = rawConfig.disablePrototypePoisoningProtection; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 92b2cb71ef9..550acaf3f4f 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -42,9 +42,7 @@ import { PluginsConfig, PluginsConfigType, config } from '../plugins_config'; import type { InstanceInfo } from '../plugin_context'; import { discover } from './plugins_discovery'; import { CoreContext } from '../../core_context'; - -const OPENSEARCH_DASHBOARDS_ROOT = process.cwd(); -const EXTENDED_PATH_PREFIX = process.platform === 'win32' ? '\\\\?\\' : ''; +import { PROCESS_WORKING_DIR, standardize } from '@osd/cross-platform'; const Plugins = { invalid: () => ({ @@ -87,13 +85,7 @@ const packageMock = { }; const manifestPath = (...pluginPath: string[]) => - resolve( - OPENSEARCH_DASHBOARDS_ROOT, - 'src', - 'plugins', - ...pluginPath, - 'opensearch_dashboards.json' - ); + resolve(PROCESS_WORKING_DIR, 'src', 'plugins', ...pluginPath, 'opensearch_dashboards.json'); describe('plugins discovery system', () => { let logger: ReturnType; @@ -157,8 +149,8 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/plugins/plugin_b`]: Plugins.valid('pluginB'), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), + [`${PROCESS_WORKING_DIR}/plugins/plugin_b`]: Plugins.valid('pluginB'), }, { createCwd: false } ); @@ -179,10 +171,10 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: Plugins.invalid(), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_b`]: Plugins.incomplete(), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_c`]: Plugins.incompatible(), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_ad`]: Plugins.missingManifest(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: Plugins.invalid(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_b`]: Plugins.incomplete(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_c`]: Plugins.incompatible(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_ad`]: Plugins.missingManifest(), }, { createCwd: false } ); @@ -221,7 +213,7 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins`]: mockFs.directory({ + [`${PROCESS_WORKING_DIR}/src/plugins`]: mockFs.directory({ mode: 0, // 0000 items: { plugin_a: Plugins.valid('pluginA'), @@ -241,10 +233,15 @@ describe('plugins discovery system', () => { ) .toPromise(); - const srcPluginsPath = resolve(OPENSEARCH_DASHBOARDS_ROOT, 'src', 'plugins'); + const srcPluginsPath = resolve(PROCESS_WORKING_DIR, 'src', 'plugins'); expect(errors).toEqual( expect.arrayContaining([ - `Error: EACCES, permission denied '${EXTENDED_PATH_PREFIX}${srcPluginsPath}' (invalid-search-path, ${srcPluginsPath})`, + `Error: EACCES, permission denied '${standardize( + srcPluginsPath, + false, + false, + true + )}' (invalid-search-path, ${srcPluginsPath})`, ]) ); }); @@ -258,7 +255,7 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: { + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: { ...Plugins.inaccessibleManifest(), nested_plugin: Plugins.valid('nestedPlugin'), }, @@ -279,7 +276,12 @@ describe('plugins discovery system', () => { const errorPath = manifestPath('plugin_a'); expect(errors).toEqual( expect.arrayContaining([ - `Error: EACCES, permission denied '${EXTENDED_PATH_PREFIX}${errorPath}' (missing-manifest, ${errorPath})`, + `Error: EACCES, permission denied '${standardize( + errorPath, + false, + false, + true + )}' (missing-manifest, ${errorPath})`, ]) ); }); @@ -293,10 +295,10 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(), + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(), }, { createCwd: false } ); @@ -330,7 +332,7 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/plugin_a`]: { + [`${PROCESS_WORKING_DIR}/src/plugins/plugin_a`]: { ...Plugins.valid('pluginA'), nested_plugin: Plugins.valid('nestedPlugin'), }, @@ -349,18 +351,14 @@ describe('plugins discovery system', () => { mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/plugin`]: Plugins.valid('plugin1'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/plugin`]: Plugins.valid('plugin2'), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/plugin`]: Plugins.valid( - 'plugin3' - ), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/sub4/plugin`]: Plugins.valid( - 'plugin4' - ), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/sub4/sub5/plugin`]: Plugins.valid( + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/plugin`]: Plugins.valid('plugin1'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/plugin`]: Plugins.valid('plugin2'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/plugin`]: Plugins.valid('plugin3'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/sub4/plugin`]: Plugins.valid('plugin4'), + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/sub4/sub5/plugin`]: Plugins.valid( 'plugin5' ), - [`${OPENSEARCH_DASHBOARDS_ROOT}/src/plugins/sub1/sub2/sub3/sub4/sub5/sub6/plugin`]: Plugins.valid( + [`${PROCESS_WORKING_DIR}/src/plugins/sub1/sub2/sub3/sub4/sub5/sub6/plugin`]: Plugins.valid( 'plugin6' ), }, @@ -379,11 +377,11 @@ describe('plugins discovery system', () => { it('works with symlinks', async () => { const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext, instanceInfo); - const pluginFolder = resolve(OPENSEARCH_DASHBOARDS_ROOT, '..', 'ext-plugins'); + const pluginFolder = resolve(PROCESS_WORKING_DIR, '..', 'ext-plugins'); mockFs( { - [`${OPENSEARCH_DASHBOARDS_ROOT}/plugins`]: mockFs.symlink({ + [`${PROCESS_WORKING_DIR}/plugins`]: mockFs.symlink({ path: '../ext-plugins', }), [pluginFolder]: { diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index cff5ae79b91..b020e56539a 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,7 +33,8 @@ import { mockDiscover, mockPackage } from './plugins_service.test.mocks'; import { resolve, posix } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@osd/config-schema'; -import { createAbsolutePathSerializer, REPO_ROOT } from '@osd/dev-utils'; +import { getRepoRoot } from '@osd/cross-platform'; +import { createAbsolutePathSerializer } from '@osd/dev-utils'; import { ConfigPath, ConfigService, Env } from '../config'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; @@ -127,7 +128,10 @@ describe('PluginsService', () => { }; coreId = Symbol('core'); - env = Env.createDefault(REPO_ROOT, getEnvOptions()); + /* Using getRepoRoot to get the appropriate one between REPO_ROOT and REPO_ROOT_8_3; this + * is only a problem on Windows. + */ + env = Env.createDefault(getRepoRoot(resolve('.'))!, getEnvOptions()); config$ = new BehaviorSubject>({ plugins: { initialize: true } }); const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 2860de52976..4bacfda3bd5 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -141,9 +141,7 @@ describe('IndexMigrator', () => { }, }); - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow( - /use the X-Pack upgrade assistant/ - ); + await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow(); }); test('fails if root doc type is not "doc"', async () => { @@ -164,9 +162,7 @@ describe('IndexMigrator', () => { }, }); - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow( - /use the X-Pack upgrade assistant/ - ); + await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow(); }); test('retains unknown core field mappings from the previous index', async () => { diff --git a/src/core/server/saved_objects/migrations/core/opensearch_index.ts b/src/core/server/saved_objects/migrations/core/opensearch_index.ts index 0fc03ccd05c..7df5c3881cd 100644 --- a/src/core/server/saved_objects/migrations/core/opensearch_index.ts +++ b/src/core/server/saved_objects/migrations/core/opensearch_index.ts @@ -311,7 +311,7 @@ function assertIsSupportedIndex(indexInfo: FullIndexInfo) { if (!isV7Index) { throw new Error( `Index ${indexInfo.indexName} belongs to a version of OpenSearch Dashboards ` + - `that cannot be automatically migrated. Reset it or use the X-Pack upgrade assistant.` + `that cannot be automatically migrated. Reset it.` ); } diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 72461653d65..a7aa95ebab9 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -624,6 +624,29 @@ describe('ui settings', () => { expect(await uiSettings.get('dateFormat')).toBe('foo'); }); + it('returns the overridden value for key theme:version', async () => { + const opensearchDocSource = { 'theme:version': 'v8 (beta)' }; + const overrides = { 'theme:version': 'v7' }; + const { uiSettings } = setup({ opensearchDocSource, overrides }); + + expect(await uiSettings.get('theme:version')).toBe('v7'); + }); + + it('returns the overridden value for key theme:version when doc source is empty', async () => { + const opensearchDocSource = {}; + const overrides = { 'theme:version': 'v7' }; + const { uiSettings } = setup({ opensearchDocSource, overrides }); + + expect(await uiSettings.get('theme:version')).toBe('v7'); + }); + + it('rewrites the key theme:version value without override', async () => { + const opensearchDocSource = { 'theme:version': 'v8 (beta)' }; + const { uiSettings } = setup({ opensearchDocSource }); + + expect(await uiSettings.get('theme:version')).toBe('v8 (beta)'); + }); + it('returns the default value for an override with value null', async () => { const opensearchDocSource = { dateFormat: 'YYYY-MM-DD' }; const overrides = { dateFormat: null }; diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts index 4c3dd0e4dcf..634fb5c62d5 100644 --- a/src/core/server/ui_settings/ui_settings_config.ts +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -38,7 +38,12 @@ const deprecations: ConfigDeprecationProvider = ({ unused, renameFromRoot }) => ]; const configSchema = schema.object({ - overrides: schema.object({}, { unknowns: 'allow' }), + overrides: schema.object( + { + 'theme:version': schema.string({ defaultValue: 'v7' }), + }, + { unknowns: 'allow' } + ), }); export type UiSettingsConfigType = TypeOf; diff --git a/src/dev/build/lib/config.test.ts b/src/dev/build/lib/config.test.ts index 8005f2d952a..145954c1fb4 100644 --- a/src/dev/build/lib/config.test.ts +++ b/src/dev/build/lib/config.test.ts @@ -30,8 +30,7 @@ import { resolve } from 'path'; -import { standardize } from '@osd/cross-platform'; -import { REPO_ROOT } from '@osd/utils'; +import { standardize, REPO_ROOT } from '@osd/cross-platform'; import { createAbsolutePathSerializer } from '@osd/dev-utils'; import pkg from '../../../../package.json'; @@ -80,10 +79,10 @@ describe('#getOpenSearchDashboardsPkg()', () => { }); }); -describe('#getNodeVersion()', () => { - it('returns the node version from the OpenSearch Dashboards package.json', async () => { +describe('#getNodeRange()', () => { + it('returns the node version range from the OpenSearch Dashboards package.json', async () => { const config = await setup(); - expect(config.getNodeVersion()).toEqual(pkg.engines.node); + expect(config.getNodeRange()).toEqual(pkg.engines.node); }); }); diff --git a/src/dev/build/lib/config.ts b/src/dev/build/lib/config.ts index 4f47a5ec8f5..03fdd14b05c 100644 --- a/src/dev/build/lib/config.ts +++ b/src/dev/build/lib/config.ts @@ -86,7 +86,7 @@ export class Config { private readonly targetAllPlatforms: boolean, private readonly targetPlatforms: TargetPlatforms, private readonly pkg: Package, - private readonly nodeVersion: string, + private readonly nodeRange: string, private readonly repoRoot: string, private readonly versionInfo: VersionInfo, public readonly isRelease: boolean @@ -102,8 +102,8 @@ export class Config { /** * Get the node version required by OpenSearch Dashboards */ - getNodeVersion() { - return this.nodeVersion; + getNodeRange() { + return this.nodeRange; } /** diff --git a/src/dev/build/lib/integration_tests/fs.test.ts b/src/dev/build/lib/integration_tests/fs.test.ts index 0cf777cce66..1ad9afa54ef 100644 --- a/src/dev/build/lib/integration_tests/fs.test.ts +++ b/src/dev/build/lib/integration_tests/fs.test.ts @@ -34,6 +34,7 @@ import { chmodSync, statSync } from 'fs'; import del from 'del'; import { mkdirp, write, read, getChildPaths, copyAll, getFileHash, untar, gunzip } from '../fs'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const TMP = resolve(__dirname, '../__tmp__'); const FIXTURES = resolve(__dirname, '../__fixtures__'); @@ -61,13 +62,13 @@ beforeAll(async () => { // clean and recreate TMP directory beforeEach(async () => { - await del(TMP); + await del(TMP, { cwd: PROCESS_WORKING_DIR }); await mkdirp(TMP); }); // cleanup TMP directory afterAll(async () => { - await del(TMP); + await del(TMP, { cwd: PROCESS_WORKING_DIR }); }); describe('mkdirp()', () => { diff --git a/src/dev/build/lib/integration_tests/scan_copy.test.ts b/src/dev/build/lib/integration_tests/scan_copy.test.ts index 9ed5aaa1d48..bf010ef8a13 100644 --- a/src/dev/build/lib/integration_tests/scan_copy.test.ts +++ b/src/dev/build/lib/integration_tests/scan_copy.test.ts @@ -35,6 +35,7 @@ import del from 'del'; import { getChildPaths } from '../fs'; import { scanCopy } from '../scan_copy'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const IS_WINDOWS = process.platform === 'win32'; const FIXTURES = resolve(__dirname, '../__fixtures__'); @@ -50,7 +51,7 @@ beforeAll(async () => { // cleanup TMP directory afterEach(async () => { - await del(TMP); + await del(TMP, { cwd: PROCESS_WORKING_DIR }); }); it('rejects if source path is not absolute', async () => { diff --git a/src/dev/build/lib/scan_copy.ts b/src/dev/build/lib/scan_copy.ts index f467c0fca25..2bc63a654c0 100644 --- a/src/dev/build/lib/scan_copy.ts +++ b/src/dev/build/lib/scan_copy.ts @@ -107,13 +107,13 @@ export async function scanCopy(options: Options) { await copyFileAsync(record.absolute, record.absoluteDest, Fs.constants.COPYFILE_EXCL); } - if (time) { - await utimesAsync(record.absoluteDest, time, time); - } - if (record.isDirectory) { await copyChildren(record); } + + if (time) { + await utimesAsync(record.absoluteDest, time, time); + } }; await mkdirp(destination); diff --git a/src/dev/build/lib/scan_delete.test.ts b/src/dev/build/lib/scan_delete.test.ts index 2f8f5473ad0..900b5b52af3 100644 --- a/src/dev/build/lib/scan_delete.test.ts +++ b/src/dev/build/lib/scan_delete.test.ts @@ -36,12 +36,16 @@ import del from 'del'; // @ts-ignore import { mkdirp, write } from './fs'; import { scanDelete } from './scan_delete'; +import { PROCESS_WORKING_DIR, getRepoRoot } from '@osd/cross-platform'; -const TMP = resolve(__dirname, '__tests__/__tmp__'); +const rootPath = getRepoRoot(__dirname); +const currentDir = rootPath ? resolve('.', relative(rootPath, __dirname)) : __dirname; + +const TMP = resolve(currentDir, '__tests__/__tmp__'); // clean and recreate TMP directory beforeEach(async () => { - await del(TMP); + await del(TMP, { cwd: currentDir }); await mkdirp(resolve(TMP, 'foo/bar/baz')); await mkdirp(resolve(TMP, 'foo/bar/box')); await mkdirp(resolve(TMP, 'a/b/c/d/e')); @@ -50,13 +54,13 @@ beforeEach(async () => { // cleanup TMP directory afterAll(async () => { - await del(TMP); + await del(TMP, { cwd: currentDir }); }); it('requires absolute paths', async () => { await expect( scanDelete({ - directory: relative(process.cwd(), TMP), + directory: relative(PROCESS_WORKING_DIR, TMP), regularExpressions: [], }) ).rejects.toMatchInlineSnapshot( diff --git a/src/dev/build/tasks/create_archives_sources_task.ts b/src/dev/build/tasks/create_archives_sources_task.ts index 0409c406708..bf53c291556 100644 --- a/src/dev/build/tasks/create_archives_sources_task.ts +++ b/src/dev/build/tasks/create_archives_sources_task.ts @@ -50,7 +50,7 @@ export const CreateArchivesSources: Task = { // copy node.js install await scanCopy({ - source: getNodeDownloadInfo(config, platform).extractDir, + source: (await getNodeDownloadInfo(config, platform)).extractDir, destination: build.resolvePathForPlatform(platform, 'node'), }); diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.ts index 91331184ce7..9771e92e209 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.ts @@ -30,16 +30,17 @@ import { download, GlobalTask } from '../../lib'; import { getNodeShasums } from './node_shasums'; -import { getNodeDownloadInfo } from './node_download_info'; +import { getLatestNodeVersion, getNodeDownloadInfo } from './node_download_info'; export const DownloadNodeBuilds: GlobalTask = { global: true, description: 'Downloading node.js builds for all platforms', async run(config, log) { - const shasums = await getNodeShasums(log, config.getNodeVersion()); + const latestNodeVersion = await getLatestNodeVersion(config); + const shasums = await getNodeShasums(log, latestNodeVersion); await Promise.all( config.getTargetPlatforms().map(async (platform) => { - const { url, downloadPath, downloadName } = getNodeDownloadInfo(config, platform); + const { url, downloadPath, downloadName } = await getNodeDownloadInfo(config, platform); await download({ log, url, diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts index 6b3a066a1d2..02a6a7bfa72 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts @@ -28,10 +28,6 @@ * under the License. */ -import { readFileSync } from 'fs'; -import Path from 'path'; - -import { REPO_ROOT } from '@osd/utils'; import { ToolingLog, ToolingLogCollectingWriter, @@ -41,6 +37,7 @@ import { import { Config } from '../../lib'; import { ExtractNodeBuilds } from './extract_node_builds_task'; +import { getLatestNodeVersion } from './node_download_info'; jest.mock('../../lib/fs'); jest.mock('../../lib/get_build_number'); @@ -53,14 +50,6 @@ log.setWriters([testWriter]); expect.addSnapshotSerializer(createAbsolutePathSerializer()); -const nodeVersion = readFileSync(Path.resolve(REPO_ROOT, '.node-version'), 'utf8').trim(); -expect.addSnapshotSerializer( - createRecursiveSerializer( - (s) => typeof s === 'string' && s.includes(nodeVersion), - (s) => s.split(nodeVersion).join('') - ) -); - async function setup() { const config = await Config.create({ isRelease: true, @@ -73,6 +62,16 @@ async function setup() { }, }); + const realNodeVersion = await getLatestNodeVersion(config); + if (realNodeVersion) { + expect.addSnapshotSerializer( + createRecursiveSerializer( + (s) => typeof s === 'string' && s.includes(realNodeVersion), + (s) => s.split(realNodeVersion).join('') + ) + ); + } + return { config }; } diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.ts b/src/dev/build/tasks/nodejs/extract_node_builds_task.ts index 8252ce2153f..28b2ebe24d4 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.ts @@ -37,7 +37,7 @@ export const ExtractNodeBuilds: GlobalTask = { async run(config) { await Promise.all( config.getTargetPlatforms().map(async (platform) => { - const { downloadPath, extractDir } = getNodeDownloadInfo(config, platform); + const { downloadPath, extractDir } = await getNodeDownloadInfo(config, platform); if (platform.isWindows()) { await unzip(downloadPath, extractDir, { strip: 1 }); } else { diff --git a/src/dev/build/tasks/nodejs/node_download_info.ts b/src/dev/build/tasks/nodejs/node_download_info.ts index 86e0c680ab0..73163bc3c30 100644 --- a/src/dev/build/tasks/nodejs/node_download_info.ts +++ b/src/dev/build/tasks/nodejs/node_download_info.ts @@ -29,18 +29,22 @@ */ import { basename } from 'path'; +import fetch from 'node-fetch'; +import semver from 'semver'; import { Config, Platform } from '../../lib'; -export function getNodeDownloadInfo(config: Config, platform: Platform) { - const version = config.getNodeVersion(); +const NODE_RANGE_CACHE: { [key: string]: string } = {}; + +export async function getNodeDownloadInfo(config: Config, platform: Platform) { + const version = await getLatestNodeVersion(config); const arch = platform.getNodeArch(); const downloadName = platform.isWindows() ? `node-v${version}-win-x64.zip` : `node-v${version}-${arch}.tar.gz`; - const url = `https://nodejs.org/dist/v${version}/${downloadName}`; + const url = `https://mirrors.nodejs.org/dist/v${version}/${downloadName}`; const downloadPath = config.resolveFromRepo('.node_binaries', version, basename(downloadName)); const extractDir = config.resolveFromRepo('.node_binaries', version, arch); @@ -52,3 +56,23 @@ export function getNodeDownloadInfo(config: Config, platform: Platform) { version, }; } + +export async function getLatestNodeVersion(config: Config) { + const range = config.getNodeRange(); + // Check cache and return if known + if (NODE_RANGE_CACHE[range]) return NODE_RANGE_CACHE[range]; + + const releaseDoc = await fetch('https://nodejs.org/dist/index.json'); + const releaseList: [{ version: string }] = await releaseDoc.json(); + const releases = releaseList.map(({ version }) => version.replace(/^v/, '')); + const maxVersion = semver.maxSatisfying(releases, range); + + if (!maxVersion) { + throw new Error(`Cannot find a version of Node.js that satisfies ${range}.`); + } + + // Cache it + NODE_RANGE_CACHE[range] = maxVersion; + + return maxVersion; +} diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index ccb75897ccf..4724fa73e9f 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -28,15 +28,10 @@ * under the License. */ -import Path from 'path'; -import Fs from 'fs'; - -import { REPO_ROOT } from '@osd/utils'; import { ToolingLog, ToolingLogCollectingWriter, createAnyInstanceSerializer, - createRecursiveSerializer, } from '@osd/dev-utils'; import { Config, Platform } from '../../lib'; @@ -48,7 +43,7 @@ jest.mock('../../lib/fs'); jest.mock('../../lib/get_build_number'); const { getNodeShasums } = jest.requireMock('./node_shasums'); -const { getNodeDownloadInfo } = jest.requireMock('./node_download_info'); +const { getNodeDownloadInfo, getLatestNodeVersion } = jest.requireMock('./node_download_info'); const { getFileHash } = jest.requireMock('../../lib/fs'); const log = new ToolingLog(); @@ -58,14 +53,6 @@ log.setWriters([testWriter]); expect.addSnapshotSerializer(createAnyInstanceSerializer(Config)); expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); -const nodeVersion = Fs.readFileSync(Path.resolve(REPO_ROOT, '.node-version'), 'utf8').trim(); -expect.addSnapshotSerializer( - createRecursiveSerializer( - (s) => typeof s === 'string' && s.includes(nodeVersion), - (s) => s.split(nodeVersion).join('') - ) -); - async function setup(actualShaSums?: Record) { const config = await Config.create({ isRelease: true, @@ -74,6 +61,7 @@ async function setup(actualShaSums?: Record) { linux: false, linuxArm: false, darwin: false, + windows: false, }, }); @@ -89,9 +77,12 @@ async function setup(actualShaSums?: Record) { return { downloadPath: `${platform.getName()}:${platform.getNodeArch()}:downloadPath`, downloadName: `${platform.getName()}:${platform.getNodeArch()}:downloadName`, + version: '', }; }); + getLatestNodeVersion.mockReturnValue(''); + getFileHash.mockImplementation((downloadPath: string) => { if (actualShaSums?.[downloadPath]) { return actualShaSums[downloadPath]; @@ -176,6 +167,7 @@ it('checks shasums for each downloaded node build', async () => { "value": Object { "downloadName": "linux:linux-x64:downloadName", "downloadPath": "linux:linux-x64:downloadPath", + "version": "", }, }, Object { @@ -183,6 +175,7 @@ it('checks shasums for each downloaded node build', async () => { "value": Object { "downloadName": "linux:linux-arm64:downloadName", "downloadPath": "linux:linux-arm64:downloadPath", + "version": "", }, }, Object { @@ -190,6 +183,7 @@ it('checks shasums for each downloaded node build', async () => { "value": Object { "downloadName": "darwin:darwin-x64:downloadName", "downloadPath": "darwin:darwin-x64:downloadPath", + "version": "", }, }, Object { @@ -197,6 +191,7 @@ it('checks shasums for each downloaded node build', async () => { "value": Object { "downloadName": "win32:win32-x64:downloadName", "downloadPath": "win32:win32-x64:downloadPath", + "version": "", }, }, ], diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts index 665774bf5bc..4c64c4bbd25 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.ts @@ -29,18 +29,19 @@ */ import { getFileHash, GlobalTask } from '../../lib'; -import { getNodeDownloadInfo } from './node_download_info'; +import { getNodeDownloadInfo, getLatestNodeVersion } from './node_download_info'; import { getNodeShasums } from './node_shasums'; export const VerifyExistingNodeBuilds: GlobalTask = { global: true, description: 'Verifying previously downloaded node.js build for all platforms', async run(config, log) { - const shasums = await getNodeShasums(log, config.getNodeVersion()); + const latestNodeVersion = await getLatestNodeVersion(config); + const shasums = await getNodeShasums(log, latestNodeVersion); await Promise.all( config.getTargetPlatforms().map(async (platform) => { - const { downloadPath, downloadName } = getNodeDownloadInfo(config, platform); + const { downloadPath, downloadName } = await getNodeDownloadInfo(config, platform); const sha256 = await getFileHash(downloadPath, 'sha256'); if (sha256 !== shasums[downloadName]) { diff --git a/src/dev/build/tasks/notice_file_task.ts b/src/dev/build/tasks/notice_file_task.ts index 869829644dd..08792b653f4 100644 --- a/src/dev/build/tasks/notice_file_task.ts +++ b/src/dev/build/tasks/notice_file_task.ts @@ -42,7 +42,7 @@ export const CreateNoticeFile: Task = { log.info('Generating notice from source'); log.indent(4); const noticeFromSource = await generateNoticeFromSource({ - productName: 'OpenSearch', + productName: 'OpenSearch (https://opensearch.org/)', directory: build.resolvePath(), log, }); @@ -57,7 +57,7 @@ export const CreateNoticeFile: Task = { log.info('Generating build notice'); - const { extractDir: nodeDir, version: nodeVersion } = getNodeDownloadInfo( + const { extractDir: nodeDir, version: nodeVersion } = await getNodeDownloadInfo( config, config.hasSpecifiedPlatform() ? config.getPlatform( diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index dc05bf8e091..de9ec4e4b5d 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -50,6 +50,7 @@ opensearch_dashboards_vars=( opensearch.ssl.truststore.password opensearch.ssl.verificationMode opensearch.username + opensearch.disablePrototypePoisoningProtection i18n.locale interpreter.enableInVisualize opensearchDashboards.autocompleteTerminateAfter @@ -149,7 +150,7 @@ opensearch_dashboards_vars=( telemetry.optIn telemetry.optInStatusUrl telemetry.sendUsageFrom - wizard.enabled + vis_builder.enabled data_source.enabled data_source.encryption.wrappingKeyName data_source.encryption.wrappingKeyNamespace diff --git a/src/dev/build/tasks/patch_native_modules_task.test.ts b/src/dev/build/tasks/patch_native_modules_task.test.ts index 25bb49e06ee..f3a3daa432c 100644 --- a/src/dev/build/tasks/patch_native_modules_task.test.ts +++ b/src/dev/build/tasks/patch_native_modules_task.test.ts @@ -67,8 +67,8 @@ it('patch native modules task downloads the correct platform package', async () "destination": /.native_modules/re2/linux-arm64-83.tar.gz, "log": , "retries": 3, - "sha256": "f25124adc64d269a513b99abd4a5eed8d7a929db565207f8ece1f3b7b7931668", - "url": "https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.15.4/linux-arm64-83.tar.gz", + "sha256": "d86ced75b794fbf518b90908847b3c09a50f3ff5a2815aa30f53080f926a2873", + "url": "https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.17.4/linux-arm64-83.tar.gz", }, ], ] diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index e234251a671..3bd9fa63c35 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -52,31 +52,41 @@ interface Package { >; } +/* Process for updating URLs and checksums after bumping the version of `re2` or NodeJS: + * 1. Match the `version` with the version in the yarn.lock file. + * 2. Match the module version, the digits at the end of the filename, with the output of + * `node -p process.versions.modules`. + * 3. Confirm that the URLs exist for each platform-architecture combo on + * https://github.com/uhop/node-re2/releases/tag/[VERSION]; reach out to maintainers for ARM + * releases of `re2` as they currently don't have an official ARM release. + * 4. Generate new checksums for each artifact by downloading each one and calling + * `shasum -a 256` or `sha256sum` on the downloaded file. + */ const packages: Package[] = [ { name: 're2', - version: '1.15.4', + version: '1.17.4', destinationPath: 'node_modules/re2/build/Release/re2.node', extractMethod: 'gunzip', archives: { 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/darwin-x64-83.gz', - sha256: 'b45cd8296fd6eb2a091399c20111af43093ba30c99ed9e5d969278f5ff69ba8f', + url: 'https://github.com/uhop/node-re2/releases/download/1.17.4/darwin-x64-83.gz', + sha256: '9112ed93c1544ecc6397f7ff20bd2b28f3b04c7fbb54024e10f9a376a132a87d', }, 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/linux-x64-83.gz', - sha256: '1bbc3f90f0ba105772b37c04e3a718f69544b4df01dda00435c2b8e50b2ad0d9', + url: 'https://github.com/uhop/node-re2/releases/download/1.17.4/linux-x64-83.gz', + sha256: '86e03540783a18c41f81df0aec320b1f64aca6cbd3a87fc1b7a9b4109c5f5986', }, 'linux-arm64': { url: - 'https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.15.4/linux-arm64-83.tar.gz', - sha256: 'f25124adc64d269a513b99abd4a5eed8d7a929db565207f8ece1f3b7b7931668', + 'https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.17.4/linux-arm64-83.tar.gz', + sha256: 'd86ced75b794fbf518b90908847b3c09a50f3ff5a2815aa30f53080f926a2873', overriddenExtractMethod: 'untar', overriddenDestinationPath: 'node_modules/re2/build/Release', }, 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/win32-x64-83.gz', - sha256: 'efe939d3cda1d64ee3ee3e60a20613b95166d55632e702c670763ea7e69fca06', + url: 'https://github.com/uhop/node-re2/releases/download/1.17.4/win32-x64-83.gz', + sha256: '2f842d9757288afd4bd5dec0e7b370a4c3e89ac98050598b17abb9e8e00e3294', }, }, }, diff --git a/src/dev/build/tasks/verify_env_task.ts b/src/dev/build/tasks/verify_env_task.ts index a6b3bd88557..ceef5c8d38c 100644 --- a/src/dev/build/tasks/verify_env_task.ts +++ b/src/dev/build/tasks/verify_env_task.ts @@ -28,6 +28,7 @@ * under the License. */ +import semver from 'semver'; import { GlobalTask } from '../lib'; export const VerifyEnv: GlobalTask = { @@ -35,10 +36,12 @@ export const VerifyEnv: GlobalTask = { description: 'Verifying environment meets requirements', async run(config, log) { - const version = `v${config.getNodeVersion()}`; + const range = config.getNodeRange(); - if (version !== process.version) { - throw new Error(`Invalid nodejs version, please use ${version}`); + if (!semver.satisfies(process.version, range)) { + throw new Error( + `Invalid Node.js version (${process.version}); please use a version that satisfies ${range}.` + ); } log.success('Node.js version verified'); diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index e33de19284a..632f2dd30ac 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -126,14 +126,13 @@ yarn config set yarn-offline-mirror "$cacheDir/yarn-offline-cache" yarnGlobalDir="$(yarn global bin)" export PATH="$PATH:$yarnGlobalDir" +# TODO: Find out if these are OSD's or if this entire file should be removed # use a proxy to fetch chromedriver/geckodriver asset export GECKODRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CHROMEDRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export RE2_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" -export CHECKS_REPORTER_ACTIVE=false - # This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" @@ -143,26 +142,6 @@ else echo "Chrome not detected, installing default chromedriver binary for the package version" fi -### only run on pr jobs for opensearch-project/OpenSearch-Dashboards, checks-reporter doesn't work for other repos -if [[ "$ghprbPullId" && "$ghprbGhRepository" == 'opensearch-project/OpenSearch-Dashboards' ]] ; then - export CHECKS_REPORTER_ACTIVE=true -fi - -### -### Implements github-checks-reporter kill switch when scripts are called from the command line -### $@ - all arguments -### -function checks-reporter-with-killswitch() { - if [ "$CHECKS_REPORTER_ACTIVE" == "true" ] ; then - yarn run github-checks-reporter "$@" - else - arguments=("$@"); - "${arguments[@]:1}"; - fi -} - -export -f checks-reporter-with-killswitch - source "$OPENSEARCH_DASHBOARDS_DIR/src/dev/ci_setup/load_env_keys.sh" OPENSEARCH_DIR="$WORKSPACE/opensearch" diff --git a/src/dev/file.ts b/src/dev/file.ts index 7d959387995..48c62234a27 100644 --- a/src/dev/file.ts +++ b/src/dev/file.ts @@ -29,6 +29,7 @@ */ import { dirname, extname, join, relative, resolve, sep, basename } from 'path'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; export class File { private path: string; @@ -37,7 +38,7 @@ export class File { constructor(path: string) { this.path = resolve(path); - this.relativePath = relative(process.cwd(), this.path); + this.relativePath = relative(PROCESS_WORKING_DIR, this.path); this.ext = extname(this.path); } diff --git a/src/dev/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts index 8bec5b7e740..f11419868fa 100644 --- a/src/dev/i18n/integrate_locale_files.test.ts +++ b/src/dev/i18n/integrate_locale_files.test.ts @@ -30,11 +30,12 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.test.mocks'; -import path from 'path'; +import { resolve } from 'path'; import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files'; -import { normalizePath } from './utils'; +import { relativeToRepoRoot, standardize } from '@osd/cross-platform'; -const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); +const currentDir = relativeToRepoRoot(__dirname); +const localePath = resolve(currentDir, '__fixtures__', 'integrate_locale_files', 'fr.json'); const mockDefaultMessagesMap = new Map([ ['plugin-1.message-id-1', { message: 'Message text 1' }], @@ -180,9 +181,12 @@ Map { const [[path1, json1], [path2, json2]] = mockWriteFileAsync.mock.calls; const [[dirPath1], [dirPath2]] = mockMakeDirAsync.mock.calls; - expect([normalizePath(path1), json1]).toMatchSnapshot(); - expect([normalizePath(path2), json2]).toMatchSnapshot(); - expect([normalizePath(dirPath1), normalizePath(dirPath2)]).toMatchSnapshot(); + expect([standardize(relativeToRepoRoot(path1)), json1]).toMatchSnapshot(); + expect([standardize(relativeToRepoRoot(path2)), json2]).toMatchSnapshot(); + expect([ + standardize(relativeToRepoRoot(dirPath1)), + standardize(relativeToRepoRoot(dirPath2)), + ]).toMatchSnapshot(); }); }); }); diff --git a/src/dev/i18n/utils/utils.js b/src/dev/i18n/utils/utils.js index 370a49e466f..79868ddd631 100644 --- a/src/dev/i18n/utils/utils.js +++ b/src/dev/i18n/utils/utils.js @@ -48,6 +48,7 @@ import chalk from 'chalk'; import parser from 'intl-messageformat-parser'; import { createFailError } from '@osd/dev-utils'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const ESCAPE_LINE_BREAK_REGEX = /(?/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', + '@opensearch-project/opensearch/aws': + '/node_modules/@opensearch-project/opensearch/lib/aws', '^src/plugins/(.*)': '/src/plugins/$1', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', @@ -75,7 +77,12 @@ export default { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], coverageReporters: ['lcov', 'text-summary'], moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], - modulePathIgnorePatterns: ['__fixtures__/', 'target/', '/src/plugins/maps_legacy'], + modulePathIgnorePatterns: [ + '__fixtures__/', + 'target/', + '/src/plugins/maps_legacy', + '/src/cli_plugin/list/.test.data.list', + ], testEnvironment: 'jest-environment-jsdom', testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ @@ -104,4 +111,5 @@ export default { globals: { Uint8Array: Uint8Array, }, + flakyTestRetries: 2, }; diff --git a/src/dev/jest/integration_tests/junit_reporter.test.js b/src/dev/jest/integration_tests/junit_reporter.test.js index e3f46c40eb1..9ae4e660d21 100644 --- a/src/dev/jest/integration_tests/junit_reporter.test.js +++ b/src/dev/jest/integration_tests/junit_reporter.test.js @@ -36,6 +36,7 @@ import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; import { getUniqueJunitReportPath } from '@osd/test'; +import { PROCESS_WORKING_DIR } from '@osd/cross-platform'; const MINUTE = 1000 * 60; const ROOT_DIR = resolve(__dirname, '../../../../'); @@ -44,7 +45,7 @@ const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); const XML_PATH = getUniqueJunitReportPath(FIXTURE_DIR, 'Jest Tests'); afterAll(async () => { - await del(TARGET_DIR); + await del(TARGET_DIR, { cwd: PROCESS_WORKING_DIR }); }); const parseXml = promisify(xml2js.parseString); diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 35b8a5a3960..65f244042a1 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -70,8 +70,9 @@ export default class JestJUnitReporter { { skipNullAttributes: true } ); - const msToIso = (ms) => (ms ? new Date(ms).toISOString().slice(0, -5) : undefined); - const msToSec = (ms) => (ms ? (ms / 1000).toFixed(3) : undefined); + const isNumeric = (val) => !isNaN(parseFloat(val)) && isFinite(val); + const msToIso = (ms) => (isNumeric(ms) ? new Date(ms).toISOString().slice(0, -5) : undefined); + const msToSec = (ms) => (isNumeric(ms) ? (ms / 1000).toFixed(3) : undefined); root.att({ name: 'jest', diff --git a/src/dev/node_versions_must_match.test.ts b/src/dev/node_versions_must_match.test.ts index 7ddaa54b121..c9755707207 100644 --- a/src/dev/node_versions_must_match.test.ts +++ b/src/dev/node_versions_must_match.test.ts @@ -29,11 +29,13 @@ */ import fs from 'fs'; +import semver from 'semver'; import { engines } from '../../package.json'; import { promisify } from 'util'; const readFile = promisify(fs.readFile); import expect from '@osd/expect'; +// ToDo: `.node-version` seems to exist for no good reason; find out if we can get rid of it and this test. describe('All configs should use a single version of Node', () => { it('should compare .node-version and .nvmrc', async () => { const [nodeVersion, nvmrc] = await Promise.all([ @@ -48,6 +50,6 @@ describe('All configs should use a single version of Node', () => { const nodeVersion = await readFile('./.node-version', { encoding: 'utf-8', }); - expect(nodeVersion.trim()).to.be(engines.node); + expect(semver.satisfies(nodeVersion.trim(), engines.node)).to.be(true); }); }); diff --git a/src/dev/notice/cli.js b/src/dev/notice/cli.js index f176d53c8fb..62957b33107 100644 --- a/src/dev/notice/cli.js +++ b/src/dev/notice/cli.js @@ -80,7 +80,7 @@ if (opts.help) { (async function run() { const path = resolve(REPO_ROOT, 'NOTICE.txt'); const newContent = await generateNoticeFromSource({ - productName: 'OpenSearch Dashboards source code with OpenSearch Dashboards X-Pack source code', + productName: 'OpenSearch (https://opensearch.org/)', directory: REPO_ROOT, log, }); diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 48dc147bfbe..0bfc2a978e0 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -33,6 +33,17 @@ import { ToolingLog } from '@osd/dev-utils'; const NOTICE_COMMENT_RE = /\/\*[\s\n\*]*@notice([\w\W]+?)\*\//g; const NEWLINE_RE = /\r?\n/g; +const NOTICE_TEXT = `Copyright OpenSearch Contributors + +This product includes software, including Kibana source code, +developed by Elasticsearch (http://www.elastic.co). +Copyright 2009-2021 Elasticsearch B.V. + +This product includes software developed by The Apache Software +Foundation (http://www.apache.org/) + +This product includes software developed by +Joda.org (http://www.joda.org/).`; interface Options { /** @@ -89,17 +100,10 @@ export async function generateNoticeFromSource({ productName, directory, log }: .on('end', resolve); }); - let noticeText = ''; - noticeText += `${productName}\n`; - noticeText += `Copyright ${new Date().getUTCFullYear()} OpenSearch Contributors\n\n`; - noticeText += `This product includes software developed by Elasticsearch (http://www.elastic.co).\n`; - noticeText += `Copyright 2009-2018 Elasticsearch\n\n`; - noticeText += `This product includes software developed by The Apache Software Foundation (http://www.apache.org/)\n\n`; - noticeText += `This product includes software developed by Joda.org (http://www.joda.org/).\n`; - + let notice = `${productName}\n` + NOTICE_TEXT; for (const comment of noticeComments.sort()) { - noticeText += '\n---\n'; - noticeText += comment + notice += '\n---\n'; + notice += comment .split(NEWLINE_RE) .map((line) => line @@ -110,11 +114,9 @@ export async function generateNoticeFromSource({ productName, directory, log }: ) .join('\n') .trim(); - noticeText += '\n'; + notice += '\n'; } - - noticeText += '\n'; - - log.debug(`notice text:\n\n${noticeText}`); - return noticeText; + notice += '\n'; + log.debug(`notice text:\n\n${notice}`); + return notice; } diff --git a/src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts b/src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts index 3b95625e706..fdfaa301b34 100644 --- a/src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts +++ b/src/plugins/apm_oss/server/tutorial/instructions/apm_server_instructions.ts @@ -36,7 +36,7 @@ export const createEditConfig = () => ({ }), textPre: i18n.translate('apmOss.tutorial.editConfig.textPre', { defaultMessage: - "If you're using an X-Pack secured version of Elastic Stack, you must specify \ + "If you're using a secured version of OpenSearch, you must specify \ credentials in the `apm-server.yml` config file.", }), commands: [ diff --git a/src/plugins/charts/README.md b/src/plugins/charts/README.md index f7ff405c74f..1afd049ea28 100644 --- a/src/plugins/charts/README.md +++ b/src/plugins/charts/README.md @@ -1,29 +1,88 @@ # Charts -The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all OpenSearch Dashboards charts and visualizations. +The Charts plugin provides utility services for accessing shared colors and themes for visual consistency across all OpenSearch Dashboards charts and visualizations. It also provides a number of static utility functions and standard components for user-specified chart configuration. -## Static methods +## Services -### `vislibColorMaps` +### Theme service -Color mappings related to vislib visualizations +A utility service for fetching `chartsTheme` and `chartsBaseTheme`. -### `truncatedColorMaps` +For more, see Theme service [docs](public/services/theme/README.md) -Color mappings subset of `vislibColorMaps` +### Color service +#### Static properties +##### `seedColors` + +A list of colors chosen for visual appeal. + +#### Static methods +##### `mappedColors` -### `colorSchemas` +Get a value-based mapping of colors. + +##### `createColorLookupFunction` + +Factory for color mapping function. + +## Static functions and components +### Color maps +#### `colorSchemas` Color mappings in `value`/`text` form -### `getHeatmapColors` +#### `getHeatmapColors` Function to retrieve heatmap related colors based on `value` and `colorSchemaName` -### `truncatedColorSchemas` +#### `truncatedColorMaps` + +Color mappings subset of `vislibColorMaps` + +#### `truncatedColorSchemas` Truncated color mappings in `value`/`text` form -## Theme +#### `vislibColorMaps` + +Color mappings related to vislib visualizations + +### Components + +Standardized React input UI components which can be used by visualization editors to specify various visualization options. + +#### `BasicOptions` + +Components for specifying legend and tooltip + +#### `ColorRanges` + +Component for specifying color range thresholds + +#### `ColorSchemaOptions` + +Component for specifying color schemas (palettes) + +#### `NumberInputOption` + +Deprecated in favor of `RequiredNumberInputOption` + +#### `RangeOption` + +Component for specifying a numerical value with a slider + +#### `RequiredNumberInputOption` + +Component for specifying numerical values, such as a threshold. + +#### `SelectOption` + +Basic select component + +#### `SwitchOption` + +Basic toggle component + +#### `TextInputOption` -See Theme service [docs](public/services/theme/README.md) +Basic text input component diff --git a/src/plugins/console/public/application/contexts/services_context.tsx b/src/plugins/console/public/application/contexts/services_context.tsx index 9e926eef973..fc9ab157f78 100644 --- a/src/plugins/console/public/application/contexts/services_context.tsx +++ b/src/plugins/console/public/application/contexts/services_context.tsx @@ -29,7 +29,7 @@ */ import React, { createContext, useContext, useEffect } from 'react'; -import { NotificationsSetup } from 'opensearch-dashboards/public'; +import { HttpSetup, NotificationsSetup } from 'opensearch-dashboards/public'; import { History, Settings, Storage } from '../../services'; import { ObjectStorageClient } from '../../../common/types'; import { MetricsTracker } from '../../types'; @@ -43,6 +43,7 @@ interface ContextServices { objectStorageClient: ObjectStorageClient; trackUiMetric: MetricsTracker; opensearchHostService: OpenSearchHostService; + http: HttpSetup; } export interface ContextValue { diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts new file mode 100644 index 00000000000..eaa17113278 --- /dev/null +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts @@ -0,0 +1,182 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + HttpFetchError, + HttpFetchOptionsWithPath, + HttpResponse, + HttpSetup, +} from '../../../../../../core/public'; +import { OpenSearchRequestArgs, sendRequestToOpenSearch } from './send_request_to_opensearch'; +import * as opensearch from '../../../lib/opensearch/opensearch'; + +const createMockResponse = ( + statusCode: number, + statusText: string, + headers: Array<[string, string]> +): Response => { + return { + // headers: {} as Headers, + headers: new Headers(headers), + ok: true, + redirected: false, + status: statusCode, + statusText, + type: 'basic', + url: '', + clone: jest.fn(), + body: (jest.fn() as unknown) as ReadableStream, + bodyUsed: true, + arrayBuffer: jest.fn(), + blob: jest.fn(), + text: jest.fn(), + formData: jest.fn(), + json: jest.fn(), + }; +}; + +const createMockHttpResponse = ( + statusCode: number, + statusText: string, + headers: Array<[string, string]>, + body: any +): HttpResponse => { + return { + fetchOptions: (jest.fn() as unknown) as Readonly, + request: (jest.fn() as unknown) as Readonly, + response: createMockResponse(statusCode, statusText, headers), + body, + }; +}; +const dummyArgs: OpenSearchRequestArgs = { + http: ({ + post: jest.fn(), + } as unknown) as HttpSetup, + requests: [ + { + method: 'GET', + url: '/dummy/api', + data: ['{}'], + }, + ], +}; + +describe('test sendRequestToOpenSearch', () => { + it('test request success, json', () => { + const mockHttpResponse = createMockHttpResponse( + 200, + 'ok', + [['Content-Type', 'application/json, utf-8']], + { + ok: true, + } + ); + + jest.spyOn(opensearch, 'send').mockResolvedValue(mockHttpResponse); + sendRequestToOpenSearch(dummyArgs).then((result) => { + expect((result as any)[0].response.value).toBe('{\n "ok": true\n}'); + }); + }); + + it('test request success, text', () => { + const mockHttpResponse = createMockHttpResponse( + 200, + 'ok', + [['Content-Type', 'text/plain']], + 'response text' + ); + + jest.spyOn(opensearch, 'send').mockResolvedValue(mockHttpResponse); + sendRequestToOpenSearch(dummyArgs).then((result) => { + expect((result as any)[0].response.value).toBe('response text'); + }); + }); + + it('test request success, with warning', () => { + const mockHttpResponse = createMockHttpResponse( + 200, + 'ok', + [ + ['Content-Type', 'text/plain'], + ['warning', 'dummy warning'], + ], + 'response text' + ); + + jest.spyOn(opensearch, 'send').mockResolvedValue(mockHttpResponse); + sendRequestToOpenSearch(dummyArgs).then((result) => { + expect((result as any)[0].response.value).toBe( + '#! Deprecation: dummy warning\nresponse text' + ); + }); + }); + + it('test request 404', () => { + const mockHttpResponse = createMockHttpResponse( + 404, + 'not found', + [['Content-Type', 'text/plain']], + 'response text' + ); + + jest.spyOn(opensearch, 'send').mockResolvedValue(mockHttpResponse); + sendRequestToOpenSearch(dummyArgs).then((result) => { + expect((result as any)[0].response.value).toBe('response text'); + }); + }); + + it('test request 500, json', () => { + const mockHttpError: HttpFetchError = new HttpFetchError( + 'error message', + 'name', + (jest.fn as unknown) as Request, + createMockResponse(500, 'Server Error', [['Content-Type', 'application/json, utf-8']]), + { errorMsg: 'message' } + ); + + jest.spyOn(opensearch, 'send').mockRejectedValue(mockHttpError); + sendRequestToOpenSearch(dummyArgs).catch((error) => { + expect(error.response.value).toBe('{\n "errorMsg": "message"\n}'); + }); + }); + + it('test request 500, text', () => { + const mockHttpError: HttpFetchError = new HttpFetchError( + 'error message', + 'name', + (jest.fn as unknown) as Request, + createMockResponse(500, 'Server Error', [['Content-Type', 'text/plain']]), + 'error message' + ); + + jest.spyOn(opensearch, 'send').mockRejectedValue(mockHttpError); + sendRequestToOpenSearch(dummyArgs).catch((error) => { + expect(error.response.value).toBe('error message'); + }); + }); + + it('test no connection', () => { + const mockHttpError: HttpFetchError = new HttpFetchError( + 'error message', + 'name', + (jest.fn as unknown) as Request, + undefined, + 'error message' + ); + + jest.spyOn(opensearch, 'send').mockRejectedValue(mockHttpError); + sendRequestToOpenSearch(dummyArgs).catch((error) => { + expect(error.response.value).toBe( + "\n\nFailed to connect to Console's backend.\nPlease check the OpenSearch Dashboards server is up and running" + ); + }); + }); +}); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts index a74ab610a67..ad7ba440b6d 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts @@ -28,6 +28,7 @@ * under the License. */ +import { HttpFetchError, HttpSetup } from 'opensearch-dashboards/public'; import { extractDeprecationMessages } from '../../../lib/utils'; import { XJson } from '../../../../../opensearch_ui_shared/public'; const { collapseLiteralStrings } = XJson; @@ -36,6 +37,7 @@ import * as opensearch from '../../../lib/opensearch/opensearch'; import { BaseResponseType } from '../../../types'; export interface OpenSearchRequestArgs { + http: HttpSetup; requests: any; } @@ -76,7 +78,7 @@ export function sendRequestToOpenSearch( const isMultiRequest = requests.length > 1; - const sendNextRequest = () => { + const sendNextRequest = async () => { if (reqId !== CURRENT_REQ_ID) { resolve(results); return; @@ -94,79 +96,96 @@ export function sendRequestToOpenSearch( } // append a new line for bulk requests. const startTime = Date.now(); - opensearch - .send(opensearchMethod, opensearchPath, opensearchData) - .always((dataOrjqXHR: any, textStatus: string, jqXhrORerrorThrown: any) => { - if (reqId !== CURRENT_REQ_ID) { - return; - } - - const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; - - const isSuccess = - typeof xhr.status === 'number' && - // Things like DELETE index where the index is not there are OK. - ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); - - if (isSuccess) { - let value = xhr.responseText; - - const warnings = xhr.getResponseHeader('warning'); - if (warnings) { - const deprecationMessages = extractDeprecationMessages(warnings); - value = deprecationMessages.join('\n') + '\n' + value; - } - - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; - } - - results.push({ - response: { - timeMs: Date.now() - startTime, - statusCode: xhr.status, - statusText: xhr.statusText, - contentType: xhr.getResponseHeader('Content-Type'), - value, - }, - request: { - data: opensearchData, - method: opensearchMethod, - path: opensearchPath, - }, - }); - - // single request terminate via sendNextRequest as well - sendNextRequest(); + try { + const httpResponse = await opensearch.send( + args.http, + opensearchMethod, + opensearchPath, + opensearchData + ); + if (reqId !== CURRENT_REQ_ID) { + return; + } + const statusCode = httpResponse.response?.status; + const isSuccess = + // Things like DELETE index where the index is not there are OK. + statusCode && ((statusCode >= 200 && statusCode < 300) || statusCode === 404); + if (isSuccess) { + const contentType = httpResponse.response.headers.get('Content-Type') as BaseResponseType; + let value = ''; + if (contentType.includes('application/json')) { + value = JSON.stringify(httpResponse.body, null, 2); } else { - let value; - let contentType: string; - if (xhr.responseText) { - value = xhr.responseText; // OpenSearch error should be shown - contentType = xhr.getResponseHeader('Content-Type'); + value = httpResponse.body; + } + const warnings = httpResponse.response.headers.get('warning'); + if (warnings) { + const deprecationMessages = extractDeprecationMessages(warnings); + value = deprecationMessages.join('\n') + '\n' + value; + } + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; + } + results.push({ + response: { + timeMs: Date.now() - startTime, + statusCode, + statusText: httpResponse.response.statusText, + contentType, + value, + }, + request: { + data: opensearchData, + method: opensearchMethod, + path: opensearchPath, + }, + }); + + // single request terminate via sendNextRequest as well + await sendNextRequest(); + } + } catch (error) { + const httpError = error as HttpFetchError; + const httpResponse = httpError.response; + let value; + let contentType: string; + if (httpResponse) { + if (httpError.body) { + contentType = httpResponse.headers.get('Content-Type') as string; + if (contentType?.includes('application/json')) { + value = JSON.stringify(httpError.body, null, 2); } else { - value = 'Request failed to get to the server (status code: ' + xhr.status + ')'; - contentType = 'text/plain'; - } - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; + value = httpError.body; } - reject({ - response: { - value, - contentType, - timeMs: Date.now() - startTime, - statusCode: xhr.status, - statusText: xhr.statusText, - }, - request: { - data: opensearchData, - method: opensearchMethod, - path: opensearchPath, - }, - }); + } else { + value = + 'Request failed to get to the server (status code: ' + httpResponse.status + ')'; + contentType = 'text/plain'; } + } else { + value = + "\n\nFailed to connect to Console's backend.\nPlease check the OpenSearch Dashboards server is up and running"; + contentType = 'text/plain'; + } + + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; + } + reject({ + response: { + value, + contentType, + timeMs: Date.now() - startTime, + statusCode: httpResponse?.status, + statusText: httpResponse?.statusText, + }, + request: { + data: opensearchData, + method: opensearchMethod, + path: opensearchPath, + }, }); + } }; sendNextRequest(); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts index e5c9e7f2d2d..30714f56b85 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts @@ -40,7 +40,7 @@ import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; export const useSendCurrentRequestToOpenSearch = () => { const { - services: { history, settings, notifications, trackUiMetric }, + services: { history, settings, notifications, trackUiMetric, http }, } = useServicesContext(); const dispatch = useRequestActionContext(); @@ -64,7 +64,7 @@ export const useSendCurrentRequestToOpenSearch = () => { // Fire and forget setTimeout(() => track(requests, editor, trackUiMetric), 0); - const results = await sendRequestToOpenSearch({ requests }); + const results = await sendRequestToOpenSearch({ http, requests }); results.forEach(({ request: { path, method, data } }) => { try { @@ -112,5 +112,5 @@ export const useSendCurrentRequestToOpenSearch = () => { }); } } - }, [dispatch, settings, history, notifications, trackUiMetric]); + }, [dispatch, settings, history, notifications, trackUiMetric, http]); }; diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index 51136c7805d..a7d757482e9 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -82,6 +82,7 @@ export function renderApp({ notifications, trackUiMetric, objectStorageClient, + http, }, }} > diff --git a/src/plugins/console/public/lib/opensearch/opensearch.ts b/src/plugins/console/public/lib/opensearch/opensearch.ts index 3360d0108c1..ab6b79469e8 100644 --- a/src/plugins/console/public/lib/opensearch/opensearch.ts +++ b/src/plugins/console/public/lib/opensearch/opensearch.ts @@ -28,8 +28,7 @@ * under the License. */ -import $ from 'jquery'; -import { stringify } from 'query-string'; +import { HttpResponse, HttpSetup } from 'opensearch-dashboards/public'; const opensearchVersion: string[] = []; @@ -42,35 +41,21 @@ export function getContentType(body: any) { return 'application/json'; } -export function send(method: string, path: string, data: any) { - const wrappedDfd = $.Deferred(); - - const options: JQuery.AjaxSettings = { - url: '../api/console/proxy?' + stringify({ path, method }, { sort: false }), - headers: { - 'osd-xsrf': 'opensearchDashboards', - }, - data, - contentType: getContentType(data), - cache: false, - crossDomain: true, - type: 'POST', - dataType: 'text', // disable automatic guessing - }; - - $.ajax(options).then( - (responseData: any, textStatus: string, jqXHR: any) => { - wrappedDfd.resolveWith({}, [responseData, textStatus, jqXHR]); +export async function send( + http: HttpSetup, + method: string, + path: string, + data: any +): Promise { + return await http.post('/api/console/proxy', { + query: { + path, + method, }, - ((jqXHR: any, textStatus: string, errorThrown: Error) => { - if (jqXHR.status === 0) { - jqXHR.responseText = - "\n\nFailed to connect to Console's backend.\nPlease check the OpenSearch Dashboards server is up and running"; - } - wrappedDfd.rejectWith({}, [jqXHR, textStatus, errorThrown]); - }) as any - ); - return wrappedDfd; + body: data, + prependBasePath: true, + asResponse: true, + }); } export function constructOpenSearchUrl(baseUri: string, path: string) { diff --git a/src/plugins/data/README.md b/src/plugins/data/README.md index 383fd80a11d..39f514e8151 100644 --- a/src/plugins/data/README.md +++ b/src/plugins/data/README.md @@ -111,7 +111,7 @@ The `SearchSource` API is a convenient way to construct and run an OpenSearch se #### Default Search Strategy -One benefit of using the low-level search API, is partial response support in X-Pack, allowing for a better and more responsive user experience. +One benefit of using the low-level search API is that it allows for a better and more responsive user experience with partial responses. In OSS only the final result is returned. ```.ts diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 8bff51d1f16..c5c945f1b89 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -95,6 +95,12 @@ export abstract class FieldFormat { */ public type: any = this.constructor; + /** + * @property {boolean} - allow numeric aggregation + * @private + */ + allowsNumericalAggregations?: boolean; + protected readonly _params: any; protected getConfig: FieldFormatsGetConfigFn | undefined; diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 076fbd9de3d..583c73a2724 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -117,7 +117,7 @@ describe('AggConfigs', () => { expect(ac.aggs).toHaveLength(3); }); - it('adds new AggConfig entries to AggConfigs by default', () => { + it('adds new AggConfig entries to end of AggConfigs by default', () => { const configStates = [ { enabled: true, @@ -136,6 +136,7 @@ describe('AggConfigs', () => { schema: 'split', }); expect(ac.aggs).toHaveLength(2); + expect(ac.aggs[1].schema).toBe('split'); }); it('does not add an agg to AggConfigs if addToAggConfigs: false', () => { @@ -161,6 +162,55 @@ describe('AggConfigs', () => { ); expect(ac.aggs).toHaveLength(1); }); + + it('adds new AggConfig entries to beginning of AggConfigs if mustBeFirst: true', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(ac.aggs).toHaveLength(1); + + ac.createAggConfig( + { + enabled: true, + type: 'terms', + params: {}, + schema: 'split', + }, + { mustBeFirst: true } + ); + expect(ac.aggs).toHaveLength(2); + expect(ac.aggs[0].schema).toBe('split'); + }); + + it('does not add an agg to AggConfigs if addToAggConfigs: false and mustBeFirst: true', () => { + const configStates = [ + { + enabled: true, + type: 'histogram', + params: {}, + }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + expect(ac.aggs).toHaveLength(1); + + ac.createAggConfig( + { + enabled: true, + type: 'terms', + params: {}, + schema: 'split', + }, + { addToAggConfigs: false, mustBeFirst: true } + ); + expect(ac.aggs).toHaveLength(1); + }); }); describe('#getRequestAggs', () => { diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 90ad5820ea3..8f13fcd54f0 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -130,10 +130,10 @@ export class AggConfigs { createAggConfig = ( params: CreateAggConfigParams, - { addToAggConfigs = true } = {} + { addToAggConfigs = true, mustBeFirst = false } = {} ) => { const { type } = params; - let aggConfig; + let aggConfig: AggConfig; if (params instanceof AggConfig) { aggConfig = params; @@ -145,8 +145,12 @@ export class AggConfigs { }); } + const addAggConfig = () => { + return mustBeFirst ? this.aggs.unshift(aggConfig) : this.aggs.push(aggConfig); + }; + if (addToAggConfigs) { - this.aggs.push(aggConfig); + addAggConfig(); } return aggConfig as T; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 8b495a2a19b..3f96955e22b 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -444,6 +444,7 @@ export { Filter, Query, RefreshInterval, TimeRange } from '../common'; export { createSavedQueryService, + connectStorageToQueryState, connectToQueryState, syncQueryStateWithUrl, QueryState, diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 27ea9510000..8beb241f876 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -85,6 +85,12 @@ const createStartContract = (): Start => { }, }), get: jest.fn().mockReturnValue(Promise.resolve({})), + getDefault: jest.fn().mockReturnValue( + Promise.resolve({ + name: 'Default name', + id: 'id', + }) + ), clearCache: jest.fn(), } as unknown) as IndexPatternsContract, }; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 615ca3c2e0d..4651f4a2b70 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -368,6 +368,20 @@ export enum BUCKET_TYPES { // @public export const castOpenSearchToOsdFieldTypeName: (opensearchType: OPENSEARCH_FIELD_TYPES | string) => OSD_FIELD_TYPES; +// @public +export const connectStorageToQueryState = ( + { + filterManager, + queryString, + state$, + }: Pick, + OsdUrlStateStorage: IOsdUrlStateStorage, + syncConfig: { + filters: FilterStateStore; + query: boolean; + } +) => () => void; + // Warning: (ae-forgotten-export) The symbol "QuerySetup" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "BaseStateContainer" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "connectToQueryState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/query/state_sync/README.md b/src/plugins/data/public/query/state_sync/README.md index 6b9b1581005..f54688afccf 100644 --- a/src/plugins/data/public/query/state_sync/README.md +++ b/src/plugins/data/public/query/state_sync/README.md @@ -1,3 +1,26 @@ # Query state syncing utilities Set of helpers to connect data services to state containers and state syncing utilities + +# Connect to query state + +This set of functions help sync state storage and state container with query managers. + +1. `connectStorageToQueryState()` + - This function take three input parameters: query state managers, `OsdUrlStateStorage`, and the two configs that it helps syncing, `app filters` and `query` + - If `OsdUrlStateStorage` is empty, then we initialize the `OsdUrlStateStorage` using the default app filter and query by calling `getDefaultQuery()` and `getAppFilters()` + - If the current query state and filter state differentiate from the url state storage, we update the filter and query values using state managers for filter and query from the data plugin. This step ensures that if we refresh the page, filter and query still persists their previous values. + - Then we set up subscriptions for both filter and query, so whenever we change the values for either of them, the new state get synced up with `OsdUrlStateStorage`. + - In the return function, we unsubscribe each one of them. + +2. `connectToQueryState()` + - This function take three input parameters: query state managers, `state container`, and the four configs that it helps syncing, `filter`, `query`, `time` and `refresh intervals` + - For initial syncing, we get the initial values from the state managers in the data plugin, and we store the values in the state container + - Then we set up subscriptions for each one of the config in data plugin, so whenever we change the values for any one of them, the new state get saved and sync with the state container. + - We also set up subscriptions for the states in state container, so whenever the value in state container get changed, the state managers in the data plugin will also be updated. + - In the return function, we unsubscribe each one of them. + +There are a couple differences between the above two functions: +1. `connectStorageToQueryState()` uses `OsdUrlStateStorage` for syncing, while `connectToQueryState()` uses `state container` for syncing, and `state container` is then synced up with `OsdUrlStateStorage`. +2. `connectStorageToQueryState()` can be used for persisting the app states, specifically app filter and query values, while `connectToQueryState()` can be used for persisting both app states and global states, specificlly app filter and query which are part of app states, global filter, time range, time refresh interval which are parts of global states. +3. `connectStorageToQueryState()` sets up a one way syncing from data to `OsdUrlStateStorage`, while `connectToQueryState()` sets up two-way syncing of the data and `state container`. Both of the functions serve to connect data services to achieve state syncing. \ No newline at end of file diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 5b4e0f22806..6b540c3da5c 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -31,19 +31,32 @@ import { Subscription } from 'rxjs'; import { FilterManager } from '../filter_manager'; import { getFilter } from '../filter_manager/test_helpers/get_stub_filter'; -import { Filter, FilterStateStore, UI_SETTINGS } from '../../../common'; +import { Filter, FilterStateStore, Query, UI_SETTINGS } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; import { BaseStateContainer, createStateContainer, + IOsdUrlStateStorage, + createOsdUrlStateStorage, Storage, } from '../../../../opensearch_dashboards_utils/public'; import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from '../../../../../test_utils/public/stub_browser_storage'; -import { connectToQueryState } from './connect_to_query_state'; +import { connectStorageToQueryState, connectToQueryState } from './connect_to_query_state'; import { TimefilterContract } from '../timefilter'; import { QueryState } from './types'; - +import { createBrowserHistory, History } from 'history'; +import { QueryStringContract } from '../query_string'; + +const connectStorageToQueryStateFn = ( + query: QueryStart, + OsdUrlStateStorage: IOsdUrlStateStorage +) => { + connectStorageToQueryState(query, OsdUrlStateStorage, { + filters: FilterStateStore.APP_STATE, + query: true, + }); +}; const connectToQueryGlobalState = (query: QueryStart, state: BaseStateContainer) => connectToQueryState(query, state, { refreshInterval: true, @@ -74,6 +87,110 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { } }); +describe('connect_storage_to_query_state', () => { + let queryServiceStart: QueryStart; + let queryString: QueryStringContract; + let queryChangeSub: Subscription; + let queryChangeTriggered = jest.fn(); + let filterManager: FilterManager; + let filterManagerChangeSub: Subscription; + let filterManagerChangeTriggered = jest.fn(); + let osdUrlStateStorage: IOsdUrlStateStorage; + let history: History; + let gF1: Filter; + let gF2: Filter; + let aF1: Filter; + let aF2: Filter; + let q1: Query; + + beforeEach(() => { + const queryService = new QueryService(); + queryService.setup({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + }); + queryServiceStart = queryService.start({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + savedObjectsClient: startMock.savedObjects.client, + }); + + queryString = queryServiceStart.queryString; + queryChangeTriggered = jest.fn(); + queryChangeSub = queryString.getUpdates$().subscribe(queryChangeTriggered); + + filterManager = queryServiceStart.filterManager; + filterManagerChangeTriggered = jest.fn(); + filterManagerChangeSub = filterManager.getUpdates$().subscribe(filterManagerChangeTriggered); + + window.location.href = '/'; + history = createBrowserHistory(); + osdUrlStateStorage = createOsdUrlStateStorage({ useHash: false, history }); + + gF1 = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'key1', 'value1'); + gF2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'key2', 'value2'); + aF1 = getFilter(FilterStateStore.APP_STATE, true, true, 'key3', 'value3'); + aF2 = getFilter(FilterStateStore.APP_STATE, false, false, 'key4', 'value4'); + + q1 = { + query: 'count is less than 100', + language: 'kuery', + }; + }); + + afterEach(() => { + filterManagerChangeSub.unsubscribe(); + queryChangeSub.unsubscribe(); + }); + + test('state is initialized with default state', () => { + expect(osdUrlStateStorage.get('_q')).toBeNull(); + connectStorageToQueryStateFn(queryServiceStart, osdUrlStateStorage); + + expect(osdUrlStateStorage.get('_q')).toEqual({ + query: queryString.getDefaultQuery(), + filters: filterManager.getAppFilters(), + }); + }); + + test('state is initialized with URL states', () => { + const initialStates = { + filters: [aF1, aF2], + query: q1, + }; + osdUrlStateStorage.set('_q', initialStates, { + replace: true, + }); + connectStorageToQueryStateFn(queryServiceStart, osdUrlStateStorage); + expect(filterManager.getFilters().length).toBe(2); + expect(queryString.getQuery()).toStrictEqual(q1); + }); + + test('when global filter changes, filter in storage should not be updated', () => { + connectStorageToQueryStateFn(queryServiceStart, osdUrlStateStorage); + const previousStorage = osdUrlStateStorage.get('_q'); + filterManager.setFilters([gF1, gF1]); + const updatedStorage = osdUrlStateStorage.get('_q'); + expect(previousStorage).toStrictEqual(updatedStorage); + }); + + test('when app filter changes, filter storage should be updated', () => { + connectStorageToQueryStateFn(queryServiceStart, osdUrlStateStorage); + const previousStorage = osdUrlStateStorage.get('_q'); + filterManager.setFilters([aF1, aF1]); + const updatedStorage = osdUrlStateStorage.get('_q'); + expect(previousStorage).not.toStrictEqual(updatedStorage); + }); + + test('when query changes, state updates query', () => { + connectStorageToQueryStateFn(queryServiceStart, osdUrlStateStorage); + const previousStorage = osdUrlStateStorage.get('_q'); + queryString.setQuery(q1); + const updatedStorage = osdUrlStateStorage.get('_q'); + expect(previousStorage).not.toStrictEqual(updatedStorage); + }); +}); + describe('connect_to_global_state', () => { let queryServiceStart: QueryStart; let filterManager: FilterManager; diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index 2f492fd28f7..8b850b36eab 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -31,12 +31,113 @@ import { Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import _ from 'lodash'; -import { BaseStateContainer } from '../../../../opensearch_dashboards_utils/public'; +import { + BaseStateContainer, + IOsdUrlStateStorage, +} from '../../../../opensearch_dashboards_utils/public'; import { QuerySetup, QueryStart } from '../query_service'; import { QueryState, QueryStateChange } from './types'; import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common'; import { validateTimeRange } from '../timefilter'; +/** + * Helper function to sync up filter and query services in data plugin + * with a URL state storage so plugins can persist the app filter and query + * values across refresh + * @param QueryService: either setup or start + * @param OsdUrlStateStorage to use for syncing and store data + * @param syncConfig app filter and query + */ +export const connectStorageToQueryState = ( + { + filterManager, + queryString, + state$, + }: Pick, + OsdUrlStateStorage: IOsdUrlStateStorage, + syncConfig: { + filters: FilterStateStore; + query: boolean; + } +) => { + try { + const syncKeys: Array = []; + if (syncConfig.query) { + syncKeys.push('query'); + } + if (syncConfig.filters === FilterStateStore.APP_STATE) { + syncKeys.push('appFilters'); + } + + const initialStateFromURL: QueryState = OsdUrlStateStorage.get('_q') ?? { + query: queryString.getDefaultQuery(), + filters: filterManager.getAppFilters(), + }; + + // set up initial '_q' flag in the URL to sync query and filter changes + if (!OsdUrlStateStorage.get('_q')) { + OsdUrlStateStorage.set('_q', initialStateFromURL, { + replace: true, + }); + } + + if (syncConfig.query && !_.isEqual(initialStateFromURL.query, queryString.getQuery())) { + if (initialStateFromURL.query) { + queryString.setQuery(_.cloneDeep(initialStateFromURL.query)); + } + } + + if (syncConfig.filters === FilterStateStore.APP_STATE) { + if ( + !initialStateFromURL.filters || + !compareFilters(initialStateFromURL.filters, filterManager.getAppFilters(), { + ...COMPARE_ALL_OPTIONS, + state: false, + }) + ) { + if (initialStateFromURL.filters) { + filterManager.setAppFilters(_.cloneDeep(initialStateFromURL.filters)); + } + } + + const subs: Subscription[] = [ + state$ + .pipe( + filter(({ changes }) => { + return syncKeys.some((syncKey) => changes[syncKey]); + }), + map(({ changes, state }) => { + const newState: QueryState = { + query: state.query, + filters: state.filters, + }; + if (syncConfig.query && changes.query) { + newState.query = queryString.getQuery(); + } + + if (syncConfig.filters === FilterStateStore.APP_STATE && changes.appFilters) { + newState.filters = filterManager.getAppFilters(); + } + + return newState; + }) + ) + .subscribe((newState) => { + OsdUrlStateStorage.set('_q', newState, { + replace: true, + }); + }), + ]; + + return () => { + subs.forEach((s) => s.unsubscribe()); + }; + } + } catch (err) { + return; + } +}; + /** * Helper to setup two-way syncing of global data and a state container * @param QueryService: either setup or start diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index 7109d7d8702..5c5619461e0 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -28,6 +28,6 @@ * under the License. */ -export { connectToQueryState } from './connect_to_query_state'; +export { connectToQueryState, connectStorageToQueryState } from './connect_to_query_state'; export { syncQueryStateWithUrl } from './sync_state_with_url'; export { QueryState, QueryStateChange } from './types'; diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index f9159b3246f..a1e52fbb66a 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -96,7 +96,7 @@ export class DataServerPlugin this.autocompleteService = new AutocompleteService(initializerContext); } - public setup( + public async setup( core: CoreSetup, { expressions, usageCollection, dataSource }: DataPluginSetupDependencies ) { @@ -108,7 +108,7 @@ export class DataServerPlugin core.uiSettings.register(getUiSettings()); - const searchSetup = this.searchService.setup(core, { + const searchSetup = await this.searchService.setup(core, { registerFunction: expressions.registerFunction, usageCollection, dataSource, diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index d227dea8057..6bf6a90c63b 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -28,8 +28,7 @@ * under the License. */ -import { CoreSetup, PluginInitializerContext } from 'opensearch-dashboards/server'; -import { first } from 'rxjs/operators'; +import { CoreSetup } from 'opensearch-dashboards/server'; import { Usage } from './register'; import { ConfigSchema } from '../../../config'; @@ -40,16 +39,9 @@ export interface SearchUsage { trackSuccess(duration: number): Promise; } -export function usageProvider( - core: CoreSetup, - initializerContext: PluginInitializerContext -): SearchUsage { +export function usageProvider(core: CoreSetup, config: ConfigSchema): SearchUsage { const getTracker = (eventType: keyof Usage) => { return async (duration?: number) => { - const config = await initializerContext.config - .create() - .pipe(first()) - .toPromise(); if (config?.search?.usageTelemetry?.enabled) { const repository = await core .getStartServices() diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 1cece2277c0..4a608c3df7e 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -53,7 +53,7 @@ describe('Search service', () => { describe('setup()', () => { it('exposes proper contract', async () => { - const setup = plugin.setup(mockCoreSetup, ({ + const setup = await plugin.setup(mockCoreSetup, ({ packageInfo: { version: '8' }, registerFunction: jest.fn(), } as unknown) as SearchServiceSetupDependencies); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 0c33b95f460..6620b88a0fe 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -105,11 +105,15 @@ export class SearchService implements Plugin { private readonly logger: Logger ) {} - public setup( + public async setup( core: CoreSetup<{}, DataPluginStart>, { registerFunction, usageCollection, dataSource }: SearchServiceSetupDependencies - ): ISearchSetup { - const usage = usageCollection ? usageProvider(core, this.initializerContext) : undefined; + ): Promise { + const config = await this.initializerContext.config + .create() + .pipe(first()) + .toPromise(); + const usage = usageCollection ? usageProvider(core, config) : undefined; const router = core.http.createRouter(); const routeDependencies = { diff --git a/src/plugins/data_source/README.md b/src/plugins/data_source/README.md index fdf6baab783..ca212f4ded7 100755 --- a/src/plugins/data_source/README.md +++ b/src/plugins/data_source/README.md @@ -11,23 +11,24 @@ Update the following configuration in the `opensearch_dashboards.yml` file to ap 1. The dataSource plugin is disabled by default; to enable it: `data_source.enabled: true` -2. The audit trail is enabled by default for logging the access to data source; to disable it: - `data_source.audit.enabled: false` +2. The audit trail is disabled by default for logging the access to data source; to disable it: + `data_source.audit.enabled: true` -- Current auditor configuration: +- Default auditor configuration: -``` +```yml data_source.audit.appender.kind: 'file' data_source.audit.appender.layout.kind: 'pattern' -data_source.audit.appender.path: '/tmp/opensearch-dashboards-data-source-audit.log' +data_source.audit.appender.path: '/opensearch-dashboards-data-source-audit.log' ``` 3. The default encryption-related configuration parameters are: -``` +```yml data_source.encryption.wrappingKeyName: 'changeme' data_source.encryption.wrappingKeyNamespace: 'changeme' -data_source.encryption.wrappingKey: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] +data_source.encryption.wrappingKey: + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ``` Note that if any of the encryption keyring configuration values change (wrappingKeyName/wrappingKeyNamespace/wrappingKey), none of the previously-encrypted credentials can be decrypted; therefore, credentials of previously created data sources must be updated to continue use. @@ -79,7 +80,7 @@ a. Envelope encryption - provides strong protection on data keys. Read more deta b. Key derivation with HMAC - KDF with SHA-384 protects against accidental reuse of a data encryption keys and reduces the risk of overusing data keys. -c. Signature algorithm - ECDSA with P-384 and SHA-384. Under multiple data source case, data source documents stored on OpenSearch can be modified / replaced by attacker. With ECDSA signature, ciphertext decryption will fail if it’s getting pullted. No one will be able to create another signature that verifies with the public key because the private key has been dropped. +c. Signature algorithm - ECDSA with P-384 and SHA-384. Under multiple data source case, data source documents stored on OpenSearch can be modified / replaced by attacker. With ECDSA signature, ciphertext decryption will fail if it’s getting polluted. No one will be able to create another signature that verifies with the public key because the private key has been dropped. Please check https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1756 for more details. @@ -88,5 +89,14 @@ Please check https://github.com/opensearch-project/OpenSearch-Dashboards/issues/ ## Development See the [OpenSearch Dashboards contributing -guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions -setting up your development environment. +guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. + +### Design Documents + +- [High level design doc](../../../docs/multi-datasource/high_level_design.md) +- [User stories](../../../docs/multi-datasource/user_stories.md) +- [Client management detailed design](../../../docs/multi-datasource/client_management_design.md) + +### Integrate with multiple data source feature + +TODO: [#2455](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2455) diff --git a/src/plugins/data_source/audit_config.ts b/src/plugins/data_source/audit_config.ts index d8c99fbfa84..ec898ff5441 100644 --- a/src/plugins/data_source/audit_config.ts +++ b/src/plugins/data_source/audit_config.ts @@ -4,6 +4,8 @@ */ import { schema } from '@osd/config-schema'; +import os from 'os'; +import path from 'path'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { DateConversion } from '../../../src/core/server/logging/layouts/conversions'; @@ -36,7 +38,7 @@ export const fileAppenderSchema = schema.object( kind: 'pattern', highlight: true, }, - path: '/tmp/opensearch-dashboards-data-source-audit.log', + path: path.join(os.tmpdir(), 'opensearch-dashboards-data-source-audit.log'), }, } ); diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index afcf3d662fe..366e5a0f3f5 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -11,8 +11,20 @@ export interface DataSourceAttributes extends SavedObjectAttributes { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | undefined; + credentials: UsernamePasswordTypedContent | SigV4Content | undefined; }; + lastUpdatedTime?: string; +} + +/** + * Multiple datasource supports authenticating as IAM user, it doesn't support IAM role. + * Because IAM role session requires temporary security credentials through assuming role, + * which makes no sense to store the credentials. + */ +export interface SigV4Content extends SavedObjectAttributes { + accessKey: string; + secretKey: string; + region: string; } export interface UsernamePasswordTypedContent extends SavedObjectAttributes { @@ -23,4 +35,5 @@ export interface UsernamePasswordTypedContent extends SavedObjectAttributes { export enum AuthType { NoAuth = 'no_auth', UsernamePasswordType = 'username_password', + SigV4 = 'sigv4', } diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts index f2fd79fade9..1fc4e00c3e2 100644 --- a/src/plugins/data_source/config.ts +++ b/src/plugins/data_source/config.ts @@ -34,7 +34,7 @@ export const configSchema = schema.object({ size: schema.number({ defaultValue: 5 }), }), audit: schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.boolean({ defaultValue: false }), appender: fileAppenderSchema, }), }); diff --git a/src/plugins/data_source/server/client/client_config.test.ts b/src/plugins/data_source/server/client/client_config.test.ts index 39a3607ccba..c6dfff3fe4c 100644 --- a/src/plugins/data_source/server/client/client_config.test.ts +++ b/src/plugins/data_source/server/client/client_config.test.ts @@ -5,7 +5,7 @@ import { DataSourcePluginConfigType } from '../../config'; import { parseClientOptions } from './client_config'; -const TEST_DATA_SOURCE_ENDPOINT = 'http://datasource.com'; +const TEST_DATA_SOURCE_ENDPOINT = 'http://test.com/'; const config = { enabled: true, diff --git a/src/plugins/data_source/server/client/client_pool.ts b/src/plugins/data_source/server/client/client_pool.ts index fe5458d8f6c..288682ef253 100644 --- a/src/plugins/data_source/server/client/client_pool.ts +++ b/src/plugins/data_source/server/client/client_pool.ts @@ -7,11 +7,12 @@ import { Client } from '@opensearch-project/opensearch'; import { Client as LegacyClient } from 'elasticsearch'; import LRUCache from 'lru-cache'; import { Logger } from 'src/core/server'; +import { AuthType } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; export interface OpenSearchClientPoolSetup { - getClientFromPool: (id: string) => Client | LegacyClient | undefined; - addClientToPool: (endpoint: string, client: Client | LegacyClient) => void; + getClientFromPool: (endpoint: string, authType: AuthType) => Client | LegacyClient | undefined; + addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void; } /** @@ -21,23 +22,28 @@ export interface OpenSearchClientPoolSetup { * It reuse TPC connections for each OpenSearch endpoint. */ export class OpenSearchClientPool { - // LRU cache + // LRU cache of client // key: data source endpoint - // value: OpenSearch client object | Legacy client object - private cache?: LRUCache; + // value: OpenSearch client | Legacy client + private clientCache?: LRUCache; + // LRU cache of aws clients + // key: endpoint + dataSourceId + lastUpdatedTime together to support update case. + // value: OpenSearch client | Legacy client + private awsClientCache?: LRUCache; private isClosed = false; constructor(private logger: Logger) {} - public async setup(config: DataSourcePluginConfigType): Promise { + public setup(config: DataSourcePluginConfigType): OpenSearchClientPoolSetup { const logger = this.logger; const { size } = config.clientPool; + const MAX_AGE = 15 * 60 * 1000; // by default, TCP connection times out in 15 minutes - this.cache = new LRUCache({ + this.clientCache = new LRUCache({ max: size, - maxAge: 15 * 60 * 1000, // by default, TCP connection times out in 15 minutes + maxAge: MAX_AGE, - async dispose(endpoint, client) { + async dispose(key, client) { try { await client.close(); } catch (error: any) { @@ -50,12 +56,34 @@ export class OpenSearchClientPool { }); this.logger.info(`Created data source client pool of size ${size}`); - const getClientFromPool = (endpoint: string) => { - return this.cache!.get(endpoint); + // aws client specific pool + this.awsClientCache = new LRUCache({ + max: size, + maxAge: MAX_AGE, + + async dispose(key, client) { + try { + await client.close(); + } catch (error: any) { + logger.warn( + `Error closing OpenSearch client when removing from aws client pool: ${error.message}` + ); + } + }, + }); + this.logger.info(`Created data source aws client pool of size ${size}`); + + const getClientFromPool = (key: string, authType: AuthType) => { + const selectedCache = authType === AuthType.SigV4 ? this.awsClientCache : this.clientCache; + + return selectedCache!.get(key); }; - const addClientToPool = (endpoint: string, client: Client | LegacyClient) => { - this.cache!.set(endpoint, client); + const addClientToPool = (key: string, authType: string, client: Client | LegacyClient) => { + const selectedCache = authType === AuthType.SigV4 ? this.awsClientCache : this.clientCache; + if (!selectedCache?.has(key)) { + return selectedCache!.set(key, client); + } }; return { @@ -71,7 +99,15 @@ export class OpenSearchClientPool { if (this.isClosed) { return; } - await Promise.all(this.cache!.values().map((client) => client.close())); - this.isClosed = true; + + try { + await Promise.all([ + ...this.clientCache!.values().map((client) => client.close()), + ...this.awsClientCache!.values().map((client) => client.close()), + ]); + this.isClosed = true; + } catch (error) { + this.logger.error(`Error closing clients in pool. ${error}`); + } } } diff --git a/src/plugins/data_source/server/client/configure_client.test.ts b/src/plugins/data_source/server/client/configure_client.test.ts index fa404416361..1499ccd411c 100644 --- a/src/plugins/data_source/server/client/configure_client.test.ts +++ b/src/plugins/data_source/server/client/configure_client.test.ts @@ -6,7 +6,12 @@ import { SavedObjectsClientContract } from '../../../../core/server'; import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; -import { DataSourceAttributes, AuthType } from '../../common/data_sources/types'; +import { + DataSourceAttributes, + AuthType, + UsernamePasswordTypedContent, + SigV4Content, +} from '../../common/data_sources/types'; import { DataSourcePluginConfigType } from '../../config'; import { ClientMock, parseClientOptionsMock } from './configure_client.test.mocks'; import { OpenSearchClientPoolSetup } from './client_pool'; @@ -31,6 +36,8 @@ describe('configureClient', () => { let dataSourceAttr: DataSourceAttributes; let dsClient: ReturnType; let dataSourceClientParams: DataSourceClientParams; + let usernamePasswordAuthContent: UsernamePasswordTypedContent; + let sigV4AuthContent: SigV4Content; beforeEach(() => { dsClient = opensearchClientMock.createInternalClient(); @@ -51,15 +58,24 @@ describe('configureClient', () => { rejectUnauthorized: true, }, } as ClientOptions; + + usernamePasswordAuthContent = { + username: 'username', + password: 'password', + }; + + sigV4AuthContent = { + region: 'us-east-1', + accessKey: 'accessKey', + secretKey: 'secretKey', + }; + dataSourceAttr = { title: 'title', endpoint: 'http://localhost', auth: { type: AuthType.UsernamePasswordType, - credentials: { - username: 'username', - password: 'password', - }, + credentials: usernamePasswordAuthContent, }, } as DataSourceAttributes; @@ -126,6 +142,48 @@ describe('configureClient', () => { expect(client).toBe(dsClient.child.mock.results[0].value); }); + test('configure client with auth.type == sigv4, will first call decodeAndDecrypt()', async () => { + savedObjectsMock.get.mockReset().mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: { + ...dataSourceAttr, + auth: { + type: AuthType.SigV4, + credentials: sigV4AuthContent, + }, + }, + references: [], + }); + + const decodeAndDecryptSpy = jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ + decryptedText: 'accessKey', + encryptionContext: { endpoint: 'http://localhost' }, + }); + await configureClient(dataSourceClientParams, clientPoolSetup, config, logger); + + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); + expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(2); + }); + + test('configure test client for non-exist datasource should not call saved object api, nor decode any credential', async () => { + const decodeAndDecryptSpy = jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ + decryptedText: 'password', + encryptionContext: { endpoint: 'http://localhost' }, + }); + const testClientParams: DataSourceClientParams = { + ...dataSourceClientParams, + testClientDataSourceAttr: dataSourceAttr, + dataSourceId: undefined, + }; + await configureClient(testClientParams, clientPoolSetup, config, logger); + + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(savedObjectsMock.get).not.toHaveBeenCalled(); + expect(decodeAndDecryptSpy).not.toHaveBeenCalled(); + }); + test('configure client with auth.type == username_password and password contaminated', async () => { const decodeAndDecryptSpy = jest .spyOn(cryptographyMock, 'decodeAndDecrypt') @@ -137,7 +195,7 @@ describe('configureClient', () => { configureClient(dataSourceClientParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).not.toHaveBeenCalled(); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); @@ -152,7 +210,7 @@ describe('configureClient', () => { configureClient(dataSourceClientParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).not.toHaveBeenCalled(); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/plugins/data_source/server/client/configure_client.ts b/src/plugins/data_source/server/client/configure_client.ts index c4d1b4eef9f..8b43ffa80b2 100644 --- a/src/plugins/data_source/server/client/configure_client.ts +++ b/src/plugins/data_source/server/client/configure_client.ts @@ -3,132 +3,142 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Client } from '@opensearch-project/opensearch'; -import { Logger, SavedObject, SavedObjectsClientContract } from '../../../../../src/core/server'; -import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; +import { Client, ClientOptions } from '@opensearch-project/opensearch'; +import { Client as LegacyClient } from 'elasticsearch'; +import { Credentials } from 'aws-sdk'; +import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws'; +import { Logger } from '../../../../../src/core/server'; import { AuthType, DataSourceAttributes, + SigV4Content, UsernamePasswordTypedContent, } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; import { CryptographyServiceSetup } from '../cryptography_service'; -import { createDataSourceError, DataSourceError } from '../lib/error'; +import { createDataSourceError } from '../lib/error'; import { DataSourceClientParams } from '../types'; import { parseClientOptions } from './client_config'; import { OpenSearchClientPoolSetup } from './client_pool'; +import { + getRootClient, + getAWSCredential, + getCredential, + getDataSource, + generateCacheKey, +} from './configure_client_utils'; export const configureClient = async ( - { dataSourceId, savedObjects, cryptography }: DataSourceClientParams, + { dataSourceId, savedObjects, cryptography, testClientDataSourceAttr }: DataSourceClientParams, openSearchClientPoolSetup: OpenSearchClientPoolSetup, config: DataSourcePluginConfigType, logger: Logger ): Promise => { - try { - const dataSource = await getDataSource(dataSourceId, savedObjects); - const rootClient = getRootClient(dataSource.attributes, config, openSearchClientPoolSetup); + let dataSource; + let requireDecryption = true; - return await getQueryClient(rootClient, dataSource, cryptography); + try { + // configure test client + if (testClientDataSourceAttr) { + const { + auth: { type, credentials }, + } = testClientDataSourceAttr; + // handle test connection case when changing non-credential field of existing data source + if ( + dataSourceId && + ((type === AuthType.UsernamePasswordType && !credentials?.password) || + (type === AuthType.SigV4 && !credentials?.accessKey && !credentials?.secretKey)) + ) { + dataSource = await getDataSource(dataSourceId, savedObjects); + } else { + dataSource = testClientDataSourceAttr; + requireDecryption = false; + } + } else { + dataSource = await getDataSource(dataSourceId!, savedObjects); + } + + const rootClient = getRootClient( + dataSource, + openSearchClientPoolSetup.getClientFromPool, + dataSourceId + ) as Client; + + return await getQueryClient( + dataSource, + openSearchClientPoolSetup.addClientToPool, + config, + cryptography, + rootClient, + dataSourceId, + requireDecryption + ); } catch (error: any) { - logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`); - logger.error(error); + logger.error( + `Failed to get data source client for dataSourceId: [${dataSourceId}]. ${error}: ${error.stack}` + ); // Re-throw as DataSourceError throw createDataSourceError(error); } }; -export const getDataSource = async ( - dataSourceId: string, - savedObjects: SavedObjectsClientContract -): Promise> => { - const dataSource = await savedObjects.get( - DATA_SOURCE_SAVED_OBJECT_TYPE, - dataSourceId - ); - return dataSource; -}; - -export const getCredential = async ( - dataSource: SavedObject, - cryptography: CryptographyServiceSetup -): Promise => { - const { endpoint } = dataSource.attributes!; - - const { username, password } = dataSource.attributes.auth.credentials!; - - const { decryptedText, encryptionContext } = await cryptography - .decodeAndDecrypt(password) - .catch((err: any) => { - // Re-throw as DataSourceError - throw createDataSourceError(err); - }); - - if (encryptionContext!.endpoint !== endpoint) { - throw new Error( - 'Data source "endpoint" contaminated. Please delete and create another data source.' - ); - } - - const credential = { - username, - password: decryptedText, - }; - - return credential; -}; - /** * Create a child client object with given auth info. * - * @param rootClient root client for the connection with given data source endpoint. - * @param dataSource data source saved object + * @param rootClient root client for the given data source. + * @param dataSourceAttr data source saved object attributes * @param cryptography cryptography service for password encryption / decryption - * @returns child client. + * @param config data source config + * @param addClientToPool function to add client to client pool + * @param dataSourceId id of data source saved Object + * @param requireDecryption false when creating test client before data source exists + * @returns Promise of query client */ const getQueryClient = async ( - rootClient: Client, - dataSource: SavedObject, - cryptography: CryptographyServiceSetup + dataSourceAttr: DataSourceAttributes, + addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void, + config: DataSourcePluginConfigType, + cryptography?: CryptographyServiceSetup, + rootClient?: Client, + dataSourceId?: string, + requireDecryption: boolean = true ): Promise => { - const authType = dataSource.attributes.auth.type; - - switch (authType) { + const { + auth: { type }, + endpoint, + } = dataSourceAttr; + const clientOptions = parseClientOptions(config, endpoint); + const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId); + + switch (type) { case AuthType.NoAuth: + if (!rootClient) rootClient = new Client(clientOptions); + addClientToPool(cacheKey, type, rootClient); + return rootClient.child(); case AuthType.UsernamePasswordType: - const credential = await getCredential(dataSource, cryptography); + const credential = requireDecryption + ? await getCredential(dataSourceAttr, cryptography!) + : (dataSourceAttr.auth.credentials as UsernamePasswordTypedContent); + + if (!rootClient) rootClient = new Client(clientOptions); + addClientToPool(cacheKey, type, rootClient); + return getBasicAuthClient(rootClient, credential); - default: - throw Error(`${authType} is not a supported auth type for data source`); - } -}; + case AuthType.SigV4: + const awsCredential = requireDecryption + ? await getAWSCredential(dataSourceAttr, cryptography!) + : (dataSourceAttr.auth.credentials as SigV4Content); -/** - * Gets a root client object of the OpenSearch endpoint. - * Will attempt to get from cache, if cache miss, create a new one and load into cache. - * - * @param dataSourceAttr data source saved objects attributes. - * @param config data source config - * @returns OpenSearch client for the given data source endpoint. - */ -const getRootClient = ( - dataSourceAttr: DataSourceAttributes, - config: DataSourcePluginConfigType, - { getClientFromPool, addClientToPool }: OpenSearchClientPoolSetup -): Client => { - const endpoint = dataSourceAttr.endpoint; - const cachedClient = getClientFromPool(endpoint); - if (cachedClient) { - return cachedClient as Client; - } else { - const clientOptions = parseClientOptions(config, endpoint); + const awsClient = rootClient ? rootClient : getAWSClient(awsCredential, clientOptions); + addClientToPool(cacheKey, type, awsClient); - const client = new Client(clientOptions); - addClientToPool(endpoint, client); + return awsClient; - return client; + default: + throw Error(`${type} is not a supported auth type for data source`); } }; @@ -148,3 +158,21 @@ const getBasicAuthClient = ( headers: { authorization: null }, }); }; + +const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): Client => { + const { accessKey, secretKey, region } = credential; + + const credentialProvider = (): Promise => { + return new Promise((resolve) => { + resolve(new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey })); + }); + }; + + return new Client({ + ...AwsSigv4Signer({ + region, + getCredentials: credentialProvider, + }), + ...clientOptions, + }); +}; diff --git a/src/plugins/data_source/server/client/configure_client_utils.ts b/src/plugins/data_source/server/client/configure_client_utils.ts new file mode 100644 index 00000000000..3ef8acc97b5 --- /dev/null +++ b/src/plugins/data_source/server/client/configure_client_utils.ts @@ -0,0 +1,146 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Client } from '@opensearch-project/opensearch'; +import { Client as LegacyClient } from 'elasticsearch'; +import { SavedObjectsClientContract } from '../../../../../src/core/server'; +import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; +import { + DataSourceAttributes, + UsernamePasswordTypedContent, + SigV4Content, + AuthType, +} from '../../common/data_sources'; +import { CryptographyServiceSetup } from '../cryptography_service'; +import { createDataSourceError } from '../lib/error'; + +/** + * Get the root client of datasource from + * client cache. If there's a cache miss, return undefined. + * + * @param dataSourceAttr data source saved objects attributes + * @param dataSourceId id of data source saved Object + * @param addClientToPool function to get client from client pool + * @returns cached OpenSearch client, or undefined if cache miss + */ +export const getRootClient = ( + dataSourceAttr: DataSourceAttributes, + getClientFromPool: (endpoint: string, authType: AuthType) => Client | LegacyClient | undefined, + dataSourceId?: string +): Client | LegacyClient | undefined => { + const { + auth: { type }, + lastUpdatedTime, + } = dataSourceAttr; + let cachedClient; + const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId); + + // return undefined when building SigV4 test client with new credentials + if (type === AuthType.SigV4) { + cachedClient = dataSourceId && lastUpdatedTime ? getClientFromPool(cacheKey, type) : undefined; + } else { + cachedClient = getClientFromPool(cacheKey, type); + } + + return cachedClient; +}; + +export const getDataSource = async ( + dataSourceId: string, + savedObjects: SavedObjectsClientContract +): Promise => { + const dataSourceSavedObject = await savedObjects.get( + DATA_SOURCE_SAVED_OBJECT_TYPE, + dataSourceId + ); + + const dataSourceAttr = { + ...dataSourceSavedObject.attributes, + lastUpdatedTime: dataSourceSavedObject.updated_at, + }; + + return dataSourceAttr; +}; + +export const getCredential = async ( + dataSource: DataSourceAttributes, + cryptography: CryptographyServiceSetup +): Promise => { + const { endpoint } = dataSource; + const { username, password } = dataSource.auth.credentials as UsernamePasswordTypedContent; + const { decryptedText, encryptionContext } = await cryptography.decodeAndDecrypt(password); + + if (encryptionContext!.endpoint !== endpoint) { + throw new Error( + 'Data source "endpoint" contaminated. Please delete and create another data source.' + ); + } + + const credential = { + username, + password: decryptedText, + }; + + return credential; +}; + +export const getAWSCredential = async ( + dataSource: DataSourceAttributes, + cryptography: CryptographyServiceSetup +): Promise => { + const { endpoint } = dataSource; + const { accessKey, secretKey, region } = dataSource.auth.credentials! as SigV4Content; + + const { + decryptedText: accessKeyText, + encryptionContext: accessKeyEncryptionContext, + } = await cryptography.decodeAndDecrypt(accessKey).catch((err: any) => { + // Re-throw as DataSourceError + throw createDataSourceError(err); + }); + + const { + decryptedText: secretKeyText, + encryptionContext: secretKeyEncryptionContext, + } = await cryptography.decodeAndDecrypt(secretKey).catch((err: any) => { + // Re-throw as DataSourceError + throw createDataSourceError(err); + }); + + if ( + accessKeyEncryptionContext.endpoint !== endpoint || + secretKeyEncryptionContext.endpoint !== endpoint + ) { + throw new Error( + 'Data source "endpoint" contaminated. Please delete and create another data source.' + ); + } + + const credential = { + region, + accessKey: accessKeyText, + secretKey: secretKeyText, + }; + + return credential; +}; + +export const generateCacheKey = (dataSourceAttr: DataSourceAttributes, dataSourceId?: string) => { + const CACHE_KEY_DELIMITER = ','; + const { + auth: { type }, + endpoint, + lastUpdatedTime, + } = dataSourceAttr; + // opensearch-js client doesn't support spawning child with aws sigv4 connection class, + // we are storing/getting the actual client instead of rootClient in/from aws client pool, + // by a key of ",," + const key = + type === AuthType.SigV4 + ? endpoint + CACHE_KEY_DELIMITER + dataSourceId + CACHE_KEY_DELIMITER + lastUpdatedTime + : endpoint; + + return key; +}; diff --git a/src/plugins/data_source/server/client/index.ts b/src/plugins/data_source/server/client/index.ts index f2784896507..9b6824dfa1d 100644 --- a/src/plugins/data_source/server/client/index.ts +++ b/src/plugins/data_source/server/client/index.ts @@ -4,4 +4,3 @@ */ export { OpenSearchClientPool, OpenSearchClientPoolSetup } from './client_pool'; -export { configureClient, getDataSource, getCredential } from './configure_client'; diff --git a/src/plugins/data_source/server/data_source_service.ts b/src/plugins/data_source/server/data_source_service.ts index 8466bb7e914..36a8d2a5ce5 100644 --- a/src/plugins/data_source/server/data_source_service.ts +++ b/src/plugins/data_source/server/data_source_service.ts @@ -3,16 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - Auditor, - LegacyCallAPIOptions, - Logger, - OpenSearchClient, -} from '../../../../src/core/server'; +import { LegacyCallAPIOptions, Logger, OpenSearchClient } from '../../../../src/core/server'; import { DataSourcePluginConfigType } from '../config'; -import { configureClient, OpenSearchClientPool } from './client'; +import { OpenSearchClientPool } from './client'; import { configureLegacyClient } from './legacy'; import { DataSourceClientParams } from './types'; +import { configureClient } from './client/configure_client'; export interface DataSourceServiceSetup { getDataSourceClient: (params: DataSourceClientParams) => Promise; @@ -38,8 +34,8 @@ export class DataSourceService { } async setup(config: DataSourcePluginConfigType): Promise { - const opensearchClientPoolSetup = await this.openSearchClientPool.setup(config); - const legacyClientPoolSetup = await this.legacyClientPool.setup(config); + const opensearchClientPoolSetup = this.openSearchClientPool.setup(config); + const legacyClientPoolSetup = this.legacyClientPool.setup(config); const getDataSourceClient = async ( params: DataSourceClientParams diff --git a/src/plugins/data_source/server/legacy/client_config.test.ts b/src/plugins/data_source/server/legacy/client_config.test.ts index 1b21eede35b..a15143ecf69 100644 --- a/src/plugins/data_source/server/legacy/client_config.test.ts +++ b/src/plugins/data_source/server/legacy/client_config.test.ts @@ -5,7 +5,7 @@ import { DataSourcePluginConfigType } from '../../config'; import { parseClientOptions } from './client_config'; -const TEST_DATA_SOURCE_ENDPOINT = 'http://datasource.com'; +const TEST_DATA_SOURCE_ENDPOINT = 'http://test.com/'; const config = { enabled: true, diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts index bfdf0ce585f..c047da70b28 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts @@ -168,7 +168,7 @@ describe('configureLegacyClient', () => { configureLegacyClient(dataSourceClientParams, callApiParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).not.toHaveBeenCalled(); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); @@ -183,7 +183,7 @@ describe('configureLegacyClient', () => { configureLegacyClient(dataSourceClientParams, callApiParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).not.toHaveBeenCalled(); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.ts index 94e6b40a4d2..3a9b65634a2 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.ts @@ -3,27 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Client } from 'elasticsearch'; +import { Client } from '@opensearch-project/opensearch'; +import { Client as LegacyClient, ConfigOptions } from 'elasticsearch'; +import { Credentials } from 'aws-sdk'; import { get } from 'lodash'; +import HttpAmazonESConnector from 'http-aws-es'; +import { Config } from 'aws-sdk'; import { Headers, LegacyAPICaller, LegacyCallAPIOptions, LegacyOpenSearchErrorHelpers, Logger, - SavedObject, } from '../../../../../src/core/server'; import { AuthType, DataSourceAttributes, + SigV4Content, UsernamePasswordTypedContent, } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; import { CryptographyServiceSetup } from '../cryptography_service'; import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types'; -import { OpenSearchClientPoolSetup, getCredential, getDataSource } from '../client'; +import { OpenSearchClientPoolSetup } from '../client'; import { parseClientOptions } from './client_config'; import { createDataSourceError, DataSourceError } from '../lib/error'; +import { + getRootClient, + getAWSCredential, + getCredential, + getDataSource, + generateCacheKey, +} from '../client/configure_client_utils'; export const configureLegacyClient = async ( { dataSourceId, savedObjects, cryptography }: DataSourceClientParams, @@ -33,13 +44,26 @@ export const configureLegacyClient = async ( logger: Logger ) => { try { - const dataSource = await getDataSource(dataSourceId, savedObjects); - const rootClient = getRootClient(dataSource.attributes, config, openSearchClientPoolSetup); + const dataSourceAttr = await getDataSource(dataSourceId!, savedObjects); + const rootClient = getRootClient( + dataSourceAttr, + openSearchClientPoolSetup.getClientFromPool, + dataSourceId + ) as LegacyClient; - return await getQueryClient(rootClient, dataSource, cryptography, callApiParams); + return await getQueryClient( + dataSourceAttr, + cryptography, + callApiParams, + openSearchClientPoolSetup.addClientToPool, + config, + rootClient, + dataSourceId + ); } catch (error: any) { - logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`); - logger.error(error); + logger.error( + `Failed to get data source client for dataSourceId: [${dataSourceId}]. ${error}: ${error.stack}` + ); // Re-throw as DataSourceError throw createDataSourceError(error); } @@ -49,57 +73,62 @@ export const configureLegacyClient = async ( * With given auth info, wrap the rootClient and return * * @param rootClient root client for the connection with given data source endpoint. - * @param dataSource data source saved object + * @param dataSourceAttr data source saved object attributes * @param cryptography cryptography service for password encryption / decryption + * @param config data source config + * @param addClientToPool function to add client to client pool + * @param dataSourceId id of data source saved Object * @returns child client. */ const getQueryClient = async ( - rootClient: Client, - dataSource: SavedObject, + dataSourceAttr: DataSourceAttributes, cryptography: CryptographyServiceSetup, - { endpoint, clientParams, options }: LegacyClientCallAPIParams + { endpoint, clientParams, options }: LegacyClientCallAPIParams, + addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void, + config: DataSourcePluginConfigType, + rootClient?: LegacyClient, + dataSourceId?: string ) => { - const authType = dataSource.attributes.auth.type; + const { + auth: { type }, + endpoint: nodeUrl, + } = dataSourceAttr; + const clientOptions = parseClientOptions(config, nodeUrl); + const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId); - switch (authType) { + switch (type) { case AuthType.NoAuth: + if (!rootClient) rootClient = new LegacyClient(clientOptions); + addClientToPool(cacheKey, type, rootClient); + return await (callAPI.bind(null, rootClient) as LegacyAPICaller)( endpoint, clientParams, options ); + case AuthType.UsernamePasswordType: - const credential = await getCredential(dataSource, cryptography); + const credential = await getCredential(dataSourceAttr, cryptography); + + if (!rootClient) rootClient = new LegacyClient(clientOptions); + addClientToPool(cacheKey, type, rootClient); + return getBasicAuthClient(rootClient, { endpoint, clientParams, options }, credential); - default: - throw Error(`${authType} is not a supported auth type for data source`); - } -}; + case AuthType.SigV4: + const awsCredential = await getAWSCredential(dataSourceAttr, cryptography); -/** - * Gets a root client object of the OpenSearch endpoint. - * Will attempt to get from cache, if cache miss, create a new one and load into cache. - * - * @param dataSourceAttr data source saved objects attributes. - * @param config data source config - * @returns Legacy client for the given data source endpoint. - */ -const getRootClient = ( - dataSourceAttr: DataSourceAttributes, - config: DataSourcePluginConfigType, - { getClientFromPool, addClientToPool }: OpenSearchClientPoolSetup -): Client => { - const endpoint = dataSourceAttr.endpoint; - const cachedClient = getClientFromPool(endpoint); - if (cachedClient) { - return cachedClient as Client; - } else { - const configOptions = parseClientOptions(config, endpoint); - const client = new Client(configOptions); - addClientToPool(endpoint, client); - - return client; + const awsClient = rootClient ? rootClient : getAWSClient(awsCredential, clientOptions); + addClientToPool(cacheKey, type, awsClient); + + return await (callAPI.bind(null, awsClient) as LegacyAPICaller)( + endpoint, + clientParams, + options + ); + + default: + throw Error(`${type} is not a supported auth type for data source`); } }; @@ -113,7 +142,7 @@ const getRootClient = ( * make wrap401Errors default to false, because we don't want login pop-up from browser */ const callAPI = async ( - client: Client, + client: LegacyClient, endpoint: string, clientParams: Record = {}, options: LegacyCallAPIOptions = { wrap401Errors: false } @@ -153,7 +182,7 @@ const callAPI = async ( * @param options - Options that affect the way we call the API and process the result. */ const getBasicAuthClient = async ( - rootClient: Client, + rootClient: LegacyClient, { endpoint, clientParams = {}, options }: LegacyClientCallAPIParams, { username, password }: UsernamePasswordTypedContent ) => { @@ -164,3 +193,16 @@ const getBasicAuthClient = async ( return await (callAPI.bind(null, rootClient) as LegacyAPICaller)(endpoint, clientParams, options); }; + +const getAWSClient = (credential: SigV4Content, clientOptions: ConfigOptions): LegacyClient => { + const { accessKey, secretKey, region } = credential; + const client = new LegacyClient({ + connectionClass: HttpAmazonESConnector, + awsConfig: new Config({ + region, + credentials: new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey }), + }), + ...clientOptions, + }); + return client; +}; diff --git a/src/plugins/data_source/server/lib/error.ts b/src/plugins/data_source/server/lib/error.ts index 9a9866a072d..9d1108e13e1 100644 --- a/src/plugins/data_source/server/lib/error.ts +++ b/src/plugins/data_source/server/lib/error.ts @@ -11,9 +11,18 @@ import { OsdError } from '../../../opensearch_dashboards_utils/common'; export class DataSourceError extends OsdError { // must have statusCode to avoid route handler in search.ts to return 500 statusCode: number; - constructor(error: any, message?: string, statusCode?: number) { - message = message ? message : error.message; + constructor(error: any, context?: string, statusCode?: number) { + let message: string; + if (context) { + message = context; + } else if (isResponseError(error)) { + message = JSON.stringify(error.meta.body); + } else { + message = error.message; + } + super('Data Source Error: ' + message); + if (statusCode) { this.statusCode = statusCode; } else if (error.statusCode) { diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index 069bf1d0f45..e038a0f7685 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -29,6 +29,8 @@ import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { ensureRawRequest } from '../../../../src/core/server/http/router'; import { createDataSourceError } from './lib/error'; +import { registerTestConnectionRoute } from './routes/test_connection'; + export class DataSourcePlugin implements Plugin { private readonly logger: Logger; private readonly cryptographyService: CryptographyService; @@ -103,6 +105,9 @@ export class DataSourcePlugin implements Plugin createDataSourceError(e), }; diff --git a/src/plugins/data_source/server/routes/data_source_connection_validator.ts b/src/plugins/data_source/server/routes/data_source_connection_validator.ts new file mode 100644 index 00000000000..ecec07dafcc --- /dev/null +++ b/src/plugins/data_source/server/routes/data_source_connection_validator.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpenSearchClient } from 'opensearch-dashboards/server'; +import { createDataSourceError } from '../lib/error'; + +export class DataSourceConnectionValidator { + constructor(private readonly callDataCluster: OpenSearchClient) {} + + async validate() { + try { + return await this.callDataCluster.info(); + } catch (e) { + throw createDataSourceError(e); + } + } +} diff --git a/src/plugins/data_source/server/routes/test_connection.ts b/src/plugins/data_source/server/routes/test_connection.ts new file mode 100644 index 00000000000..b32148a5742 --- /dev/null +++ b/src/plugins/data_source/server/routes/test_connection.ts @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter, OpenSearchClient } from 'opensearch-dashboards/server'; +import { AuthType, DataSourceAttributes } from '../../common/data_sources'; +import { DataSourceConnectionValidator } from './data_source_connection_validator'; +import { DataSourceServiceSetup } from '../data_source_service'; +import { CryptographyServiceSetup } from '../cryptography_service'; + +export const registerTestConnectionRoute = ( + router: IRouter, + dataSourceServiceSetup: DataSourceServiceSetup, + cryptography: CryptographyServiceSetup +) => { + router.post( + { + path: '/internal/data-source-management/validate', + validate: { + body: schema.object({ + id: schema.maybe(schema.string()), + dataSourceAttr: schema.object({ + endpoint: schema.string(), + auth: schema.maybe( + schema.object({ + type: schema.oneOf([ + schema.literal(AuthType.UsernamePasswordType), + schema.literal(AuthType.NoAuth), + schema.literal(AuthType.SigV4), + ]), + credentials: schema.oneOf([ + schema.object({ + username: schema.string(), + password: schema.string(), + }), + schema.object({ + region: schema.string(), + accessKey: schema.string(), + secretKey: schema.string(), + }), + schema.literal(null), + ]), + }) + ), + }), + }), + }, + }, + async (context, request, response) => { + const { dataSourceAttr, id: dataSourceId } = request.body; + + try { + const dataSourceClient: OpenSearchClient = await dataSourceServiceSetup.getDataSourceClient( + { + savedObjects: context.core.savedObjects.client, + cryptography, + dataSourceId, + testClientDataSourceAttr: dataSourceAttr as DataSourceAttributes, + } + ); + const dsValidator = new DataSourceConnectionValidator(dataSourceClient); + + await dsValidator.validate(); + + return response.ok({ + body: { + success: true, + }, + }); + } catch (err) { + return response.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +}; diff --git a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts index 525923f4c57..6b79248d1a9 100644 --- a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts +++ b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts @@ -17,7 +17,12 @@ import { } from 'opensearch-dashboards/server'; import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; -import { AuthType } from '../../common/data_sources'; +import { + AuthType, + DataSourceAttributes, + SigV4Content, + UsernamePasswordTypedContent, +} from '../../common/data_sources'; import { EncryptionContext, CryptographyServiceSetup } from '../cryptography_service'; /** @@ -29,7 +34,7 @@ export class DataSourceSavedObjectsClientWrapper { /** * Describes the factory used to create instances of Saved Objects Client Wrappers - * for data source spcific operations such as credntials encryption + * for data source specific operations such as credentials encryption */ public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { const createWithCredentialsEncryption = async ( @@ -159,13 +164,14 @@ export class DataSourceSavedObjectsClientWrapper { }; case AuthType.UsernamePasswordType: // Signing the data source with endpoint - const encryptionContext = { - endpoint, + return { + ...attributes, + auth: await this.encryptBasicAuthCredential(auth, { endpoint }), }; - + case AuthType.SigV4: return { ...attributes, - auth: await this.encryptCredentials(auth, encryptionContext), + auth: await this.encryptSigV4Credential(auth, { endpoint }), }; default: throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${auth.type}'`); @@ -191,6 +197,8 @@ export class DataSourceSavedObjectsClientWrapper { } const { type, credentials } = auth; + const existingDataSourceAttr = await this.getDataSourceAttributes(wrapperOptions, id, options); + const encryptionContext = await this.getEncryptionContext(existingDataSourceAttr); switch (type) { case AuthType.NoAuth: @@ -204,18 +212,33 @@ export class DataSourceSavedObjectsClientWrapper { }; case AuthType.UsernamePasswordType: if (credentials?.password) { - // Fetch and validate existing signature - const encryptionContext = await this.validateEncryptionContext( - wrapperOptions, - id, - options - ); - + this.validateEncryptionContext(encryptionContext, existingDataSourceAttr); + return { + ...attributes, + auth: await this.encryptBasicAuthCredential(auth, encryptionContext), + }; + } else { + return attributes; + } + case AuthType.SigV4: + this.validateEncryptionContext(encryptionContext, existingDataSourceAttr); + if (credentials?.accessKey && credentials?.secretKey) { return { ...attributes, - auth: await this.encryptCredentials(auth, encryptionContext), + auth: await this.encryptSigV4Credential(auth, encryptionContext), }; } else { + if (credentials?.accessKey) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `Failed to update existing data source with auth type ${type}: "credentials.secretKey" missing.` + ); + } + + if (credentials?.secretKey) { + throw SavedObjectsErrorHelpers.createBadRequestError( + `Failed to update existing data source with auth type ${type}: "credentials.accessKey" missing.` + ); + } return attributes; } default: @@ -259,7 +282,7 @@ export class DataSourceSavedObjectsClientWrapper { ); } - const { username, password } = credentials; + const { username, password } = credentials as UsernamePasswordTypedContent; if (!username) { throw SavedObjectsErrorHelpers.createBadRequestError( @@ -272,36 +295,45 @@ export class DataSourceSavedObjectsClientWrapper { '"auth.credentials.password" attribute is required' ); } + break; + case AuthType.SigV4: + if (!credentials) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"auth.credentials" attribute is required' + ); + } + const { accessKey, secretKey, region } = credentials as SigV4Content; + + if (!accessKey) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"auth.credentials.accessKey" attribute is required' + ); + } + + if (!secretKey) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"auth.credentials.secretKey" attribute is required' + ); + } + + if (!region) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"auth.credentials.region" attribute is required' + ); + } break; default: throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`); } } - private async validateEncryptionContext( - wrapperOptions: SavedObjectsClientWrapperOptions, - id: string, - options: SavedObjectsUpdateOptions = {} - ) { - let attributes; - - try { - // Fetch existing data source by id - const savedObject = await wrapperOptions.client.get(DATA_SOURCE_SAVED_OBJECT_TYPE, id, { - namespace: options.namespace, - }); - attributes = savedObject.attributes; - } catch (err: any) { - const errMsg = `Failed to fetch existing data source for dataSourceId [${id}]`; - this.logger.error(errMsg); - this.logger.error(err); - throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); - } + private async getEncryptionContext(attributes: DataSourceAttributes) { + let encryptionContext: EncryptionContext; if (!attributes) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "attributes" missing. Please delete and create another data source.' + 'Failed to update existing data source: "attributes" missing. Please delete and create another data source.' ); } @@ -309,65 +341,109 @@ export class DataSourceSavedObjectsClientWrapper { if (!endpoint) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "endpoint" missing. Please delete and create another data source.' + 'Failed to update existing data source: "endpoint" missing. Please delete and create another data source.' ); } if (!auth) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "auth" missing. Please delete and create another data source.' + 'Failed to update existing data source: "auth" missing. Please delete and create another data source.' ); } switch (auth.type) { case AuthType.NoAuth: - // Signing the data source with exsiting endpoint - return { - endpoint, - }; + // Signing the data source with existing endpoint + encryptionContext = { endpoint }; + break; case AuthType.UsernamePasswordType: const { credentials } = auth; if (!credentials) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "credentials" missing. Please delete and create another data source.' + 'Failed to update existing data source: "credentials" missing. Please delete and create another data source.' ); } - const { username, password } = credentials; + const { username, password } = credentials as UsernamePasswordTypedContent; if (!username) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "auth.credentials.username" missing. Please delete and create another data source.' + 'Failed to update existing data source: "auth.credentials.username" missing. Please delete and create another data source.' ); } if (!password) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "auth.credentials.username" missing. Please delete and create another data source.' + 'Failed to update existing data source: "auth.credentials.password" missing. Please delete and create another data source.' ); } + encryptionContext = await this.getEncryptionContextFromCipher(password); + break; + case AuthType.SigV4: + const { accessKey, secretKey } = auth.credentials as SigV4Content; + const accessKeyEncryptionContext = await this.getEncryptionContextFromCipher(accessKey); + const secretKeyEncryptionContext = await this.getEncryptionContextFromCipher(secretKey); - const { encryptionContext } = await this.cryptography - .decodeAndDecrypt(password) - .catch((err: any) => { - const errMsg = `Failed to update existing data source for dataSourceId [${id}]: unable to decrypt "auth.credentials.password"`; - this.logger.error(errMsg); - this.logger.error(err); - throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); - }); - - if (encryptionContext.endpoint !== endpoint) { + if (accessKeyEncryptionContext.endpoint !== secretKeyEncryptionContext.endpoint) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Update failed due to deprecated data source: "endpoint" contaminated. Please delete and create another data source.' + 'Failed to update existing data source: encryption contexts for "auth.credentials.accessKey" and "auth.credentials.secretKey" must be same. Please delete and create another data source.' ); } - return encryptionContext; + encryptionContext = accessKeyEncryptionContext; + break; default: - throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`); + throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${auth.type}'`); + } + + return encryptionContext; + } + + private async getDataSourceAttributes( + wrapperOptions: SavedObjectsClientWrapperOptions, + id: string, + options: SavedObjectsUpdateOptions = {} + ): Promise { + try { + // Fetch existing data source by id + const savedObject = await wrapperOptions.client.get(DATA_SOURCE_SAVED_OBJECT_TYPE, id, { + namespace: options.namespace, + }); + return savedObject.attributes as DataSourceAttributes; + } catch (err: any) { + const errMsg = `Failed to fetch existing data source for dataSourceId [${id}]`; + this.logger.error(`${errMsg}: ${err} ${err.stack}`); + throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); + } + } + + private validateEncryptionContext = ( + encryptionContext: EncryptionContext, + dataSource: DataSourceAttributes + ) => { + // validate encryption context + if (encryptionContext.endpoint !== dataSource.endpoint) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'Failed to update existing data source: "endpoint" contaminated. Please delete and create another data source.' + ); } + }; + + private async getEncryptionContextFromCipher(cipher: string) { + const { encryptionContext } = await this.cryptography + .decodeAndDecrypt(cipher) + .catch((err: any) => { + const errMsg = `Failed to update existing data source: unable to decrypt auth content`; + this.logger.error(`${errMsg}: ${err} ${err.stack}`); + throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); + }); + + return encryptionContext; } - private async encryptCredentials(auth: T, encryptionContext: EncryptionContext) { + private async encryptBasicAuthCredential( + auth: T, + encryptionContext: EncryptionContext + ) { const { credentials: { username, password }, } = auth; @@ -380,4 +456,19 @@ export class DataSourceSavedObjectsClientWrapper { }, }; } + + private async encryptSigV4Credential(auth: T, encryptionContext: EncryptionContext) { + const { + credentials: { accessKey, secretKey, region }, + } = auth; + + return { + ...auth, + credentials: { + region, + accessKey: await this.cryptography.encryptAndEncode(accessKey, encryptionContext), + secretKey: await this.cryptography.encryptAndEncode(secretKey, encryptionContext), + }, + }; + } } diff --git a/src/plugins/data_source/server/types.ts b/src/plugins/data_source/server/types.ts index cf2748d046d..68a840ebbbc 100644 --- a/src/plugins/data_source/server/types.ts +++ b/src/plugins/data_source/server/types.ts @@ -8,6 +8,7 @@ import { OpenSearchClient, SavedObjectsClientContract, } from 'src/core/server'; +import { DataSourceAttributes } from '../common/data_sources'; import { CryptographyServiceSetup } from './cryptography_service'; import { DataSourceError } from './lib/error'; @@ -19,10 +20,13 @@ export interface LegacyClientCallAPIParams { } export interface DataSourceClientParams { - dataSourceId: string; - // this saved objects client is used to fetch data source on behalf of users, caller should pass scoped saved objects client + // to fetch data source on behalf of users, caller should pass scoped saved objects client savedObjects: SavedObjectsClientContract; cryptography: CryptographyServiceSetup; + // optional when creating test client, required for normal client + dataSourceId?: string; + // required when creating test client + testClientDataSourceAttr?: DataSourceAttributes; } export interface DataSourcePluginRequestContext { diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap b/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap deleted file mode 100644 index b4331e26f79..00000000000 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap +++ /dev/null @@ -1,1937 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Create Datasource Wizard case1: should load resources successfully should render normally 1`] = ` - - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

- - - Create a new data source connection to help you retrieve data from an external OpenSearch compatible source. - - -
-

-
-
-
-
- -
- -
- -
- - -
- -
-

- - - Connection Details - - -

-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
- - -
-

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
-
- -
- - - - - - -
- -
- - - - - -`; - -exports[`Datasource Management: Create Datasource Wizard case2: should fail to load resources should not render component and go back to listing page 1`] = ` - - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

- - - Create a new data source connection to help you retrieve data from an external OpenSearch compatible source. - - -
-

-
-
-
-
- -
- -
- -
- - -
- -
-

- - - Connection Details - - -

-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
- - -
-

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
-
- -
- - - - - - -
- -
- - - - - -`; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap deleted file mode 100644 index fcd197cc082..00000000000 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/__snapshots__/create_data_source_form.test.tsx.snap +++ /dev/null @@ -1,3605 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Create Datasource form should create data source with No Auth when all fields are valid 1`] = ` - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

- - - Create a new data source connection to help you retrieve data from an external OpenSearch compatible source. - - -
-

-
-
-
-
- -
- -
- -
- - -
- -
-

- - - Connection Details - - -

-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
- - -
-

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
- - - - - - -
- -
- - - - -`; - -exports[`Datasource Management: Create Datasource form should create data source with username & password when all fields are valid 1`] = ` - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

- - - Create a new data source connection to help you retrieve data from an external OpenSearch compatible source. - - -
-

-
-
-
-
- -
- -
- -
- - -
- -
-

- - - Connection Details - - -

-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
- - -
-

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
-
- -
- - - - - - -
- -
- - - - -`; - -exports[`Datasource Management: Create Datasource form should render normally 1`] = ` - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

- - - Create a new data source connection to help you retrieve data from an external OpenSearch compatible source. - - -
-

-
-
-
-
- -
- -
- -
- - -
- -
-

- - - Connection Details - - -

-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
- - -
-

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
-
- -
- - - - - - -
- -
- - - - -`; - -exports[`Datasource Management: Create Datasource form should throw validation error when title is not valid & remove error on update valid title 1`] = ` - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

- - - Create a new data source connection to help you retrieve data from an external OpenSearch compatible source. - - -
-

-
-
-
-
- -
- -
- -
- - -
- -
-

- - - Connection Details - - -

-
-
- -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
- -
- -
-
- - - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
- - -
-

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
-
- -
- - - - - - -
- -
- - - - -`; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx index 5ed8370afc3..bcd7fe6b9c4 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx @@ -20,11 +20,13 @@ const authTypeIdentifier = '[data-test-subj="createDataSourceFormAuthTypeSelect" const usernameIdentifier = '[data-test-subj="createDataSourceFormUsernameField"]'; const passwordIdentifier = '[data-test-subj="createDataSourceFormPasswordField"]'; const createButtonIdentifier = '[data-test-subj="createDataSourceButton"]'; +const testConnectionButtonIdentifier = '[data-test-subj="createDataSourceTestConnectionButton"]'; describe('Datasource Management: Create Datasource form', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); let component: ReactWrapper, React.Component<{}, {}, any>>; const mockSubmitHandler = jest.fn(); + const mockTestConnectionHandler = jest.fn(); const getFields = (comp: ReactWrapper, React.Component<{}, {}, any>>) => { return { @@ -66,6 +68,7 @@ describe('Datasource Management: Create Datasource form', () => { component = mount( wrapWithIntl( @@ -81,7 +84,8 @@ describe('Datasource Management: Create Datasource form', () => { /* Scenario 1: Should render the page normally*/ test('should render normally', () => { - expect(component).toMatchSnapshot(); + const testConnBtn = component.find(testConnectionButtonIdentifier).last(); + expect(testConnBtn.prop('disabled')).toBe(true); }); /* Scenario 2: submit without any input from user - should display validation error messages*/ @@ -117,7 +121,6 @@ describe('Datasource Management: Create Datasource form', () => { const { title, description, endpoint, username, password } = getFields(component); - expect(component).toMatchSnapshot(); expect(title.prop('isInvalid')).toBe(true); expect(description.prop('isInvalid')).toBe(undefined); expect(endpoint.prop('isInvalid')).toBe(false); @@ -142,9 +145,10 @@ describe('Datasource Management: Create Datasource form', () => { changeTextFieldValue(usernameIdentifier, 'test123'); changeTextFieldValue(passwordIdentifier, 'test123'); - findTestSubject(component, 'createDataSourceButton').simulate('click'); + findTestSubject(component, 'createDataSourceTestConnectionButton').simulate('click'); - expect(component).toMatchSnapshot(); + findTestSubject(component, 'createDataSourceButton').simulate('click'); + expect(mockTestConnectionHandler).toHaveBeenCalled(); expect(mockSubmitHandler).toHaveBeenCalled(); // should call submit as all fields are valid }); @@ -158,7 +162,6 @@ describe('Datasource Management: Create Datasource form', () => { findTestSubject(component, 'createDataSourceButton').simulate('click'); - expect(component).toMatchSnapshot(); expect(mockSubmitHandler).toHaveBeenCalled(); // should call submit as all fields are valid }); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index 429790231a5..f069d2a1f0f 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -8,6 +8,8 @@ import { EuiButton, EuiFieldPassword, EuiFieldText, + EuiFlexGroup, + EuiFlexItem, EuiForm, EuiFormRow, EuiPageContent, @@ -23,6 +25,7 @@ import { DataSourceAttributes, DataSourceManagementContextValue, UsernamePasswordTypedContent, + SigV4Content, } from '../../../../types'; import { Header } from '../header'; import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; @@ -37,6 +40,7 @@ import { isValidUrl } from '../../../utils'; export interface CreateDataSourceProps { existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; + handleTestConnection: (formValues: DataSourceAttributes) => void; } export interface CreateDataSourceState { /* Validation */ @@ -47,7 +51,7 @@ export interface CreateDataSourceState { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent; + credentials: UsernamePasswordTypedContent | SigV4Content; }; } @@ -117,20 +121,7 @@ export class CreateDataSourceForm extends React.Component< }; onChangeAuthType = (value: string) => { - const valueToSave = - value === AuthType.UsernamePasswordType ? AuthType.UsernamePasswordType : AuthType.NoAuth; - - const formErrorsByField = { - ...this.state.formErrorsByField, - createCredential: { ...this.state.formErrorsByField.createCredential }, - }; - if (valueToSave === AuthType.NoAuth) { - formErrorsByField.createCredential = { - username: [], - password: [], - }; - } - this.setState({ auth: { ...this.state.auth, type: valueToSave }, formErrorsByField }); + this.setState({ auth: { ...this.state.auth, type: value as AuthType } }); }; onChangeUsername = (e: { target: { value: any } }) => { @@ -177,14 +168,75 @@ export class CreateDataSourceForm extends React.Component< }); }; + onChangeRegion = (e: { target: { value: any } }) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, region: e.target.value }, + }, + }); + }; + + validateRegion = () => { + const isValid = !!this.state.auth.credentials.region?.trim().length; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + awsCredential: { + ...this.state.formErrorsByField.awsCredential, + region: isValid ? [] : [''], + }, + }, + }); + }; + + onChangeAccessKey = (e: { target: { value: any } }) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, accessKey: e.target.value }, + }, + }); + }; + + validateAccessKey = () => { + const isValid = !!this.state.auth.credentials.accessKey; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + awsCredential: { + ...this.state.formErrorsByField.awsCredential, + accessKey: isValid ? [] : [''], + }, + }, + }); + }; + + onChangeSecretKey = (e: { target: { value: any } }) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, secretKey: e.target.value }, + }, + }); + }; + + validateSecretKey = () => { + const isValid = !!this.state.auth.credentials.secretKey; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + awsCredential: { + ...this.state.formErrorsByField.awsCredential, + secretKey: isValid ? [] : [''], + }, + }, + }); + }; + onClickCreateNewDataSource = () => { if (this.isFormValid()) { - const formValues: DataSourceAttributes = { - title: this.state.title, - description: this.state.description, - endpoint: this.state.endpoint, - auth: { ...this.state.auth }, - }; + const formValues: DataSourceAttributes = this.getFormValues(); /* Remove credentials object for NoAuth */ if (this.state.auth.type === AuthType.NoAuth) { @@ -195,6 +247,37 @@ export class CreateDataSourceForm extends React.Component< } }; + onClickTestConnection = () => { + if (this.isFormValid()) { + /* Submit */ + this.props.handleTestConnection(this.getFormValues()); + } + }; + + getFormValues = (): DataSourceAttributes => { + let credentials = this.state.auth.credentials; + if (this.state.auth.type === AuthType.UsernamePasswordType) { + credentials = { + username: this.state.auth.credentials.username, + password: this.state.auth.credentials.password, + } as UsernamePasswordTypedContent; + } + if (this.state.auth.type === AuthType.SigV4) { + credentials = { + region: this.state.auth.credentials.region, + accessKey: this.state.auth.credentials.accessKey, + secretKey: this.state.auth.credentials.secretKey, + } as SigV4Content; + } + + return { + title: this.state.title, + description: this.state.description, + endpoint: this.state.endpoint, + auth: { ...this.state.auth, credentials }, + }; + }; + /* Render methods */ /* Render header*/ @@ -233,54 +316,133 @@ export class CreateDataSourceForm extends React.Component< }; /* Render create new credentials*/ - renderCreateNewCredentialsForm = () => { - return ( - <> - - - - - - - - ); + renderCreateNewCredentialsForm = (type: AuthType) => { + switch (type) { + case AuthType.UsernamePasswordType: + return ( + <> + + + + + + + + ); + case AuthType.SigV4: + return ( + <> + + + + + + + + + + + ); + + default: + break; + } }; renderContent = () => { @@ -404,23 +566,48 @@ export class CreateDataSourceForm extends React.Component< {/* Create New credentials */} {this.state.auth.type === AuthType.UsernamePasswordType - ? this.renderCreateNewCredentialsForm() + ? this.renderCreateNewCredentialsForm(this.state.auth.type) + : null} + + {this.state.auth.type === AuthType.SigV4 + ? this.renderCreateNewCredentialsForm(this.state.auth.type) : null} - {/* Create Data Source button*/} - - - + + + + {/* Test Connection button*/} + + + + + {/* Create Data Source button*/} + + + + + + + ); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx index 06084a41461..162af4c891f 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx @@ -46,9 +46,6 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); component.update(); }); - test('should render normally', () => { - expect(component).toMatchSnapshot(); - }); test('should create datasource successfully', async () => { spyOn(utils, 'createSingleDataSource').and.returnValue({}); @@ -72,6 +69,30 @@ describe('Datasource Management: Create Datasource Wizard', () => { component.update(); expect(utils.createSingleDataSource).toHaveBeenCalled(); }); + + test('should test connection to the endpoint successfully', async () => { + spyOn(utils, 'testConnection').and.returnValue({}); + + await act(async () => { + // @ts-ignore + await component.find('CreateDataSourceForm').first().prop('handleTestConnection')( + mockDataSourceAttributesWithAuth + ); + }); + expect(utils.testConnection).toHaveBeenCalled(); + }); + + test('should fail to test connection to the endpoint', async () => { + spyOn(utils, 'testConnection').and.throwError('error'); + await act(async () => { + // @ts-ignore + await component.find('CreateDataSourceForm').first().prop('handleTestConnection')( + mockDataSourceAttributesWithAuth + ); + }); + component.update(); + expect(utils.testConnection).toHaveBeenCalled(); + }); }); describe('case2: should fail to load resources', () => { beforeEach(async () => { @@ -96,7 +117,6 @@ describe('Datasource Management: Create Datasource Wizard', () => { component.update(); }); test('should not render component and go back to listing page', () => { - expect(component).toMatchSnapshot(); expect(history.push).toBeCalledWith(''); }); }); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx index 08ac198c756..83477b7a242 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx @@ -8,12 +8,16 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceManagementContext, DataSourceTableItem, ToastMessageItem } from '../../types'; +import { + DataSourceAttributes, + DataSourceManagementContext, + DataSourceTableItem, + ToastMessageItem, +} from '../../types'; import { getCreateBreadcrumbs } from '../breadcrumbs'; import { CreateDataSourceForm } from './components/create_form'; -import { createSingleDataSource, getDataSources } from '../utils'; +import { createSingleDataSource, getDataSources, testConnection } from '../utils'; import { LoadingMask } from '../loading_mask'; -import { DataSourceAttributes } from '../../types'; type CreateDataSourceWizardProps = RouteComponentProps; @@ -24,6 +28,7 @@ export const CreateDataSourceWizard: React.FunctionComponent().services; @@ -74,8 +79,34 @@ export const CreateDataSourceWizard: React.FunctionComponent { - toasts.addDanger(i18n.translate(id, { defaultMessage })); + /* Handle submit - create data source*/ + const handleTestConnection = async (attributes: DataSourceAttributes) => { + setIsLoading(true); + try { + await testConnection(http, attributes); + handleDisplayToastMessage({ + id: 'dataSourcesManagement.createDataSource.testConnectionSuccessMsg', + defaultMessage: + 'Connecting to the endpoint using the provided authentication method was successful.', + success: true, + }); + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.createDataSource.testConnectionFailMsg', + defaultMessage: + 'Failed Connecting to the endpoint using the provided authentication method.', + }); + } finally { + setIsLoading(false); + } + }; + + const handleDisplayToastMessage = ({ id, defaultMessage, success }: ToastMessageItem) => { + if (success) { + toasts.addSuccess(i18n.translate(id, { defaultMessage })); + } else { + toasts.addDanger(i18n.translate(id, { defaultMessage })); + } }; /* Render the creation wizard */ @@ -84,6 +115,7 @@ export const CreateDataSourceWizard: React.FunctionComponent {isLoading ? : null} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/__snapshots__/edit_data_source.test.tsx.snap b/src/plugins/data_source_management/public/components/edit_data_source/__snapshots__/edit_data_source.test.tsx.snap deleted file mode 100644 index a3d004457a7..00000000000 --- a/src/plugins/data_source_management/public/components/edit_data_source/__snapshots__/edit_data_source.test.tsx.snap +++ /dev/null @@ -1,1226 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Edit Datasource Wizard should load resources successfully should render normally 1`] = ` - - - -
- - -
- -
- -
-
- -

- create-test-ds -

-
- -
- -
-
- - -
- - - - - - - -
-
-
- -
- -
- -
- -
-

- - - Connection Details - - -

-
-
- -
-
- - -

- } - title={ -

- -

- } - > -
- -
- -
- -

- - - Object Details - - -

-
- -
- -
-

- - - This connection information is used for reference in tables and when adding to a data source connection - - -

-
-
-
-
-
-
- -
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
- -
-

- - - Endpoint - - -

-
-
- -
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
- -
- - -
- -
-

- - - Authentication - - -

-
-
- -
-
- - - - } - > -
- -
- -
- -

- - - Authentication Method - - -

-
-
-
- -
- -
-
- - - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- -
- -
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
- -
- - - - - -
-
-
-
-
-
-
-
- -
- -
- -
- -
- - -
- - -
- - - - -`; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/__snapshots__/edit_data_source_form.test.tsx.snap b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/__snapshots__/edit_data_source_form.test.tsx.snap deleted file mode 100644 index 1ce16bf131c..00000000000 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/__snapshots__/edit_data_source_form.test.tsx.snap +++ /dev/null @@ -1,2180 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Edit Datasource Form Case 1: With Username & Password should render normally 1`] = ` - - -
- -
- -
-
- -

- create-test-ds -

-
- -
- -
-
- - -
- - - - - - - -
-
-
- -
- -
- -
- -
-

- - - Connection Details - - -

-
-
- -
-
- - -

- } - title={ -

- -

- } - > -
- -
- -
- -

- - - Object Details - - -

-
- -
- -
-

- - - This connection information is used for reference in tables and when adding to a data source connection - - -

-
-
-
-
-
-
- -
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
- -
-

- - - Endpoint - - -

-
-
- -
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
- -
- - -
- -
-

- - - Authentication - - -

-
-
- -
-
- - - - } - > -
- -
- -
- -

- - - Authentication Method - - -

-
-
-
- -
- -
-
- - - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- -
- -
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
- -
- - - - - -
-
-
-
-
-
-
-
- -
- -
- -
- -
- - -
- - -
- - - -`; - -exports[`Datasource Management: Edit Datasource Form Case 2: With No Authentication should render normally 1`] = ` - - -
- -
- -
-
- -

- create-test-ds123 -

-
- -
- -
-
- - -
- - - - - - - -
-
-
- -
- -
- -
- -
-

- - - Connection Details - - -

-
-
- -
-
- - -

- } - title={ -

- -

- } - > -
- -
- -
- -

- - - Object Details - - -

-
- -
- -
-

- - - This connection information is used for reference in tables and when adding to a data source connection - - -

-
-
-
-
-
-
- -
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- - - - - - - - - - - } - labelType="label" - > -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - -
- -
-

- - - Endpoint - - -

-
-
- -
-
- -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
-
-
- -
- - -
- -
-

- - - Authentication - - -

-
-
- -
-
- - - - } - > -
- -
- -
- -

- - - Authentication Method - - -

-
-
-
- -
- -
-
- - - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
- -
- -
- -
- -
- -
- - -
- - -
- - - -`; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index bf63f9ff812..492e34e4e19 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -58,6 +58,7 @@ describe('Datasource Management: Edit Datasource Form', () => { existingDatasourceNamesList={existingDatasourceNamesList} onDeleteDataSource={mockFn} handleSubmit={mockFn} + handleTestConnection={mockFn} displayToastMessage={mockFn} /> ), @@ -72,7 +73,6 @@ describe('Datasource Management: Edit Datasource Form', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); // @ts-ignore expect(component.find({ name: titleFieldIdentifier }).first().props().value).toBe( mockDataSourceAttributesWithAuth.title @@ -230,6 +230,7 @@ describe('Datasource Management: Edit Datasource Form', () => { existingDatasourceNamesList={existingDatasourceNamesList} onDeleteDataSource={mockFn} handleSubmit={mockFn} + handleTestConnection={mockFn} displayToastMessage={mockFn} /> ), @@ -244,7 +245,6 @@ describe('Datasource Management: Edit Datasource Form', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); // @ts-ignore expect(component.find({ name: titleFieldIdentifier }).first().props().value).toBe( mockDataSourceAttributesWithNoAuth.title @@ -326,5 +326,14 @@ describe('Datasource Management: Edit Datasource Form', () => { }, 100) ); }); + + /* Test Connection */ + test('should test connection on click test connection button', async () => { + expect(component.find('Header').exists()).toBe(true); + // @ts-ignore + component.find('Header').first().prop('onClickTestConnection')(); + component.update(); + expect(mockFn).toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index bda8709cf50..af17eb2d694 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -29,6 +29,7 @@ import { credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, + SigV4Content, ToastMessageItem, UsernamePasswordTypedContent, } from '../../../../types'; @@ -40,11 +41,13 @@ import { performDataSourceFormValidation, } from '../../../validation'; import { UpdatePasswordModal } from '../update_password_modal'; +import { UpdateAwsCredentialModal } from '../update_aws_credential_modal'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; + handleTestConnection: (formValues: DataSourceAttributes) => void; onDeleteDataSource?: () => void; displayToastMessage: (info: ToastMessageItem) => void; } @@ -55,9 +58,10 @@ export interface EditDataSourceState { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent; + credentials: UsernamePasswordTypedContent | SigV4Content | undefined; }; showUpdatePasswordModal: boolean; + showUpdateAwsCredentialModal: boolean; showUpdateOptions: boolean; isLoading: boolean; } @@ -80,9 +84,13 @@ export class EditDataSourceForm extends React.Component { - const valueToSave = - value === AuthType.UsernamePasswordType ? AuthType.UsernamePasswordType : AuthType.NoAuth; - - const formErrorsByField = { - ...this.state.formErrorsByField, - createCredential: { ...this.state.formErrorsByField.createCredential }, - }; - if (valueToSave === AuthType.NoAuth) { - formErrorsByField.createCredential = { - username: [], - password: [], - }; - } - this.setState({ auth: { ...this.state.auth, type: valueToSave }, formErrorsByField }, () => { + this.setState({ auth: { ...this.state.auth, type: value as AuthType } }, () => { this.onChangeFormValues(); }); }; @@ -173,7 +176,10 @@ export class EditDataSourceForm extends React.Component { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, region: e.target.value } as SigV4Content, + }, + }); + }; + + validateRegion = () => { + const isValid = !!this.state.auth.credentials.region?.trim().length; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + awsCredential: { + ...this.state.formErrorsByField.awsCredential, + region: isValid ? [] : [''], + }, + }, + }); + }; + + onChangeAccessKey = (e: { target: { value: any } }) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, accessKey: e.target.value } as SigV4Content, + }, + }); + }; + + validateAccessKey = () => { + const isValid = !!this.state.auth.credentials.accessKey; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + awsCredential: { + ...this.state.formErrorsByField.awsCredential, + accessKey: isValid ? [] : [''], + }, + }, + }); + }; + + onChangeSecretKey = (e: { target: { value: any } }) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { ...this.state.auth.credentials, secretKey: e.target.value } as SigV4Content, + }, + }); + }; + + validateSecretKey = () => { + const isValid = !!this.state.auth.credentials.secretKey; + this.setState({ + formErrorsByField: { + ...this.state.formErrorsByField, + awsCredential: { + ...this.state.formErrorsByField.awsCredential, + secretKey: isValid ? [] : [''], + }, }, }); }; @@ -220,18 +295,36 @@ export class EditDataSourceForm extends React.Component { + this.setState({ isLoading: true }); + const isNewCredential = !!(this.state.auth.type !== this.props.existingDataSource.auth.type); + + let credentials = this.state.auth.credentials; + + switch (this.state.auth.type) { + case AuthType.UsernamePasswordType: + credentials = { + username: this.state.auth.credentials?.username, + password: isNewCredential ? this.state.auth.credentials?.password : '', + } as UsernamePasswordTypedContent; + break; + case AuthType.SigV4: + credentials = { + region: this.state.auth.credentials?.region, + accessKey: isNewCredential ? this.state.auth.credentials?.accessKey : '', + secretKey: isNewCredential ? this.state.auth.credentials?.secretKey : '', + } as SigV4Content; + break; + case AuthType.NoAuth: + credentials = undefined; + break; + + default: + break; + } + + const formValues: DataSourceAttributes = { + title: this.state.title, + description: this.state.description, + endpoint: this.state.endpoint, + auth: { ...this.state.auth, credentials }, + }; + + try { + await this.props.handleTestConnection(formValues); + + this.props.displayToastMessage({ + id: 'dataSourcesManagement.editDataSource.testConnectionSuccessMsg', + defaultMessage: + 'Connecting to the endpoint using the provided authentication method was successful.', + success: true, + }); + } catch (e) { + this.props.displayToastMessage({ + id: 'dataSourcesManagement.editDataSource.testConnectionFailMsg', + defaultMessage: + 'Failed Connecting to the endpoint using the provided authentication method.', + }); + } finally { + this.setState({ isLoading: false }); + } + }; + onChangeFormValues = () => { setTimeout(() => { this.didFormValuesChange(); @@ -262,6 +410,10 @@ export class EditDataSourceForm extends React.Component { + this.setState({ showUpdateAwsCredentialModal: true }); + }; + /* Update password */ updatePassword = async (password: string) => { const { title, description, auth } = this.props.existingDataSource; @@ -274,13 +426,46 @@ export class EditDataSourceForm extends React.Component { + const { title, description, auth } = this.props.existingDataSource; + const updateAttributes: DataSourceAttributes = { + title, + description, + endpoint: undefined, + auth: { + type: auth.type, + credentials: { + region: auth.credentials ? auth.credentials.region : '', + accessKey, + secretKey, + } as SigV4Content, + }, + }; + this.closeAwsCredentialModal(); + + try { + await this.props.handleSubmit(updateAttributes); this.props.displayToastMessage({ id: 'dataSourcesManagement.editDataSource.updatePasswordSuccessMsg', defaultMessage: 'Password updated successfully.', @@ -296,11 +481,15 @@ export class EditDataSourceForm extends React.Component { this.setState({ showUpdatePasswordModal: false }); }; + closeAwsCredentialModal = () => { + this.setState({ showUpdateAwsCredentialModal: false }); + }; + renderUpdatePasswordModal = () => { return ( <> @@ -326,13 +515,42 @@ export class EditDataSourceForm extends React.Component ); }; + + renderUpdateAwsCredentialModal = () => { + return ( + <> + + { + + } + + + {this.state.showUpdateAwsCredentialModal ? ( + + ) : null} + + ); + }; + /* Render header*/ renderHeader = () => { return (
); }; @@ -530,8 +748,106 @@ export class EditDataSourceForm extends React.Component + {this.renderSelectedAuthType(this.state.auth.type)} + + ); + }; + + renderSelectedAuthType = (type: AuthType) => { + switch (type) { + case AuthType.UsernamePasswordType: + return this.renderUsernamePasswordFields(); + case AuthType.SigV4: + return this.renderSigV4ContentFields(); + default: + return null; + } + }; - {this.state.auth.type !== AuthType.NoAuth ? this.renderUsernamePasswordFields() : null} + renderSigV4ContentFields = () => { + return ( + <> + + + + + + + + + + + {this.props.existingDataSource.auth.type === AuthType.SigV4 + ? this.renderUpdateAwsCredentialModal() + : null} ); }; @@ -555,7 +871,7 @@ export class EditDataSourceForm extends React.Component - {this.props.existingDataSource.auth.type !== AuthType.NoAuth ? ( + {this.props.existingDataSource.auth.type === AuthType.UsernamePasswordType ? ( {this.renderUpdatePasswordModal()} ) : null} @@ -613,12 +930,17 @@ export class EditDataSourceForm extends React.Component -
- -
- -
-
- -

- testTest20 -

-
- -
- -
-
- - -
- -
- -
- -`; - -exports[`Datasource Management: Edit Datasource Header show delete icon should render normally 1`] = ` - -
- -
- -
-
- -

- testTest20 -

-
- -
- -
-
- - -
- - - - - - - -
-
-
- -
-
-`; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx index 36a3551d9ad..f679a7db6e6 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx @@ -26,8 +26,10 @@ describe('Datasource Management: Edit Datasource Header', () => { component = mount( wrapWithIntl(
), @@ -41,7 +43,6 @@ describe('Datasource Management: Edit Datasource Header', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); expect(component.find(headerTitleIdentifier).last().text()).toBe(dataSourceName); }); test('should show confirm delete modal pop up on trash icon click and cancel button work normally', () => { @@ -76,8 +77,10 @@ describe('Datasource Management: Edit Datasource Header', () => { component = mount( wrapWithIntl(
), @@ -90,7 +93,6 @@ describe('Datasource Management: Edit Datasource Header', () => { ); }); test('should render normally', () => { - expect(component).toMatchSnapshot(); expect(component.find(headerTitleIdentifier).last().text()).toBe(dataSourceName); expect(component.find(deleteIconIdentifier).exists()).toBe(false); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx index 8a73bcccc27..49c100b7ec9 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx @@ -13,6 +13,7 @@ import { EuiToolTip, EuiButtonIcon, EuiConfirmModal, + EuiButton, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; @@ -21,11 +22,15 @@ import { DataSourceManagementContext } from '../../../../types'; export const Header = ({ showDeleteIcon, + isFormValid, onClickDeleteIcon, + onClickTestConnection, dataSourceName, }: { showDeleteIcon: boolean; + isFormValid: boolean; onClickDeleteIcon: () => void; + onClickTestConnection: () => void; dataSourceName: string; }) => { /* State Variables */ @@ -105,9 +110,28 @@ export const Header = ({ ); }; + const renderTestConnectionButton = () => { + return ( + { + onClickTestConnection(); + }} + data-test-subj="datasource-edit-testConnectionButton" + > + + + ); + }; return ( + {/* Title */}
@@ -116,7 +140,16 @@ export const Header = ({
- {showDeleteIcon ? renderDeleteButton() : null} + + {/* Right side buttons */} + + + {/* Test connection button */} + {renderTestConnectionButton()} + {/* Delete icon button */} + {showDeleteIcon ? renderDeleteButton() : null} + +
); }; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/index.ts b/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/index.ts new file mode 100644 index 00000000000..449bdb63a00 --- /dev/null +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { UpdateAwsCredentialModal } from './update_aws_credential_modal'; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.tsx new file mode 100644 index 00000000000..5997a0bae38 --- /dev/null +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_aws_credential_modal/update_aws_credential_modal.tsx @@ -0,0 +1,183 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldPassword, + EuiForm, + EuiFormRow, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; + +export interface UpdateAwsCredentialModalProps { + region: string; + handleUpdateAwsCredential: (accessKey: string, secretKey: string) => void; + closeUpdateAwsCredentialModal: () => void; +} + +export const UpdateAwsCredentialModal = ({ + region, + handleUpdateAwsCredential, + closeUpdateAwsCredentialModal, +}: UpdateAwsCredentialModalProps) => { + /* State Variables */ + const [newAccessKey, setNewAccessKey] = useState(''); + const [isNewAccessKeyValid, setIsNewAccessKeyValid] = useState(true); + + const [newSecretKey, setNewSecretKey] = useState(''); + const [isNewSecretKeyValid, setIsNewSecretKeyValid] = useState(true); + + const onClickUpdateAwsCredential = () => { + if (isFormValid()) { + handleUpdateAwsCredential(newAccessKey, newSecretKey); + } + }; + + const isFormValid = () => { + return !!(newAccessKey && newSecretKey); + }; + + const validateNewAccessKey = () => { + setIsNewAccessKeyValid(!!newAccessKey); + }; + + const validateNewSecretKey = () => { + setIsNewSecretKeyValid(!!newSecretKey); + }; + + const renderUpdateAwsCredentialModal = () => { + return ( + + + +

+ { + + } +

+
+
+ + + + + { + + } + + + + + + {/* Region */} + + + {region} + + + + {/* updated access key */} + + setNewAccessKey(e.target.value)} + onBlur={validateNewAccessKey} + /> + + + {/* updated secret key */} + + setNewSecretKey(e.target.value)} + onBlur={validateNewSecretKey} + /> + + + + + + + { + + } + + + {i18n.translate('dataSourcesManagement.editDataSource.updateStoredAwsCredential', { + defaultMessage: 'Update stored aws credential', + })} + + +
+ ); + }; + + /* Return the modal */ + return
{renderUpdateAwsCredentialModal()}
; +}; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap index ef19172398e..7c5de9b6ba2 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap @@ -142,6 +142,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma id="generated-id" name="updatedPassword" placeholder="Updated password" + spellcheck="false" type="password" value="" /> @@ -205,6 +206,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma id="generated-id" name="confirmUpdatedPassword" placeholder="Confirm Updated password" + spellcheck="false" type="password" value="" /> @@ -529,6 +531,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Updated password" + spellCheck={false} type="dual" value="" > @@ -566,6 +569,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Updated password" + spellCheck={false} type="password" value="" /> @@ -691,6 +695,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Confirm Updated password" + spellCheck={false} type="dual" value="" > @@ -728,6 +733,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Confirm Updated password" + spellCheck={false} type="password" value="" /> diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx index 95ca0abe6e9..693a8a84234 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/update_password_modal.tsx @@ -125,6 +125,7 @@ export const UpdatePasswordModal = ({ type={'dual'} value={newPassword} isInvalid={!isNewPasswordValid} + spellCheck={false} onChange={(e) => setNewPassword(e.target.value)} onBlur={validateNewPassword} /> @@ -149,6 +150,7 @@ export const UpdatePasswordModal = ({ type={'dual'} value={confirmNewPassword} isInvalid={!!isConfirmNewPasswordValid.length} + spellCheck={false} onChange={(e) => setConfirmNewPassword(e.target.value)} onBlur={validateConfirmNewPassword} /> diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx index d11f8c8bc9d..5f6e823e0f8 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx @@ -85,7 +85,6 @@ describe('Datasource Management: Edit Datasource Wizard', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); expect(component.find(notFoundIdentifier).exists()).toBe(false); expect(utils.getDataSources).toHaveBeenCalled(); expect(utils.getDataSourceById).toHaveBeenCalled(); @@ -136,5 +135,14 @@ describe('Datasource Management: Edit Datasource Wizard', () => { component.update(); expect(utils.deleteDataSourceById).toHaveBeenCalled(); }); + test('should test connection', () => { + spyOn(utils, 'testConnection'); + // @ts-ignore + component.find('EditDataSourceForm').first().prop('handleTestConnection')( + mockDataSourceAttributesWithAuth + ); + component.update(); + expect(utils.testConnection).toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx index 9bbaecfccce..bc2bac5b66b 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx @@ -15,6 +15,7 @@ import { deleteDataSourceById, getDataSourceById, getDataSources, + testConnection, updateDataSourceById, } from '../utils'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -39,6 +40,7 @@ export const EditDataSource: React.FunctionComponent().services; const dataSourceID: string = props.match.params.id; @@ -110,6 +112,11 @@ export const EditDataSource: React.FunctionComponent { + await testConnection(http, attributes, dataSourceID); + }; + /* Render the edit wizard */ const renderContent = () => { if (!isLoading && (!dataSource || !dataSource.id)) { @@ -124,6 +131,7 @@ export const EditDataSource: React.FunctionComponent ) : null} {isLoading || !dataSource?.endpoint ? : null} diff --git a/src/plugins/data_source_management/public/components/utils.test.ts b/src/plugins/data_source_management/public/components/utils.test.ts index 7aeb00e14f7..a94d5b2260e 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -10,6 +10,7 @@ import { getDataSourceById, getDataSources, isValidUrl, + testConnection, updateDataSourceById, } from './utils'; import { coreMock } from '../../../../core/public/mocks'; @@ -23,6 +24,7 @@ import { mockResponseForSavedObjectsCalls, } from '../mocks'; import { AuthType } from '../types'; +import { HttpStart } from 'opensearch-dashboards/public'; const { savedObjects } = coreMock.createStart(); @@ -139,6 +141,51 @@ describe('DataSourceManagement: Utils.ts', () => { }); }); + describe('Test connection to the endpoint of the data source - success', () => { + let http: jest.Mocked; + const mockSuccess = jest.fn().mockResolvedValue({ body: { success: true } }); + const mockError = jest.fn().mockRejectedValue(null); + beforeEach(() => { + http = coreMock.createStart().http; + http.post.mockResolvedValue(mockSuccess); + }); + test('Success: Test Connection to the endpoint while creating a new data source', async () => { + await testConnection(http, getDataSourceByIdWithoutCredential.attributes); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/data-source-management/validate", + Object { + "body": "{\\"dataSourceAttr\\":{\\"endpoint\\":\\"https://test.com\\",\\"auth\\":{\\"type\\":\\"no_auth\\"}}}", + }, + ], + ] + `); + }); + + test('Success: Test Connection to the endpoint while existing data source is updated', async () => { + await testConnection(http, getDataSourceByIdWithoutCredential.attributes, 'test1234'); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/data-source-management/validate", + Object { + "body": "{\\"id\\":\\"test1234\\",\\"dataSourceAttr\\":{\\"endpoint\\":\\"https://test.com\\",\\"auth\\":{\\"type\\":\\"no_auth\\"}}}", + }, + ], + ] + `); + }); + test('failure: Test Connection to the endpoint while creating/updating a data source', async () => { + try { + http.post.mockRejectedValue(mockError); + await testConnection(http, getDataSourceByIdWithoutCredential.attributes, 'test1234'); + } catch (e) { + expect(e).toBeTruthy(); + } + }); + }); + describe('Delete multiple data sources by id', () => { test('Success: deleting multiple data source', async () => { try { diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 51f190be1ba..539edbca970 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectsClientContract } from 'src/core/public'; -import { DataSourceTableItem, DataSourceAttributes } from '../types'; +import { HttpStart, SavedObjectsClientContract } from 'src/core/public'; +import { AuthType, DataSourceAttributes, DataSourceTableItem } from '../types'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient @@ -79,6 +79,27 @@ export async function deleteMultipleDataSources( ); } +export async function testConnection( + http: HttpStart, + { endpoint, auth: { type, credentials } }: DataSourceAttributes, + dataSourceID?: string +) { + const query: any = { + id: dataSourceID, + dataSourceAttr: { + endpoint, + auth: { + type, + credentials, + }, + }, + }; + + await http.post(`/internal/data-source-management/validate`, { + body: JSON.stringify(query), + }); +} + export const isValidUrl = (endpoint: string) => { try { const url = new URL(endpoint); diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts index 0e861f1184f..1abde2d54ed 100644 --- a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts @@ -16,6 +16,11 @@ export interface CreateEditDataSourceValidation { username: string[]; password: string[]; }; + awsCredential: { + region: string[]; + accessKey: string[]; + secretKey: string[]; + }; } export const defaultValidation: CreateEditDataSourceValidation = { @@ -25,6 +30,11 @@ export const defaultValidation: CreateEditDataSourceValidation = { username: [], password: [], }, + awsCredential: { + region: [], + accessKey: [], + secretKey: [], + }, }; export const isTitleValid = ( @@ -84,6 +94,23 @@ export const performDataSourceFormValidation = ( return false; } } + /* AWS SigV4 Content */ + if (formValues?.auth?.type === AuthType.SigV4) { + /* Access key */ + if (!formValues.auth.credentials?.accessKey) { + return false; + } + + /* Secret key */ + if (!formValues.auth.credentials?.secretKey) { + return false; + } + + /* Region */ + if (!formValues.auth.credentials?.region) { + return false; + } + } return true; }; diff --git a/src/plugins/data_source_management/public/management_app/compoenent/experimental_callout/experimental_callout.tsx b/src/plugins/data_source_management/public/management_app/compoenent/experimental_callout/experimental_callout.tsx index 23f0d45a320..abe03c07d58 100644 --- a/src/plugins/data_source_management/public/management_app/compoenent/experimental_callout/experimental_callout.tsx +++ b/src/plugins/data_source_management/public/management_app/compoenent/experimental_callout/experimental_callout.tsx @@ -26,7 +26,7 @@ export const ExperimentalCallout = ({ docLinks }: { docLinks: DocLinksStart }) = defaultMessage="This feature is experimental and should not be used in a production environment. Any index patterns, visualization, and observability panels will be impacted if the feature is deactivated. For more information see " /> } - + { ({ buckets: [], error: '', exists: 1, total: true, columns: [] })), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 1 })), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 157cb88e782..e807267435e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -40,6 +40,10 @@ import { getFieldTypeName } from './lib/get_field_type_name'; import './discover_field.scss'; export interface DiscoverFieldProps { + /** + * the selected columns displayed in the doc table in discover + */ + columns: string[]; /** * The displayed field */ @@ -76,6 +80,7 @@ export interface DiscoverFieldProps { } export function DiscoverField({ + columns, field, indexPattern, onAddField, @@ -228,9 +233,10 @@ export function DiscoverField({ {infoIsOpen && ( )} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx index 1f1af8e9133..6a4dbe295e5 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx @@ -68,7 +68,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) { title={ bucket.display === '' ? emptyTxt - : `${bucket.display}: ${bucket.count} (${bucket.percent}%)` + : `${bucket.display}: ${bucket.count} (${bucket.percent.toFixed(1)}%)` } size="xs" className="eui-textTruncate" @@ -78,7 +78,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) { - {bucket.percent}% + {bucket.percent.toFixed(1)}% diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index c57300f3032..63d5c7ace30 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -29,15 +29,26 @@ */ import React from 'react'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { act } from '@testing-library/react'; // @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { DiscoverFieldDetails } from './discover_field_details'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternField } from '../../../../../data/public'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +const mockGetHref = jest.fn(); +const mockGetTriggerCompatibleActions = jest.fn(); + +jest.mock('../../../opensearch_dashboards_services', () => ({ + getUiActions: () => ({ + getTriggerCompatibleActions: mockGetTriggerCompatibleActions, + }), +})); + const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -48,17 +59,187 @@ const indexPattern = getStubIndexPattern( describe('discover sidebar field details', function () { const defaultProps = { + columns: [], + details: { buckets: [], error: '', exists: 1, total: 1 }, indexPattern, - details: { buckets: [], error: '', exists: 1, total: true, columns: [] }, onAddFilter: jest.fn(), }; - function mountComponent(field: IndexPatternField) { - const compProps = { ...defaultProps, field }; + beforeEach(() => { + mockGetHref.mockReturnValue('/foo/bar'); + mockGetTriggerCompatibleActions.mockReturnValue([ + { + getHref: mockGetHref, + }, + ]); + }); + + function mountComponent(field: IndexPatternField, props?: Record) { + const compProps = { ...defaultProps, ...props, field }; return mountWithIntl(); } - it('should enable the visualize link for a number field', function () { + it('should render buckets if they exist', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const buckets = [1, 2, 3].map((n) => ({ + display: `display-${n}`, + value: `value-${n}`, + percent: 25, + count: 100, + })); + const comp = mountComponent(visualizableField, { + details: { ...defaultProps.details, buckets }, + }); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe( + buckets.length + ); + // Visualize link should not be rendered until async hook update + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + + // Complete async hook + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe( + buckets.length + ); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should only render buckets if they exist', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + + await act(async () => { + await nextTick(); + comp.update(); + }); + + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should render a details error', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const errText = 'Some error'; + const comp = mountComponent(visualizableField, { + details: { ...defaultProps.details, error: errText }, + }); + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').text()).toBe(errText); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should handle promise rejection from isFieldVisualizable', async function () { + mockGetTriggerCompatibleActions.mockRejectedValue(new Error('Async error')); + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + }); + + it('should handle promise rejection from getVisualizeHref', async function () { + mockGetHref.mockRejectedValue(new Error('Async error')); + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + }); + + it('should enable the visualize link for a number field', async function () { const visualizableField = new IndexPatternField( { name: 'bytes', @@ -73,10 +254,17 @@ describe('discover sidebar field details', function () { 'bytes' ); const comp = mountComponent(visualizableField); - expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); }); - it('should disable the visualize link for an _id field', function () { + it('should disable the visualize link for an _id field', async function () { + expect.assertions(1); const conflictField = new IndexPatternField( { name: '_id', @@ -91,10 +279,15 @@ describe('discover sidebar field details', function () { 'test' ); const comp = mountComponent(conflictField); - expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualize-_id').length).toBe(0); }); - it('should disable the visualize link for an unknown field', function () { + it('should disable the visualize link for an unknown field', async function () { const unknownField = new IndexPatternField( { name: 'test', @@ -109,6 +302,11 @@ describe('discover sidebar field details', function () { 'test' ); const comp = mountComponent(unknownField); - expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualize-test').length).toBe(0); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx index 1fbcebbc0c8..906c173ed07 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx @@ -43,16 +43,18 @@ import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import './discover_field_details.scss'; interface DiscoverFieldDetailsProps { + columns: string[]; + details: FieldDetails; field: IndexPatternField; indexPattern: IndexPattern; - details: FieldDetails; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; } export function DiscoverFieldDetails({ + columns, + details, field, indexPattern, - details, onAddFilter, }: DiscoverFieldDetailsProps) { const warnings = getWarnings(field); @@ -60,37 +62,37 @@ export function DiscoverFieldDetails({ const [visualizeLink, setVisualizeLink] = useState(''); useEffect(() => { - isFieldVisualizable(field, indexPattern.id, details.columns).then( - (flag) => { - setShowVisualizeLink(flag); - // get href only if Visualize button is enabled - getVisualizeHref(field, indexPattern.id, details.columns).then( - (uri) => { - if (uri) setVisualizeLink(uri); - }, - () => { - setVisualizeLink(''); - } - ); - }, - () => { - setShowVisualizeLink(false); + const checkIfVisualizable = async () => { + const visualizable = await isFieldVisualizable(field, indexPattern.id, columns).catch( + () => false + ); + + setShowVisualizeLink(visualizable); + if (visualizable) { + const href = await getVisualizeHref(field, indexPattern.id, columns).catch(() => ''); + setVisualizeLink(href || ''); } - ); - }, [field, indexPattern.id, details.columns]); + }; + checkIfVisualizable(); + }, [field, indexPattern.id, columns]); const handleVisualizeLinkClick = (event: React.MouseEvent) => { // regular link click. let the uiActions code handle the navigation and show popup if needed event.preventDefault(); - triggerVisualizeActions(field, indexPattern.id, details.columns); + triggerVisualizeActions(field, indexPattern.id, columns); }; return ( <> -
- {details.error && {details.error}} - {!details.error && ( -
+
+ {details.error && ( + + {details.error} + + )} + + {!details.error && details.buckets.length > 0 && ( +
{details.buckets.map((bucket: Bucket, idx: number) => ( )} - {showVisualizeLink && ( - <> + {showVisualizeLink && visualizeLink && ( +
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */} 0 && ( )} - +
)}
{!details.error && ( diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index f957b93a4cc..865aff59028 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -117,8 +117,8 @@ export function DiscoverSidebar({ ); const getDetailsByField = useCallback( - (ipField: IndexPatternField) => getDetails(ipField, hits, columns, selectedIndexPattern), - [hits, columns, selectedIndexPattern] + (ipField: IndexPatternField) => getDetails(ipField, hits, selectedIndexPattern), + [hits, selectedIndexPattern] ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); @@ -199,6 +199,7 @@ export function DiscoverSidebar({ className="dscSidebar__item" > ; - let params: any; - let values: any; + let grouped: boolean; + let values: any[]; beforeEach(function () { values = [ ['foo', 'bar'], @@ -88,30 +76,28 @@ describe('fieldCalculator', function () { 'foo', undefined, ]; - params = {}; - groups = fieldCalculator._groupValues(values, params); + groups = groupValues(values, grouped); }); - it('should have a _groupValues that counts values', function () { + it('should return an object values', function () { expect(groups).toBeInstanceOf(Object); }); it('should throw an error if any value is a plain object', function () { expect(function () { - fieldCalculator._groupValues([{}, true, false], params); + groupValues([{}, true, false], grouped); }).toThrowError(); }); it('should handle values with dots in them', function () { values = ['0', '0.........', '0.......,.....']; - params = {}; - groups = fieldCalculator._groupValues(values, params); + groups = groupValues(values, grouped); expect(groups[values[0]].count).toBe(1); expect(groups[values[1]].count).toBe(1); expect(groups[values[2]].count).toBe(1); }); - it('should have a a key for value in the array when not grouping array terms', function () { + it('should have a key for value in the array when not grouping array terms', function () { expect(_.keys(groups).length).toBe(3); expect(groups.foo).toBeInstanceOf(Object); expect(groups.bar).toBeInstanceOf(Object); @@ -119,7 +105,7 @@ describe('fieldCalculator', function () { }); it('should count array terms independently', function () { - expect(groups['foo,bar']).toBe(undefined); + expect(groups['foo,bar']).toBeUndefined(); expect(groups.foo.count).toBe(5); expect(groups.bar.count).toBe(3); expect(groups.baz.count).toBe(1); @@ -127,11 +113,11 @@ describe('fieldCalculator', function () { describe('grouped array terms', function () { beforeEach(function () { - params.grouped = true; - groups = fieldCalculator._groupValues(values, params); + grouped = true; + groups = groupValues(values, grouped); }); - it('should group array terms when passed params.grouped', function () { + it('should group array terms when grouped is true', function () { expect(_.keys(groups).length).toBe(4); expect(groups['foo,bar']).toBeInstanceOf(Object); }); @@ -155,12 +141,12 @@ describe('fieldCalculator', function () { hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); }); - it('Should return an array of values for _source fields', function () { - const extensions = fieldCalculator.getFieldValues( + it('should return an array of values for _source fields', function () { + const extensions = getFieldValues({ hits, - indexPattern.fields.getByName('extension'), - indexPattern - ); + field: indexPattern.fields.getByName('extension') as IndexPatternField, + indexPattern, + }); expect(extensions).toBeInstanceOf(Array); expect( _.filter(extensions, function (v) { @@ -170,12 +156,12 @@ describe('fieldCalculator', function () { expect(_.uniq(_.clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']); }); - it('Should return an array of values for core meta fields', function () { - const types = fieldCalculator.getFieldValues( + it('should return an array of values for core meta fields', function () { + const types = getFieldValues({ hits, - indexPattern.fields.getByName('_type'), - indexPattern - ); + field: indexPattern.fields.getByName('_type') as IndexPatternField, + indexPattern, + }); expect(types).toBeInstanceOf(Array); expect( _.filter(types, function (v) { @@ -187,48 +173,96 @@ describe('fieldCalculator', function () { }); describe('getFieldValueCounts', function () { - let params: { hits: any; field: any; count: number; indexPattern: IndexPattern }; + let params: FieldValueCountsParams; beforeEach(function () { params = { hits: _.cloneDeep(realHits), - field: indexPattern.fields.getByName('extension'), + field: indexPattern.fields.getByName('extension') as IndexPatternField, count: 3, indexPattern, }; }); + it('counts the top 5 values by default', function () { + params.hits = params.hits.map((hit: Record, i) => ({ + ...hit, + _source: { + extension: `${hit._source.extension}-${i}`, + }, + })); + params.count = undefined; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(5); + expect(extensions.error).toBeUndefined(); + }); + + it('counts only distinct values if less than default', function () { + params.count = undefined; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(4); + expect(extensions.error).toBeUndefined(); + }); + + it('counts only distinct values if less than specified count', function () { + params.count = 10; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(4); + expect(extensions.error).toBeUndefined(); + }); + it('counts the top 3 values', function () { - const extensions = fieldCalculator.getFieldValueCounts(params); + const extensions = getFieldValueCounts(params); expect(extensions).toBeInstanceOf(Object); expect(extensions.buckets).toBeInstanceOf(Array); - expect(extensions.buckets.length).toBe(3); - expect(_.map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']); - expect(extensions.error).toBe(undefined); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(3); + expect(_.map(buckets, 'value')).toEqual(['html', 'gif', 'php']); + expect(extensions.error).toBeUndefined(); }); it('fails to analyze geo and attachment types', function () { - params.field = indexPattern.fields.getByName('point'); - expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined); + params.field = indexPattern.fields.getByName('point') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); - params.field = indexPattern.fields.getByName('area'); - expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined); + params.field = indexPattern.fields.getByName('area') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); - params.field = indexPattern.fields.getByName('request_body'); - expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined); + params.field = indexPattern.fields.getByName('request_body') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); }); it('fails to analyze fields that are in the mapping, but not the hits', function () { - params.field = indexPattern.fields.getByName('ip'); - expect(fieldCalculator.getFieldValueCounts(params).error).not.toBe(undefined); + params.field = indexPattern.fields.getByName('ip') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); }); it('counts the total hits', function () { - expect(fieldCalculator.getFieldValueCounts(params).total).toBe(params.hits.length); + expect(getFieldValueCounts(params).total).toBe(params.hits.length); }); it('counts the hits the field exists in', function () { - params.field = indexPattern.fields.getByName('phpmemory'); - expect(fieldCalculator.getFieldValueCounts(params).exists).toBe(5); + params.field = indexPattern.fields.getByName('phpmemory') as IndexPatternField; + expect(getFieldValueCounts(params).exists).toBe(5); + }); + + it('catches and returns errors', function () { + params.hits = params.hits.map((hit: Record) => ({ + ...hit, + _source: { + extension: { foo: hit._source.extension }, + }, + })); + params.grouped = true; + expect(typeof getFieldValueCounts(params).error).toBe('string'); }); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.ts new file mode 100644 index 00000000000..54f8832fa1f --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.ts @@ -0,0 +1,148 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; +import { FieldValueCounts } from '../types'; + +const NO_ANALYSIS_TYPES = ['geo_point', 'geo_shape', 'attachment']; + +interface FieldValuesParams { + hits: Array>; + field: IndexPatternField; + indexPattern: IndexPattern; +} + +interface FieldValueCountsParams extends FieldValuesParams { + count?: number; + grouped?: boolean; +} + +const getFieldValues = ({ hits, field, indexPattern }: FieldValuesParams) => { + const name = field.name; + const flattenHit = indexPattern.flattenHit; + return hits.map((hit) => flattenHit(hit)[name]); +}; + +const getFieldValueCounts = (params: FieldValueCountsParams): FieldValueCounts => { + const { hits, field, indexPattern, count = 5, grouped = false } = params; + const { type: fieldType } = field; + + if (NO_ANALYSIS_TYPES.includes(fieldType)) { + return { + error: i18n.translate( + 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage', + { + defaultMessage: 'Analysis is not available for {fieldType} fields.', + values: { + fieldType, + }, + } + ), + }; + } + + const allValues = getFieldValues({ hits, field, indexPattern }); + const missing = allValues.filter((v) => v === undefined || v === null).length; + + try { + const groups = groupValues(allValues, grouped); + const counts = Object.keys(groups) + .sort((a, b) => groups[b].count - groups[a].count) + .slice(0, count) + .map((key) => ({ + value: groups[key].value, + count: groups[key].count, + percent: (groups[key].count / (hits.length - missing)) * 100, + display: indexPattern.getFormatterForField(field).convert(groups[key].value), + })); + + if (hits.length === missing) { + return { + error: i18n.translate( + 'discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage', + { + defaultMessage: + 'This field is present in your OpenSearch mapping but not in the {hitsLength} documents shown in the doc table. You may still be able to visualize or search on it.', + values: { + hitsLength: hits.length, + }, + } + ), + }; + } + + return { + total: hits.length, + exists: hits.length - missing, + missing, + buckets: counts, + }; + } catch (e) { + return { + error: e instanceof Error ? e.message : String(e), + }; + } +}; + +const groupValues = ( + allValues: any[], + grouped?: boolean +): Record => { + const values = grouped ? allValues : allValues.flat(); + + return values + .filter((v) => { + if (v instanceof Object && !Array.isArray(v)) { + throw new Error( + i18n.translate( + 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage', + { + defaultMessage: 'Analysis is not available for object fields.', + } + ) + ); + } + return v !== undefined && v !== null; + }) + .reduce((groups, value) => { + if (groups.hasOwnProperty(value)) { + groups[value].count++; + } else { + groups[value] = { + value, + count: 1, + }; + } + return groups; + }, {}); +}; + +export { FieldValueCountsParams, groupValues, getFieldValues, getFieldValueCounts }; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index fb8f22e202c..823cbde9ba7 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -29,27 +29,38 @@ */ // @ts-ignore -import { fieldCalculator } from './field_calculator'; +import { i18n } from '@osd/i18n'; +import { getFieldValueCounts } from './field_calculator'; import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; export function getDetails( field: IndexPatternField, hits: Array>, - columns: string[], indexPattern?: IndexPattern ) { + const defaultDetails = { + error: '', + exists: 0, + total: 0, + buckets: [], + }; if (!indexPattern) { - return {}; + return { + ...defaultDetails, + error: i18n.translate('discover.fieldChooser.noIndexPatternSelectedErrorMessage', { + defaultMessage: 'Index pattern not specified.', + }), + }; } const details = { - ...fieldCalculator.getFieldValueCounts({ + ...defaultDetails, + ...getFieldValueCounts({ hits, field, indexPattern, count: 5, grouped: false, }), - columns, }; if (details.buckets) { for (const bucket of details.buckets) { diff --git a/src/plugins/discover/public/application/components/sidebar/types.ts b/src/plugins/discover/public/application/components/sidebar/types.ts index b254057b0de..a43120b28e9 100644 --- a/src/plugins/discover/public/application/components/sidebar/types.ts +++ b/src/plugins/discover/public/application/components/sidebar/types.ts @@ -36,9 +36,12 @@ export interface IndexPatternRef { export interface FieldDetails { error: string; exists: number; - total: boolean; + total: number; buckets: Bucket[]; - columns: string[]; +} + +export interface FieldValueCounts extends Partial { + missing?: number; } export interface Bucket { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 0d337077dd9..bb05e93c439 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -81,7 +81,7 @@ export interface EmbeddableFactory< * Returns a display name for this type of embeddable. Used in "Create new... " options * in the add panel for containers. */ - getDisplayName(): string; + getDisplayName(): JSX.Element | string; /** * If false, this type of embeddable can't be created with the "createNew" functionality. Instead, diff --git a/src/plugins/embeddable/public/lib/errors.ts b/src/plugins/embeddable/public/lib/errors.ts index 6561c463890..2a261d45c90 100644 --- a/src/plugins/embeddable/public/lib/errors.ts +++ b/src/plugins/embeddable/public/lib/errors.ts @@ -49,7 +49,7 @@ export class EmbeddableFactoryNotFoundError extends Error { constructor(type: string) { super( i18n.translate('embeddableApi.errors.embeddableFactoryNotFound', { - defaultMessage: `{type} can't be loaded. Please upgrade to the default distribution of OpenSearch and OpenSearch Dashboards with the appropriate license.`, + defaultMessage: `OpenSearch Dashboards can't load "{type}" visualizations. Check for a missing plugin or an incompatible visualization type.`, values: { type, }, diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 616ccb65493..e6e78df164d 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -83,6 +83,12 @@ interface Props { SavedObjectFinder: React.ComponentType; stateTransfer?: EmbeddableStateTransfer; hideHeader?: boolean; + // By default, embeddable.destroy() is called when this component unmounts. + // To prevent this default behavior, set this prop to true. + isDestroyPrevented?: boolean; + // Toggle off the border and shadow applied around the embeddable. + // By default, the embeddable will have a shadow and border around it. + isBorderless?: boolean; } interface State { @@ -200,7 +206,10 @@ export class EmbeddablePanel extends React.Component { if (this.state.errorEmbeddable) { this.state.errorEmbeddable.destroy(); } - this.props.embeddable.destroy(); + + if (!this.props.isDestroyPrevented) { + this.props.embeddable.destroy(); + } } public onFocus = (focusedPanelIndex: string) => { @@ -234,6 +243,8 @@ export class EmbeddablePanel extends React.Component { paddingSize="none" role="figure" aria-labelledby={headerId} + hasBorder={!this.props.isBorderless} + hasShadow={!this.props.isBorderless} > {!this.props.hideHeader && ( - - - } - icon={ - - } - onClick={[Function]} - title={ - - } - /> - + {/* TODO: [UNCOMMENTME] Once we have long-term fix for https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2684 } /> - + */} {getMlCardState() !== MlCardState.HIDDEN ? mlCard : <>} + {`Custom WMS Configuration`} - + ), }} /> diff --git a/src/plugins/opensearch_dashboards_utils/README.md b/src/plugins/opensearch_dashboards_utils/README.md index 46e436ba89d..d5650849ff1 100644 --- a/src/plugins/opensearch_dashboards_utils/README.md +++ b/src/plugins/opensearch_dashboards_utils/README.md @@ -4,3 +4,4 @@ Utilities for building OpenSearch Dashboards plugins. - [State containers](./docs/state_containers). - [State syncing utilities](./docs/state_sync). +- [Global data persistence](./docs/global_data_persistence.md) diff --git a/src/plugins/opensearch_dashboards_utils/docs/global_data_persistence.md b/src/plugins/opensearch_dashboards_utils/docs/global_data_persistence.md new file mode 100644 index 00000000000..d756b8c35ac --- /dev/null +++ b/src/plugins/opensearch_dashboards_utils/docs/global_data_persistence.md @@ -0,0 +1,99 @@ +# Global data persistence + +As of 12/1/2022, there are five plugins that have implemented global data persistence ability in OpenSearch Dashboards, and they are visualize, discover, Timeline, dashboards, and vis-builder. Global data persistence means that the data are not only persisted over refreshes, but also able to be persisted across multiple plugins. We utilize [state containers](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_containers), [state storage](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_sync/storages) and [state syncing utilities](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_sync) from [OpenSearch Dashboards Utils](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils) to achieve global data persistence. User can choose to persist data either in URL or session storage by changing the setting `Store URLs in session storage` under advanced setting page. + +One of the global data persistence example that currently exists is global query parameters. Global query parameters include globally pinned filters, time range and time refresh intervals. For example, we set a specific time range and time refresh interval when trying to a new visualization. When we navigate to the dashboard page, we can see the previous time range and time refresh interval that are set within the visualization app are still there. However, when we create a filter, it will only be persisted within that specific plugin since it is not a global filter. We can make a filter become a global filter by selecting `Pin across all apps`. Only global filters are persisted across all other globally persistent plugins within the application. + +The following five steps demonstrate how to add global query parameter persistence for a plugin. Step 3 is specific to global query parameter persistence. For implementing global data persistence in general, step 1 and 2 are required. A function that is similar to step 3 to sync up the state manager of the data with osdUrlStateStorage is also required. + +# Steps to add global data persistence ability to a plugin + +1. Call [`createOsdUrlTracker()`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/opensearch_dashboards_utils/public/state_management/url/osd_url_tracker.ts) in the set up function within public/plugin.ts. This creates a tracker that syncs the storage with the state manager by listening to history changes and global state changes, and updating the nav link URL of a given app to point to the last visited page. The two functions that get returned, `appMounted()` and `appUnMounted()`, help with global data persistence across the app. When user enters one app, `appMounted()` will be called to make sure that the current app is actively listening to history changes. It will also initialize the URL to be previously stored URL from storage. When user leaves one app, `appUnmounted()` will be called so the app will stop listening actively on history changes, but start subscribing to the global states. Therefore, if the global states are changed in another app, the global state listener will still be triggered in this app even though it is not currently active. It will also update the corresponding URL in the browser storage. By using `appMounted()` and `appUnMounted()`, it makes sure that global data are always persisted no matter which app we are currently on. + * declare two private variables: `appStateUpdater` observable and `stopUrlTracking()` + ```ts + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking?: () => void; + ``` + * within the `setup()` function in the plugin class, call `createOsdUrlTracker` by passing in the corresponding baseUrl, defaultSubUrl, storageKey, navLinkUpdater observable and stateParams. StorageKey should follow format: `lastUrl:${core.http.basePath.get()}:pluginID`. + - `this.appStateUpdater` is passed into the function as `navLinkUpdater`. + - return three functions `appMounted()`, `appUnMounted()` and `stopUrlTracker()`. Then class variable `stopUrlTracking()` is set to be `stopUrlTracker()` + * call `appMounted()` in the `mount()` function + * call `appUnMounted()` in return of `mount()` + * call `stopUrlTracking()` in `stop()` function for the plugin + + ```ts + const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/vis-builder'), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:vis-builder`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => + !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + ``` + +2. Set [`osdUrlStateStorage()`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/opensearch_dashboards_utils/public/state_sync/state_sync_state_storage/create_osd_url_state_storage.ts#L83) service. This step initializes the store, and indicates global storage by using '_g' flag. + * when setting the plugin services, set osdUrlStateStorage service by calling `createOsdUrlStateStorage()` with the current history, useHash and withNotifyErrors + + ```ts + const services: VisBuilderServices = { + ...coreStart, + history: params.history, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: params.history, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), + ... + + ``` +3. Sync states with storage. There are many ways to do this and use whatever makes sense for your specific use cases. One such implementation is for syncing the query data in `syncQueryStateWithUrl` from the data plugin. + * import [`syncQueryStateWithUrl`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/data/public/query/state_sync/sync_state_with_url.ts#L48) from data plugin and call it with query service and osdUrlStateStorage service that we set in step 2. This function completes two jobs: 1. When we first enter the app and there is no data stored in the URL, it initializes the URL by putting the `_g` key followed by default data values. 2. When we refresh the page, this function is responsible to retrive the stored states in the URL, and apply them to the app. + + ```ts + export const VisBuilderApp = () => { + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); + ``` + + * If not already, add query services from data plugin in public/plugin_services.ts + + ```ts + export const [getQueryService, setQueryService] = createGetterSetter('Query'); + ``` + diff --git a/src/plugins/region_map/common/constants/shared.ts b/src/plugins/region_map/common/constants/shared.ts index 6d82a3a3360..3dc3e6ce6e8 100644 --- a/src/plugins/region_map/common/constants/shared.ts +++ b/src/plugins/region_map/common/constants/shared.ts @@ -5,3 +5,4 @@ export const DEFAULT_MAP_CHOICE = 'default'; export const CUSTOM_MAP_CHOICE = 'custom'; +export const CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING = 'visualization:regionmap:customVectorMapMaxSize'; diff --git a/src/plugins/region_map/common/index.ts b/src/plugins/region_map/common/index.ts index bdda981590e..f7f2062b491 100644 --- a/src/plugins/region_map/common/index.ts +++ b/src/plugins/region_map/common/index.ts @@ -3,6 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE } from './constants/shared'; +import { + DEFAULT_MAP_CHOICE, + CUSTOM_MAP_CHOICE, + CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING, +} from './constants/shared'; -export { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE }; +export { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE, CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING }; diff --git a/src/plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js index e0213108f64..10d2389c576 100644 --- a/src/plugins/region_map/public/choropleth_layer.js +++ b/src/plugins/region_map/public/choropleth_layer.js @@ -37,7 +37,11 @@ import { getNotifications } from './opensearch_dashboards_services'; import { colorUtil, OpenSearchDashboardsMapLayer } from '../../maps_legacy/public'; import { truncatedColorMaps } from '../../charts/public'; import { getServices } from './services'; -import { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE } from '../common'; +import { + DEFAULT_MAP_CHOICE, + CUSTOM_MAP_CHOICE, + CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING, +} from '../common'; const EMPTY_STYLE = { weight: 1, @@ -94,7 +98,8 @@ export class ChoroplethLayer extends OpenSearchDashboardsMapLayer { serviceSettings, leaflet, layerChosenByUser, - http + http, + uiSettings ) { super(); this._serviceSettings = serviceSettings; @@ -112,6 +117,7 @@ export class ChoroplethLayer extends OpenSearchDashboardsMapLayer { this._layerChosenByUser = layerChosenByUser; this._http = http; this._visParams = null; + this._uiSettings = uiSettings; // eslint-disable-next-line no-undef this._leafletLayer = this._leaflet.geoJson(null, { @@ -241,7 +247,8 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards // fetch data from index and transform it to feature collection try { const services = getServices(this._http); - const result = await services.getIndexData(this._layerName); + const indexSize = this._uiSettings.get(CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING); + const result = await services.getIndexData(this._layerName, indexSize); const finalResult = { type: 'FeatureCollection', @@ -337,7 +344,8 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards serviceSettings, leaflet, layerChosenByUser, - http + http, + uiSettings ) { const clonedLayer = new ChoroplethLayer( name, @@ -349,7 +357,8 @@ CORS configuration of the server permits requests from the OpenSearch Dashboards serviceSettings, leaflet, layerChosenByUser, - http + http, + uiSettings ); clonedLayer.setJoinField(this._joinField); clonedLayer.setColorRamp(this._colorRamp); diff --git a/src/plugins/region_map/public/components/map_choice_options.tsx b/src/plugins/region_map/public/components/map_choice_options.tsx index f18cf88b371..f08f026233a 100644 --- a/src/plugins/region_map/public/components/map_choice_options.tsx +++ b/src/plugins/region_map/public/components/map_choice_options.tsx @@ -4,7 +4,7 @@ */ import './map_choice_options.scss'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiPanel, EuiSpacer, diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 101e4233ca7..69dca0525b8 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -158,7 +158,6 @@ export function createRegionMapVisualization({ let selectedLayer; if (DEFAULT_MAP_CHOICE === this._params.layerChosenByUser && this._params.selectedLayer) { selectedLayer = await this._loadConfig(this._params.selectedLayer); - this._params.selectedJoinField = selectedLayer?.fields[0]; } else if ( CUSTOM_MAP_CHOICE === this._params.layerChosenByUser && this._params.selectedCustomLayer @@ -231,7 +230,8 @@ export function createRegionMapVisualization({ await getServiceSettings(), (await lazyLoadMapsLegacyModules()).L, this._params.layerChosenByUser, - http + http, + uiSettings ); } else { const { ChoroplethLayer } = await import('./choropleth_layer'); @@ -245,7 +245,8 @@ export function createRegionMapVisualization({ await getServiceSettings(), (await lazyLoadMapsLegacyModules()).L, this._params.layerChosenByUser, - http + http, + uiSettings ); } this._choroplethLayer.setLayerChosenByUser(this._params.layerChosenByUser); diff --git a/src/plugins/region_map/public/services.ts b/src/plugins/region_map/public/services.ts index dd26be3ccd7..ba182c7aa0f 100644 --- a/src/plugins/region_map/public/services.ts +++ b/src/plugins/region_map/public/services.ts @@ -7,7 +7,7 @@ import { CoreStart, HttpFetchError } from 'opensearch-dashboards/public'; export interface Services { getCustomIndices: () => Promise; - getIndexData: (indexName: string) => Promise; + getIndexData: (indexName: string, size: number) => Promise; getIndexMapping: (indexName: string) => Promise; } @@ -25,11 +25,12 @@ export function getServices(http: CoreStart['http']): Services { return e; } }, - getIndexData: async (indexName: string) => { + getIndexData: async (indexName: string, size: number) => { try { const response = await http.post('../api/geospatial/_search', { body: JSON.stringify({ index: indexName, + size, }), }); return response; diff --git a/src/plugins/region_map/server/routes/opensearch.ts b/src/plugins/region_map/server/routes/opensearch.ts index 5eebc9a0ffd..dfdcb4a1900 100644 --- a/src/plugins/region_map/server/routes/opensearch.ts +++ b/src/plugins/region_map/server/routes/opensearch.ts @@ -57,14 +57,15 @@ export function registerGeospatialRoutes(router: IRouter) { validate: { body: schema.object({ index: schema.string(), + size: schema.number(), }), }, }, async (context, req, res) => { const client = context.core.opensearch.client.asCurrentUser; try { - const { index } = req.body; - const params = { index, body: {} }; + const { index, size } = req.body; + const params = { index, body: {}, size }; const results = await client.search(params); return res.ok({ body: { diff --git a/src/plugins/region_map/server/ui_settings.ts b/src/plugins/region_map/server/ui_settings.ts index e5c90f6779b..037ee4b67b9 100644 --- a/src/plugins/region_map/server/ui_settings.ts +++ b/src/plugins/region_map/server/ui_settings.ts @@ -31,6 +31,7 @@ import { i18n } from '@osd/i18n'; import { UiSettingsParams } from 'opensearch-dashboards/server'; import { schema } from '@osd/config-schema'; +import { CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING } from '../common'; export function getUiSettings(): Record> { return { @@ -49,5 +50,20 @@ export function getUiSettings(): Record> { schema: schema.boolean(), category: ['visualization'], }, + [CUSTOM_VECTOR_MAP_MAX_SIZE_SETTING]: { + name: i18n.translate('regionMap.advancedSettings.visualization.customVectorMapDefaultSize', { + defaultMessage: 'Custom vector map size', + }), + value: 1000, + description: i18n.translate( + 'regionMap.advancedSettings.visualization.customVectorMapDefaultSizeText', + { + defaultMessage: + 'The maximum number of features to load from custom vector map. A higher number might have negative impact on browser rendering performance.', + } + ), + schema: schema.number(), + category: ['visualization'], + }, }; } diff --git a/src/plugins/saved_objects_management/public/index.ts b/src/plugins/saved_objects_management/public/index.ts index 147247c56d7..2377afe175c 100644 --- a/src/plugins/saved_objects_management/public/index.ts +++ b/src/plugins/saved_objects_management/public/index.ts @@ -39,6 +39,9 @@ export { SavedObjectsManagementColumnServiceSetup, SavedObjectsManagementColumnServiceStart, SavedObjectsManagementColumn, + SavedObjectsManagementNamespaceServiceSetup, + SavedObjectsManagementNamespaceServiceStart, + SavedObjectsManagementNamespace, SavedObjectsManagementRecord, ISavedObjectsManagementServiceRegistry, SavedObjectsManagementServiceRegistryEntry, diff --git a/src/plugins/saved_objects_management/public/lib/filter_query.test.ts b/src/plugins/saved_objects_management/public/lib/filter_query.test.ts new file mode 100644 index 00000000000..ed0a133ed2b --- /dev/null +++ b/src/plugins/saved_objects_management/public/lib/filter_query.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { filterQuery } from './filter_query'; + +describe('filterQuery', () => { + it('should return full list of allowed vals, requested values unspecified', () => { + const allowedVals = ['config', 'index-pattern', 'url', 'query']; + const requestedVals = undefined; + + const expected = ['config', 'index-pattern', 'url', 'query']; + expect(filterQuery(allowedVals, requestedVals)).toEqual(expected); + }); + + it('should return list of all requested values, all values within allowed values', () => { + const allowedVals = ['config', 'index-pattern', 'url', 'query']; + const requestedVals = ['config', 'index-pattern']; + + const expected = ['config', 'index-pattern']; + expect(filterQuery(allowedVals, requestedVals)).toEqual(expected); + }); + + it('should return only allowed values within requested values', () => { + const allowedVals = ['config', 'index-pattern', 'url', 'query']; + const requestedVals = ['config', 'index-pattern', 'forbidden']; + + const expected = ['config', 'index-pattern']; + expect(filterQuery(allowedVals, requestedVals)).toEqual(expected); + }); +}); diff --git a/src/plugins/saved_objects_management/public/lib/filter_query.ts b/src/plugins/saved_objects_management/public/lib/filter_query.ts new file mode 100644 index 00000000000..4194aba5f9e --- /dev/null +++ b/src/plugins/saved_objects_management/public/lib/filter_query.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export function filterQuery(allowedVals: string[], requestedVals?: string[]): string[] { + const filteredVals = requestedVals + ? allowedVals.filter((val) => requestedVals.includes(val)) + : allowedVals; + return filteredVals; +} diff --git a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index 0dbf71f8e8c..6eaaac7d35f 100644 --- a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -30,13 +30,18 @@ import { HttpStart } from 'src/core/public'; +export interface SavedObjectCountOptions { + typesToInclude: string[]; + namespacesToInclude?: string[]; + searchString?: string; +} + export async function getSavedObjectCounts( http: HttpStart, - typesToInclude: string[], - searchString?: string + options: SavedObjectCountOptions ): Promise> { return await http.post>( `/api/opensearch-dashboards/management/saved_objects/scroll/counts`, - { body: JSON.stringify({ typesToInclude, searchString }) } + { body: JSON.stringify(options) } ); } diff --git a/src/plugins/saved_objects_management/public/lib/index.ts b/src/plugins/saved_objects_management/public/lib/index.ts index e01e58f4bdd..fae58cad3eb 100644 --- a/src/plugins/saved_objects_management/public/lib/index.ts +++ b/src/plugins/saved_objects_management/public/lib/index.ts @@ -32,7 +32,7 @@ export { fetchExportByTypeAndSearch } from './fetch_export_by_type_and_search'; export { fetchExportObjects } from './fetch_export_objects'; export { canViewInApp } from './in_app_url'; export { getRelationships } from './get_relationships'; -export { getSavedObjectCounts } from './get_saved_object_counts'; +export { getSavedObjectCounts, SavedObjectCountOptions } from './get_saved_object_counts'; export { getSavedObjectLabel } from './get_saved_object_label'; export { importFile } from './import_file'; export { importLegacyFile } from './import_legacy_file'; @@ -56,3 +56,4 @@ export { findObjects, findObject } from './find_objects'; export { extractExportDetails, SavedObjectsExportResultDetails } from './extract_export_details'; export { createFieldList } from './create_field_list'; export { getAllowedTypes } from './get_allowed_types'; +export { filterQuery } from './filter_query'; diff --git a/src/plugins/saved_objects_management/public/lib/parse_query.test.ts b/src/plugins/saved_objects_management/public/lib/parse_query.test.ts index 854c1e4ea3d..a940cf3ebbc 100644 --- a/src/plugins/saved_objects_management/public/lib/parse_query.test.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.test.ts @@ -34,8 +34,19 @@ describe('getQueryText', () => { it('should know how to get the text out of the AST', () => { const ast = { getTermClauses: () => [{ value: 'foo' }, { value: 'bar' }], - getFieldClauses: () => [{ value: 'lala' }, { value: 'lolo' }], + getFieldClauses: (field) => { + if (field === 'type') { + return [{ value: 'lala' }, { value: 'lolo' }]; + } else if (field === 'namespaces') { + return [{ value: 'default' }]; + } + return []; + }, }; - expect(parseQuery({ ast } as any)).toEqual({ queryText: 'foo bar', visibleTypes: 'lala' }); + expect(parseQuery({ ast } as any, ['type'])).toEqual({ + queryText: 'foo bar', + visibleTypes: 'lala', + visibleNamespaces: 'default', + }); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/parse_query.ts b/src/plugins/saved_objects_management/public/lib/parse_query.ts index dca6384efbe..24c35d500aa 100644 --- a/src/plugins/saved_objects_management/public/lib/parse_query.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.ts @@ -38,6 +38,7 @@ interface ParsedQuery { export function parseQuery(query: Query): ParsedQuery { let queryText: string | undefined; let visibleTypes: string[] | undefined; + let visibleNamespaces: string[] | undefined; if (query) { if (query.ast.getTermClauses().length) { @@ -49,10 +50,14 @@ export function parseQuery(query: Query): ParsedQuery { if (query.ast.getFieldClauses('type')) { visibleTypes = query.ast.getFieldClauses('type')[0].value as string[]; } + if (query.ast.getFieldClauses('namespaces')) { + visibleNamespaces = query.ast.getFieldClauses('namespaces')[0].value as string[]; + } } return { queryText, visibleTypes, + visibleNamespaces, }; } diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 8013c05aad7..2c42df5c782 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -104,6 +104,7 @@ export const mountManagementSection = async ({ serviceRegistry={serviceRegistry} actionRegistry={pluginStart.actions} columnRegistry={pluginStart.columns} + namespaceRegistry={pluginStart.namespaces} allowedTypes={allowedObjectTypes} setBreadcrumbs={setBreadcrumbs} /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index a86362994a6..d18762f4912 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -274,27 +274,35 @@ exports[`SavedObjectsTable should render normally 1`] = ` "has": [MockFunction], } } - filterOptions={ + filters={ Array [ Object { - "name": "index-pattern", - "value": "index-pattern", - "view": "index-pattern (0)", - }, - Object { - "name": "visualization", - "value": "visualization", - "view": "visualization (0)", - }, - Object { - "name": "dashboard", - "value": "dashboard", - "view": "dashboard (0)", - }, - Object { - "name": "search", - "value": "search", - "view": "search (0)", + "field": "type", + "multiSelect": "or", + "name": "Type", + "options": Array [ + Object { + "name": "index-pattern", + "value": "index-pattern", + "view": "index-pattern (0)", + }, + Object { + "name": "visualization", + "value": "visualization", + "view": "visualization (0)", + }, + Object { + "name": "dashboard", + "value": "dashboard", + "view": "dashboard (0)", + }, + Object { + "name": "search", + "value": "search", + "view": "search (0)", + }, + ], + "type": "field_value_selection", }, ] } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 8e473730615..7e5bb318f4d 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -62,7 +62,19 @@ const defaultProps: TableProps = { selectionConfig: { onSelectionChange: () => {}, }, - filterOptions: [{ value: 2 }], + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: 'Type', + multiSelect: 'or', + options: [ + { + value: 2, + }, + ], + }, + ], onDelete: () => {}, onActionRefresh: () => {}, onExport: () => {}, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 50ec838b986..636933d449d 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -61,11 +61,12 @@ export interface TableProps { basePath: IBasePath; actionRegistry: SavedObjectsManagementActionServiceStart; columnRegistry: SavedObjectsManagementColumnServiceStart; + namespaceRegistry: SavedObjectsManagementNamespaceServiceStart; selectedSavedObjects: SavedObjectWithMetadata[]; selectionConfig: { onSelectionChange: (selection: SavedObjectWithMetadata[]) => void; }; - filterOptions: any[]; + filters: any[]; canDelete: boolean; onDelete: () => void; onActionRefresh: (object: SavedObjectWithMetadata) => void; @@ -76,7 +77,7 @@ export interface TableProps { items: SavedObjectWithMetadata[]; itemId: string | (() => string); totalItemCount: number; - onQueryChange: (query: any) => void; + onQueryChange: (query: any, filterFields: string[]) => void; onTableChange: (table: any) => void; isSearching: boolean; onShowRelationships: (object: SavedObjectWithMetadata) => void; @@ -163,7 +164,7 @@ export class Table extends PureComponent { items, totalItemCount, isSearching, - filterOptions, + filters, selectionConfig: selection, onDelete, onActionRefresh, @@ -174,6 +175,7 @@ export class Table extends PureComponent { basePath, actionRegistry, columnRegistry, + namespaceRegistry, dateFormat, } = this.props; @@ -184,26 +186,6 @@ export class Table extends PureComponent { pageSizeOptions: [5, 10, 20, 50], }; - const filters = [ - { - type: 'field_value_selection', - field: 'type', - name: i18n.translate('savedObjectsManagement.objectsTable.table.typeFilterName', { - defaultMessage: 'Type', - }), - multiSelect: 'or', - options: filterOptions, - }, - // Add this back in once we have tag support - // { - // type: 'field_value_selection', - // field: 'tag', - // name: 'Tags', - // multiSelect: 'or', - // options: [], - // }, - ]; - const columns = [ { field: 'type', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 2f8382c5946..5a6bf0713d9 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -53,6 +53,7 @@ import { dataPluginMock } from '../../../../data/public/mocks'; import { serviceRegistryMock } from '../../services/service_registry.mock'; import { actionServiceMock } from '../../services/action_service.mock'; import { columnServiceMock } from '../../services/column_service.mock'; +import { namespaceServiceMock } from '../../services/namespace_service.mock'; import { SavedObjectsTable, SavedObjectsTableProps, @@ -136,10 +137,12 @@ describe('SavedObjectsTable', () => { http.post.mockResolvedValue([]); getSavedObjectCountsMock.mockReturnValue({ - 'index-pattern': 0, - visualization: 0, - dashboard: 0, - search: 0, + type: { + 'index-pattern': 0, + visualization: 0, + dashboard: 0, + search: 0, + }, }); defaultProps = { @@ -147,6 +150,7 @@ describe('SavedObjectsTable', () => { serviceRegistry: serviceRegistryMock.create(), actionRegistry: actionServiceMock.createStart(), columnRegistry: columnServiceMock.createStart(), + namespaceRegistry: namespaceServiceMock.createStart(), savedObjectsClient: savedObjects.client, indexPatterns: dataPluginMock.createStartContract().indexPatterns, http, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 8d0d13d3334..2f78f307d16 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -71,10 +71,12 @@ import { IndexPatternsContract } from '../../../../data/public'; import { parseQuery, getSavedObjectCounts, + SavedObjectCountOptions, getRelationships, getSavedObjectLabel, fetchExportObjects, fetchExportByTypeAndSearch, + filterQuery, findObjects, findObject, extractExportDetails, @@ -85,6 +87,7 @@ import { ISavedObjectsManagementServiceRegistry, SavedObjectsManagementActionServiceStart, SavedObjectsManagementColumnServiceStart, + SavedObjectsManagementNamespaceServiceStart, } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; @@ -99,6 +102,7 @@ export interface SavedObjectsTableProps { serviceRegistry: ISavedObjectsManagementServiceRegistry; actionRegistry: SavedObjectsManagementActionServiceStart; columnRegistry: SavedObjectsManagementColumnServiceStart; + namespaceRegistry: SavedObjectsManagementNamespaceServiceStart; savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; http: HttpStart; @@ -176,37 +180,57 @@ export class SavedObjectsTable extends Component { - const { allowedTypes } = this.props; - const { queryText, visibleTypes } = parseQuery(this.state.activeQuery); + const { allowedTypes, namespaceRegistry } = this.props; + const { queryText, visibleTypes, visibleNamespaces } = parseQuery(this.state.activeQuery); - const filteredTypes = allowedTypes.filter( - (type) => !visibleTypes || visibleTypes.includes(type) - ); + const filteredTypes = filterQuery(allowedTypes, visibleTypes); + + const availableNamespaces = namespaceRegistry.getAll()?.map((ns) => ns.id) || []; + + const filteredCountOptions: SavedObjectCountOptions = { + typesToInclude: filteredTypes, + searchString: queryText, + }; + + if (availableNamespaces.length) { + const filteredNamespaces = filterQuery(availableNamespaces, visibleNamespaces); + filteredCountOptions.namespacesToInclude = filteredNamespaces; + } // These are the saved objects visible in the table. const filteredSavedObjectCounts = await getSavedObjectCounts( this.props.http, - filteredTypes, - queryText + filteredCountOptions ); const exportAllOptions: ExportAllOption[] = []; const exportAllSelectedOptions: Record = {}; - Object.keys(filteredSavedObjectCounts).forEach((id) => { + const filteredTypeCounts = filteredSavedObjectCounts.type || {}; + + Object.keys(filteredTypeCounts).forEach((id) => { // Add this type as a bulk-export option. exportAllOptions.push({ id, - label: `${id} (${filteredSavedObjectCounts[id] || 0})`, + label: `${id} (${filteredTypeCounts[id] || 0})`, }); // Select it by default. exportAllSelectedOptions[id] = true; }); + const countOptions: SavedObjectCountOptions = { + typesToInclude: allowedTypes, + searchString: queryText, + }; + + if (availableNamespaces.length) { + countOptions.namespacesToInclude = availableNamespaces; + } + // Fetch all the saved objects that exist so we can accurately populate the counts within // the table filter dropdown. - const savedObjectCounts = await getSavedObjectCounts(this.props.http, allowedTypes, queryText); + const savedObjectCounts = await getSavedObjectCounts(this.props.http, countOptions); this.setState((state) => ({ ...state, @@ -226,8 +250,9 @@ export class SavedObjectsTable extends Component { const { activeQuery: query, page, perPage } = this.state; - const { notifications, http, allowedTypes } = this.props; - const { queryText, visibleTypes } = parseQuery(query); + const { notifications, http, allowedTypes, namespaceRegistry } = this.props; + const { queryText, visibleTypes, visibleNamespaces } = parseQuery(query); + const filteredTypes = filterQuery(allowedTypes, visibleTypes); // "searchFields" is missing from the "findOptions" but gets injected via the API. // The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute const findOptions: SavedObjectsFindOptions = { @@ -235,8 +260,15 @@ export class SavedObjectsTable extends Component !visibleTypes || visibleTypes.includes(type)), + type: filteredTypes, }; + + const availableNamespaces = namespaceRegistry.getAll()?.map((ns) => ns.id) || []; + if (availableNamespaces.length) { + const filteredNamespaces = filterQuery(availableNamespaces, visibleNamespaces); + findOptions.namespaces = filteredNamespaces; + } + if (findOptions.type.length > 1) { findOptions.sortField = 'type'; } @@ -390,6 +422,7 @@ export class SavedObjectsTable extends Component { const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state; const { notifications, http } = this.props; + const { queryText } = parseQuery(activeQuery); const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => { if (selected) { @@ -764,18 +797,55 @@ export class SavedObjectsTable extends Component ({ value: type, name: type, - view: `${type} (${savedObjectCounts[type] || 0})`, + view: `${type} (${typeCounts[type] || 0})`, })); + const filters = [ + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('savedObjectsManagement.objectsTable.table.typeFilterName', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: filterOptions, + }, + ]; + + const availableNamespaces = namespaceRegistry.getAll() || []; + if (availableNamespaces.length) { + const nsCounts = savedObjectCounts.namespaces || {}; + const nsFilterOptions = availableNamespaces.map((ns) => { + return { + name: ns.name, + value: ns.id, + view: `${ns.name} (${nsCounts[ns.id] || 0})`, + }; + }); + + filters.push({ + type: 'field_value_selection', + field: 'namespaces', + name: + namespaceRegistry.getAlias() || + i18n.translate('savedObjectsManagement.objectsTable.table.namespaceFilterName', { + defaultMessage: 'Namespaces', + }), + multiSelect: 'or', + options: nsFilterOptions, + }); + } + return ( {this.renderFlyout()} @@ -799,7 +869,7 @@ export class SavedObjectsTable extends Component void; }) => { const capabilities = coreStart.application.capabilities; @@ -78,6 +80,7 @@ const SavedObjectsTablePage = ({ serviceRegistry={serviceRegistry} actionRegistry={actionRegistry} columnRegistry={columnRegistry} + namespaceRegistry={namespaceRegistry} savedObjectsClient={coreStart.savedObjects.client} indexPatterns={dataStart.indexPatterns} search={dataStart.search} diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index c15a96bddb7..ec7d64ed700 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -45,6 +45,9 @@ import { SavedObjectsManagementColumnService, SavedObjectsManagementColumnServiceSetup, SavedObjectsManagementColumnServiceStart, + SavedObjectsManagementNamespaceService, + SavedObjectsManagementNamespaceServiceSetup, + SavedObjectsManagementNamespaceServiceStart, SavedObjectsManagementServiceRegistry, ISavedObjectsManagementServiceRegistry, } from './services'; @@ -53,12 +56,14 @@ import { registerServices } from './register_services'; export interface SavedObjectsManagementPluginSetup { actions: SavedObjectsManagementActionServiceSetup; columns: SavedObjectsManagementColumnServiceSetup; + namespaces: SavedObjectsManagementNamespaceServiceSetup; serviceRegistry: ISavedObjectsManagementServiceRegistry; } export interface SavedObjectsManagementPluginStart { actions: SavedObjectsManagementActionServiceStart; columns: SavedObjectsManagementColumnServiceStart; + namespaces: SavedObjectsManagementNamespaceServiceStart; } export interface SetupDependencies { @@ -84,6 +89,7 @@ export class SavedObjectsManagementPlugin > { private actionService = new SavedObjectsManagementActionService(); private columnService = new SavedObjectsManagementColumnService(); + private namespaceService = new SavedObjectsManagementNamespaceService(); private serviceRegistry = new SavedObjectsManagementServiceRegistry(); public setup( @@ -92,6 +98,7 @@ export class SavedObjectsManagementPlugin ): SavedObjectsManagementPluginSetup { const actionSetup = this.actionService.setup(); const columnSetup = this.columnService.setup(); + const namespaceSetup = this.namespaceService.setup(); if (home) { home.featureCatalogue.register({ @@ -133,6 +140,7 @@ export class SavedObjectsManagementPlugin return { actions: actionSetup, columns: columnSetup, + namespaces: namespaceSetup, serviceRegistry: this.serviceRegistry, }; } @@ -140,10 +148,12 @@ export class SavedObjectsManagementPlugin public start(core: CoreStart, { data }: StartDependencies) { const actionStart = this.actionService.start(); const columnStart = this.columnService.start(); + const namespaceStart = this.namespaceService.start(); return { actions: actionStart, columns: columnStart, + namespaces: namespaceStart, }; } } diff --git a/src/plugins/saved_objects_management/public/services/index.ts b/src/plugins/saved_objects_management/public/services/index.ts index 7d099852776..498ea683384 100644 --- a/src/plugins/saved_objects_management/public/services/index.ts +++ b/src/plugins/saved_objects_management/public/services/index.ts @@ -38,6 +38,11 @@ export { SavedObjectsManagementColumnServiceStart, SavedObjectsManagementColumnServiceSetup, } from './column_service'; +export { + SavedObjectsManagementNamespaceService, + SavedObjectsManagementNamespaceServiceStart, + SavedObjectsManagementNamespaceServiceSetup, +} from './namespace_service'; export { SavedObjectsManagementServiceRegistry, ISavedObjectsManagementServiceRegistry, @@ -46,5 +51,6 @@ export { export { SavedObjectsManagementAction, SavedObjectsManagementColumn, + SavedObjectsManagementNamespace, SavedObjectsManagementRecord, } from './types'; diff --git a/src/plugins/saved_objects_management/public/services/namespace_service.mock.ts b/src/plugins/saved_objects_management/public/services/namespace_service.mock.ts new file mode 100644 index 00000000000..d1599920e3c --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/namespace_service.mock.ts @@ -0,0 +1,52 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + SavedObjectsManagementNamespaceService, + SavedObjectsManagementNamespaceServiceSetup, + SavedObjectsManagementNamespaceServiceStart, +} from './namespace_service'; + +const createSetupMock = (): jest.Mocked => { + const mock = { + register: jest.fn(), + registerAlias: jest.fn(), + }; + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + getAll: jest.fn(), + getAlias: jest.fn(), + }; + + mock.getAll.mockReturnValue([]); + mock.getAlias.mockReturnValue('Namespace'); + + return mock; +}; + +const createServiceMock = (): jest.Mocked< + PublicMethodsOf +> => { + const mock = { + setup: jest.fn().mockReturnValue(createSetupMock()), + start: jest.fn().mockReturnValue(createStartMock()), + }; + return mock; +}; + +export const namespaceServiceMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/src/plugins/saved_objects_management/public/services/namespace_service.ts b/src/plugins/saved_objects_management/public/services/namespace_service.ts new file mode 100644 index 00000000000..5b1a797ae2d --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/namespace_service.ts @@ -0,0 +1,59 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { SavedObjectsManagementNamespace } from './types'; + +export interface SavedObjectsManagementNamespaceServiceSetup { + /** + * register given namespace in the registry. + */ + register: (namespace: SavedObjectsManagementNamespace) => void; + registerAlias: (alias: string) => void; +} + +export interface SavedObjectsManagementNamespaceServiceStart { + /** + * return all {@link SavedObjectsManagementNamespace | namespaces} currently registered. + */ + getAll: () => Array>; + getAlias: () => string; +} + +export class SavedObjectsManagementNamespaceService { + private readonly namespaces = new Map>(); + private readonly alias; + + setup(): SavedObjectsManagementNamespaceServiceSetup { + return { + register: (ns) => { + if (this.namespaces.has(ns.id)) { + throw new Error(`Saved Objects Management Namespace with id '${ns.id}' already exists`); + } + this.namespaces.set(ns.id, ns); + }, + registerAlias: (alias) => { + if (!!this.alias) { + throw new Error( + `An alias has already been registered. Cannot register more than one alias.` + ); + } + this.alias = alias; + }, + }; + } + + start(): SavedObjectsManagementNamespaceServiceStart { + return { + getAll: () => [...this.namespaces.values()], + getAlias: () => this.alias, + }; + } +} diff --git a/src/plugins/saved_objects_management/public/services/types/namespace.ts b/src/plugins/saved_objects_management/public/services/types/namespace.ts new file mode 100644 index 00000000000..3a47f273c15 --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/types/namespace.ts @@ -0,0 +1,15 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export interface SavedObjectsManagementNamespace { + id: string; + name: string; +} diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts index 2987425321c..c2002fc97a1 100644 --- a/src/plugins/saved_objects_management/server/routes/find.ts +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -45,6 +45,9 @@ export const registerFindRoute = ( perPage: schema.number({ min: 0, defaultValue: 20 }), page: schema.number({ min: 0, defaultValue: 1 }), type: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + namespaces: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), search: schema.maybe(schema.string()), defaultSearchOperator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { defaultValue: 'OR', diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index 0c69b376ea5..63233748a89 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -39,17 +39,30 @@ export const registerScrollForCountRoute = (router: IRouter) => { validate: { body: schema.object({ typesToInclude: schema.arrayOf(schema.string()), + namespacesToInclude: schema.maybe(schema.arrayOf(schema.string())), searchString: schema.maybe(schema.string()), }), }, }, router.handleLegacyErrors(async (context, req, res) => { const { client } = context.core.savedObjects; + const counts = { + type: {}, + }; const findOptions: SavedObjectsFindOptions = { type: req.body.typesToInclude, perPage: 1000, }; + + const requestHasNamespaces = + Array.isArray(req.body.namespacesToInclude) && req.body.namespacesToInclude.length; + + if (requestHasNamespaces) { + counts.namespaces = {}; + findOptions.namespaces = req.body.namespacesToInclude; + } + if (req.body.searchString) { findOptions.search = `${req.body.searchString}*`; findOptions.searchFields = ['title']; @@ -57,16 +70,32 @@ export const registerScrollForCountRoute = (router: IRouter) => { const objects = await findAll(client, findOptions); - const counts = objects.reduce((accum, result) => { + objects.forEach((result) => { const type = result.type; - accum[type] = accum[type] || 0; - accum[type]++; - return accum; - }, {} as Record); + if (requestHasNamespaces) { + const resultNamespaces = (result.namespaces || []).flat(); + resultNamespaces.forEach((ns) => { + if (ns === null) { + ns = 'default'; + } + counts.namespaces[ns] = counts.namespaces[ns] || 0; + counts.namespaces[ns]++; + }); + } + counts.type[type] = counts.type[type] || 0; + counts.type[type]++; + }); for (const type of req.body.typesToInclude) { - if (!counts[type]) { - counts[type] = 0; + if (!counts.type[type]) { + counts.type[type] = 0; + } + } + + const namespacesToInclude = req.body.namespacesToInclude || []; + for (const ns of namespacesToInclude) { + if (!counts.namespaces[ns]) { + counts.namespaces[ns] = 0; } } diff --git a/src/plugins/ui_actions/README.md b/src/plugins/ui_actions/README.md index c4e02b551c8..28e3b2d63d2 100644 --- a/src/plugins/ui_actions/README.md +++ b/src/plugins/ui_actions/README.md @@ -1,6 +1,15 @@ # UI Actions -An API for: +This plugin exposes a global event bus for the OpenSearch Dashboards UI that allows other plugins to expand the ui capabilities of the application using `actions` and `triggers`. Plugins can not only register actions and triggers that trigger an action, but also use existing triggers and actions for their own use case. Multiple actions can be associated with a single trigger. All the capabilities are exposed using the uiActions service. + +Some of the uses in Dashboards for UI Actions are: + +1. For the context menus in a dashboard panel +2. Interacting directly with a visualization to trigger filters and to select time ranges. + +## API + +You can use the UI Actions service API's for the following use cases: - creating custom functionality (`actions`) - creating custom user interaction events (`triggers`) @@ -8,3 +17,83 @@ An API for: - emitting `trigger` events - executing `actions` attached to a given `trigger`. - exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger. + +The API for the service can be found in [./public/service/ui_actions_service.ts](./public/service/ui_actions_service.ts) + +## Usage + +### Creating an action + +```ts +const ACTION_ID = 'ACTION_ID'; + +// Declare the context mapping so that it is clear to the user what context the action should receive +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_ID]: ActionContext; + } +} + +// Create the action +const action = createAction({ + execute: async (context: ActionContext) => {}, // Action to execute when called + id: ACTION_ID, + // ...other action properties +}); + +// Register the action with the service +uiActions.registerAction(action); +``` + +### Creating a trigger + +```ts +const TRIGGER_ID = 'TRIGGER_ID'; + +// Declare the context mapping so that it is clear to the user what context the trigger should be called with +declare module '../../../src/plugins/ui_actions/public' { + export interface TriggerContextMapping { + [TRIGGER_ID]: TriggerContext; // The context that the trigger will execute with + } +} + +// Create the trigger +const trigger: Trigger<'TRIGGER_ID'> = { + id: TRIGGER_ID, +}; + +// Register the trigger +uiActions.registerTrigger(trigger); +``` + +### Attach an action to a trigger + +There are two ways to do this: + +1. Attach a registered action to a registered trigger + +```ts +uiActions.attachAction(TRIGGER_ID, ACTION_ID); +``` + +2. Register a action to a registered trigger (If the action is not registered, this method also registers the action) + +```ts +uiActions.addTriggerAction(TRIGGER_ID, action); +``` + +### Trigger an event + +Triggering an action is very simple. Just get the trigger using its ID and execute it with the appropriate context. + +```ts +uiActions.getTrigger(trigger.id).exec(context); +``` + +## Explorer + +Use the UI actions explorer in the Developer examples to learn more about the service and its features. It can be started up using the `--run-examples` flag and found under the `Developer examples` option in the main menu. + +```sh +yarn start --run-examples +``` diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index b38e2bd4e40..76fc2ddb3be 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -92,7 +92,7 @@ export interface Action * Returns a title to be displayed to the user. * @param context */ - getDisplayName(context: ActionExecutionContext): string; + getDisplayName(context: ActionExecutionContext): JSX.Element | string; /** * `UiComponent` to render when displaying this action as a context menu item. diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index fb39a01ed69..5610051dc3f 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -59,7 +59,7 @@ export class ActionInternal return this.definition.getIconType(context); } - public getDisplayName(context: Context): string { + public getDisplayName(context: Context): JSX.Element | string { if (!this.definition.getDisplayName) return `Action: ${this.id}`; return this.definition.getDisplayName(context); } diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts index 949595d6093..a22a660a032 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts @@ -36,20 +36,28 @@ const createTestAction = ({ type, dispayName, order, + grouping, }: { type?: string; dispayName: string; order?: number; + grouping?: any[]; }) => createAction({ type: type as any, // mapping doesn't matter for this test getDisplayName: () => dispayName, order, execute: async () => {}, + grouping, }); const resultMapper = (panel: EuiContextMenuPanelDescriptor) => ({ - items: panel.items ? panel.items.map((item) => ({ name: item.name })) : [], + items: panel.items + ? panel.items.map((item) => ({ + ...(item.name ? { name: item.name } : {}), + ...(item.isSeparator ? { isSeparator: true } : {}), + })) + : [], }); test('sorts items in DESC order by "order" field first, then by display name', async () => { @@ -248,3 +256,132 @@ test('hides items behind in "More" submenu if there are more than 4 actions', as ] `); }); + +test('tests groups and separators', async () => { + const grouping1 = [ + { + id: 'test-group', + getDisplayName: () => 'Test group', + getIconType: () => 'bell', + }, + ]; + const grouping2 = [ + { + id: 'test-group-2', + getDisplayName: () => 'Test group 2', + getIconType: () => 'bell', + }, + ]; + const grouping3 = [ + { + id: 'test-group-3', + getDisplayName: () => 'Test group 3', + getIconType: () => 'bell', + }, + { + id: 'test-group-4', + getDisplayName: () => 'Test group 4', + getIconType: () => 'bell', + }, + ]; + + const actions = [ + createTestAction({ + dispayName: 'First action', + }), + createTestAction({ + dispayName: 'Foo 1', + grouping: grouping1, + }), + createTestAction({ + dispayName: 'Foo 2', + grouping: grouping1, + }), + createTestAction({ + dispayName: 'Foo 3', + grouping: grouping1, + }), + createTestAction({ + dispayName: 'Bar 1', + grouping: grouping2, + }), + createTestAction({ + dispayName: 'Bar 2', + grouping: grouping2, + }), + createTestAction({ + dispayName: 'Inner', + grouping: grouping3, + }), + ]; + const menu = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: 'TEST' as any })), + }); + + expect(menu.map(resultMapper)).toMatchInlineSnapshot(` + Array [ + Object { + "items": Array [ + Object { + "name": "First action", + }, + Object { + "isSeparator": true, + }, + Object { + "name": "Test group", + }, + Object { + "isSeparator": true, + }, + Object { + "name": "Test group 2", + }, + Object { + "isSeparator": true, + }, + Object { + "name": "Test group 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Foo 1", + }, + Object { + "name": "Foo 2", + }, + Object { + "name": "Foo 3", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Bar 1", + }, + Object { + "name": "Bar 2", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Test group 4", + }, + ], + }, + Object { + "items": Array [ + Object { + "name": "Inner", + }, + ], + }, + ] + `); +}); diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 054900f52b7..6bde3439c97 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -157,35 +157,51 @@ export async function buildContextMenuForActions({ const context: ActionExecutionContext = { ...item.context, trigger: item.trigger }; const isCompatible = await item.action.isCompatible(context); if (!isCompatible) return; - let parentPanel = ''; - let currentPanel = ''; + + // Reference to the last group...groups are provided in array order of + // parent to children (or outer to inner) + let parentPanelId = ''; + if (action.grouping) { for (let i = 0; i < action.grouping.length; i++) { const group = action.grouping[i]; - currentPanel = group.id; - if (!panels[currentPanel]) { + const groupId = group.id; + + if (!panels[groupId]) { const name = group.getDisplayName ? group.getDisplayName(context) : group.id; - panels[currentPanel] = { - id: currentPanel, + + // Create panel for group + panels[groupId] = { + id: groupId, title: name, items: [], _level: i, _icon: group.getIconType ? group.getIconType(context) : 'empty', }; - if (parentPanel) { - panels[parentPanel].items!.push({ + + // If there are multiple groups and this is not the first group, + // then add an item to the parent panel relating to this group + if (parentPanelId) { + panels[parentPanelId].items!.push({ name, - panel: currentPanel, + panel: groupId, icon: group.getIconType ? group.getIconType(context) : 'empty', _order: group.order || 0, _title: group.getDisplayName ? group.getDisplayName(context) : '', }); } } - parentPanel = currentPanel; + + // Save the current panel, because this will be used for adding items + // to it from later groups in the array + parentPanelId = groupId; } } - panels[parentPanel || 'mainMenu'].items!.push({ + + // If grouping exists, parentPanelId will be the most-inner group, + // otherwise use the mainMenu panel. + // Add item for action to this most-inner panel or mainMenu. + panels[parentPanelId || 'mainMenu'].items!.push({ name: action.MenuItem ? React.createElement(uiToReactComponent(action.MenuItem), { context }) : action.getDisplayName(context), @@ -197,6 +213,7 @@ export async function buildContextMenuForActions({ _title: action.getDisplayName(context), }); }); + await Promise.all(promises); for (const panel of Object.values(panels)) { @@ -211,10 +228,17 @@ export async function buildContextMenuForActions({ wrapMainPanelItemsIntoSubmenu(panels, 'mainMenu'); for (const panel of Object.values(panels)) { + // If the panel is a root-level panel, such as a group-based panel, + // then create mainMenu item for this panel if (panel._level === 0) { - // TODO: Add separator line here once it is available in EUI. - // See https://github.com/elastic/eui/pull/4018 - if (panel.items.length > 3) { + // Add separator with unique key if needed + if (panels.mainMenu.items.length) { + panels.mainMenu.items.push({ isSeparator: true, key: `${panel.id}separator` }); + } + + // If a panel has more than one child, then allow item's to be grouped + // and link to it in the mainMenu. Otherwise, flatten the group. + if (panel.items.length > 1) { panels.mainMenu.items.push({ name: panel.title || panel.id, icon: panel._icon || 'empty', diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 3560f473d33..f0abdb1c870 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -71,6 +71,7 @@ export { ACTION_VISUALIZE_FIELD, ACTION_VISUALIZE_GEO_FIELD, ACTION_VISUALIZE_LENS_FIELD, + DEFAULT_ACTION, } from './types'; export { ActionByType, diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 276dfb24519..0dd2fc4cde4 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -88,6 +88,10 @@ export class UiActionsService { return trigger.contract; }; + public readonly getTriggers = (): TriggerRegistry => { + return this.triggers; + }; + public readonly registerAction = ( definition: A ): Action> => { diff --git a/src/plugins/ui_actions/public/util/presentable.ts b/src/plugins/ui_actions/public/util/presentable.ts index 428644e1c2c..aebf79cf913 100644 --- a/src/plugins/ui_actions/public/util/presentable.ts +++ b/src/plugins/ui_actions/public/util/presentable.ts @@ -60,7 +60,7 @@ export interface Presentable { /** * Returns a title to be displayed to the user. */ - getDisplayName(context: Context): string; + getDisplayName(context: Context): JSX.Element | string; /** * Returns tooltip text which should be displayed when user hovers this object. diff --git a/src/plugins/vis_builder/README.md b/src/plugins/vis_builder/README.md index 88b5afbda1f..4bbf82d9dc8 100755 --- a/src/plugins/vis_builder/README.md +++ b/src/plugins/vis_builder/README.md @@ -31,6 +31,6 @@ Outline: **Notes:** -- Currently only the metric viz is defined, so schema properties that other vis types might need may be missing and require further setup. +- Currently only the metric and table viz are defined, so schema properties that other vis types might need may be missing and require further setup. - `to_expression` has not yet been abstracted into a common utility for different visualizations. Adding more visualization types should make it easier to identify which parts of expression creation are common, and which are visualization-specific. diff --git a/src/plugins/vis_builder/config.ts b/src/plugins/vis_builder/config.ts index 79412f5c02e..b6be3f718ee 100644 --- a/src/plugins/vis_builder/config.ts +++ b/src/plugins/vis_builder/config.ts @@ -6,7 +6,7 @@ import { schema, TypeOf } from '@osd/config-schema'; export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_builder/public/application/app.tsx b/src/plugins/vis_builder/public/application/app.tsx index bd5f2be1fed..2bdc2b1c631 100644 --- a/src/plugins/vis_builder/public/application/app.tsx +++ b/src/plugins/vis_builder/public/application/app.tsx @@ -3,17 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage, EuiResizableContainer } from '@elastic/eui'; +import { useLocation } from 'react-router-dom'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; import { LeftNav } from './components/left_nav'; import { TopNav } from './components/top_nav'; import { Workspace } from './components/workspace'; import './app.scss'; import { RightNav } from './components/right_nav'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../types'; +import { syncQueryStateWithUrl } from '../../../data/public'; export const VisBuilderApp = () => { + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); + // Render the application DOM. return ( diff --git a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx b/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx index f6b7a6ca221..70b43a2c601 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; import { EuiButtonIcon, EuiDragDropContext, @@ -127,7 +128,11 @@ const DropboxComponent = ({ } ${canDrop ? 'canDrop' : ''}`} {...(isValidDropTarget && dropProps)} > - Click or drop to add + + {i18n.translate('visBuilder.dropbox.addField.title', { + defaultMessage: 'Click or drop to add', + })} + { + it('should render normal fields without a dragValue specified', async () => { + const props = { + field: new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ), + }; + render(); + + const button = screen.getByTestId('field-bytes-showDetails'); + + expect(button).toBeDefined(); + }); + + // TODO: it('should allow specified dragValue to override the field name'); + + // TODO: it('should make dots wrappable'); + + // TODO: it('should use a non-scripted FieldIcon by default'); + }); + + // TODO: describe('Field', function () { }); +}); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field.tsx new file mode 100644 index 00000000000..287c6aed621 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/field.tsx @@ -0,0 +1,113 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { EuiPopover } from '@elastic/eui'; + +import { IndexPatternField } from '../../../../../data/public'; +import { + FieldButton, + FieldButtonProps, + FieldIcon, +} from '../../../../../opensearch_dashboards_react/public'; + +import { COUNT_FIELD, useDrag } from '../../utils/drag_drop'; +import { FieldDetailsView } from './field_details'; +import { FieldDetails } from './types'; +import './field.scss'; + +export interface FieldProps { + field: IndexPatternField; + getDetails: (field) => FieldDetails; +} + +// TODO: Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) +export const Field = ({ field, getDetails }: FieldProps) => { + const [infoIsOpen, setOpen] = useState(false); + + function togglePopover() { + setOpen(!infoIsOpen); + } + + return ( + } + isOpen={infoIsOpen} + closePopover={() => setOpen(false)} + anchorPosition="rightUp" + panelClassName="vbItem__fieldPopoverPanel" + // TODO: make reposition on scroll actually work: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2782 + repositionOnScroll + data-test-subj="field-popover" + > + {infoIsOpen && } + + ); +}; + +export interface DraggableFieldButtonProps extends Partial { + dragValue?: IndexPatternField['name'] | null | typeof COUNT_FIELD; + field: Partial & Pick; +} + +export const DraggableFieldButton = ({ dragValue, field, ...rest }: DraggableFieldButtonProps) => { + const { name, displayName, type, scripted = false } = field; + const [dragProps] = useDrag({ + namespace: 'field-data', + value: dragValue ?? name, + }); + + function wrapOnDot(str: string) { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str.replace(/\./g, '.\u200B'); + } + + const defaultIcon = ; + + const defaultFieldName = ( + + {wrapOnDot(displayName)} + + ); + + const defaultProps = { + className: 'vbFieldButton', + dataTestSubj: `field-${name}-showDetails`, + fieldIcon: defaultIcon, + fieldName: defaultFieldName, + onClick: () => {}, + }; + + return ; +}; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.scss b/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.scss new file mode 100644 index 00000000000..50951d850a6 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.scss @@ -0,0 +1,4 @@ +.vbFieldDetails__barContainer { + // Constrains value to the flex item, and allows for truncation when necessary + min-width: 0; +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx new file mode 100644 index 00000000000..1a45857a655 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx @@ -0,0 +1,110 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiText, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiProgress, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +import { IndexPatternField } from '../../../../../data/public'; + +import { Bucket } from './types'; +import './field_bucket.scss'; +import { useOnAddFilter } from '../../utils/use'; + +interface FieldBucketProps { + bucket: Bucket; + field: IndexPatternField; +} + +export function FieldBucket({ bucket, field }: FieldBucketProps) { + const { count, display, percent, value } = bucket; + const { filterable: isFilterableField, name: fieldName } = field; + + const onAddFilter = useOnAddFilter(); + + const emptyText = i18n.translate('visBuilder.fieldSelector.detailsView.emptyStringText', { + // We need this to communicate to users when a top value is actually an empty string + defaultMessage: 'Empty string', + }); + const addLabel = i18n.translate( + 'visBuilder.fieldSelector.detailsView.filterValueButtonAriaLabel', + { + defaultMessage: 'Filter for {fieldName}: "{value}"', + values: { fieldName, value }, + } + ); + const removeLabel = i18n.translate( + 'visBuilder.fieldSelector.detailsView.filterOutValueButtonAriaLabel', + { + defaultMessage: 'Filter out {fieldName}: "{value}"', + values: { fieldName, value }, + } + ); + + const displayValue = display || emptyText; + + return ( + <> + + + + + + {displayValue} + + + + + {percent.toFixed(1)}% + + + + + + {/* TODO: Should we have any explanation for non-filterable fields? */} + {isFilterableField && ( + +
+ onAddFilter(field, value, '+')} + aria-label={addLabel} + data-test-subj={`plus-${fieldName}-${value}`} + /> + onAddFilter(field, value, '-')} + aria-label={removeLabel} + data-test-subj={`minus-${fieldName}-${value}`} + /> +
+
+ )} +
+ + + ); +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx new file mode 100644 index 00000000000..83a148b2f77 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx @@ -0,0 +1,155 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +// @ts-ignore +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; + +import { IndexPatternField } from '../../../../../data/public'; + +import { FieldDetailsView } from './field_details'; + +const mockUseIndexPatterns = jest.fn(() => ({ selected: 'mockIndexPattern' })); +const mockUseOnAddFilter = jest.fn(); +jest.mock('../../utils/use', () => ({ + useIndexPatterns: jest.fn(() => mockUseIndexPatterns), + useOnAddFilter: jest.fn(() => mockUseOnAddFilter), +})); + +describe('visBuilder field details', function () { + const defaultDetails = { buckets: [], error: '', exists: 1, total: 1 }; + function mountComponent(field: IndexPatternField, props?: Record) { + const compProps = { details: defaultDetails, ...props, field }; + return mountWithIntl(); + } + + it('should render buckets if they exist', async function () { + const field = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const buckets = [1, 2, 3].map((n) => ({ + display: `display-${n}`, + value: `value-${n}`, + percent: 25, + count: 100, + })); + const comp = mountComponent(field, { + details: { ...defaultDetails, buckets }, + }); + expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(0); + expect(findTestSubject(comp, 'fieldDetailsBucketsContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsBucketsContainer').children().length).toBe( + buckets.length + ); + expect(findTestSubject(comp, 'fieldDetailsExistsLink').length).toBe(1); + }); + + it('should only render buckets if they exist', async function () { + const field = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(field); + expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(0); + expect(findTestSubject(comp, 'fieldDetailsBucketsContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsBucketsContainer').children().length).toBe(0); + expect(findTestSubject(comp, 'fieldDetailsExistsLink').length).toBe(1); + }); + + it('should render a details error', async function () { + const field = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const errText = 'Some error'; + const comp = mountComponent(field, { + details: { ...defaultDetails, error: errText }, + }); + expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsBucketsContainer').children().length).toBe(0); + expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsError').text()).toBe(errText); + expect(findTestSubject(comp, 'fieldDetailsExistsLink').length).toBe(0); + }); + + it('should not render an exists filter link for scripted fields', async function () { + const field = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: true, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(field); + expect(findTestSubject(comp, 'fieldDetailsContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldDetailsError').length).toBe(0); + expect(findTestSubject(comp, 'fieldDetailsExistsLink').length).toBe(0); + }); +}); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx new file mode 100644 index 00000000000..cf6f4974bb1 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx @@ -0,0 +1,92 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiLink, EuiPopoverFooter, EuiPopoverTitle, EuiText } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +import { IndexPatternField } from '../../../../../data/public'; + +import { useIndexPatterns, useOnAddFilter } from '../../utils/use'; +import { FieldBucket } from './field_bucket'; +import { Bucket, FieldDetails } from './types'; + +interface FieldDetailsProps { + field: IndexPatternField; + details: FieldDetails; +} + +export function FieldDetailsView({ field, details }: FieldDetailsProps) { + const { buckets, error, exists, total } = details; + + const onAddFilter = useOnAddFilter(); + const indexPattern = useIndexPatterns().selected; + + const { metaFields = [] } = indexPattern ?? {}; + const isMetaField = metaFields.includes(field.name); + const shouldAllowExistsFilter = !isMetaField && !field.scripted; + + const bucketsTitle = + buckets.length > 1 + ? i18n.translate('visBuilder.fieldSelector.detailsView.fieldTopValuesLabel', { + defaultMessage: 'Top {n} values', + values: { n: buckets.length }, + }) + : i18n.translate('visBuilder.fieldSelector.detailsView.fieldTopValueLabel', { + defaultMessage: 'Top value', + }); + const errorTitle = i18n.translate('visBuilder.fieldSelector.detailsView.fieldNoValuesLabel', { + defaultMessage: 'No values found', + }); + const existsIn = i18n.translate('visBuilder.fieldSelector.detailsView.fieldExistsIn', { + defaultMessage: 'Exists in {exists}', + values: { exists }, + }); + const totalRecords = i18n.translate('visBuilder.fieldSelector.detailsView.fieldTotalRecords', { + defaultMessage: '/ {total} records', + values: { total }, + }); + + const title = buckets.length ? bucketsTitle : errorTitle; + + return ( + <> + {title} +
+ {error ? ( + + {error} + + ) : ( +
+ {buckets.map((bucket: Bucket, idx: number) => ( + + ))} +
+ )} +
+ {!error && ( + + + {shouldAllowExistsFilter ? ( + onAddFilter('_exists_', field.name, '+')} + data-test-subj="fieldDetailsExistsLink" + > + {existsIn} + + ) : ( + <>{exists} + )}{' '} + {totalRecords} + + + )} + + ); +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx new file mode 100644 index 00000000000..980cfb50c66 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { FilterManager, IndexPatternField } from '../../../../../data/public'; +import { FieldGroup } from './field_selector'; + +const mockUseIndexPatterns = jest.fn(() => ({ selected: 'mockIndexPattern' })); +const mockUseOnAddFilter = jest.fn(); +jest.mock('../../utils/use', () => ({ + useIndexPatterns: jest.fn(() => mockUseIndexPatterns), + useOnAddFilter: jest.fn(() => mockUseOnAddFilter), +})); + +const mockGetDetailsByField = jest.fn(() => ({ + buckets: [1, 2, 3].map((n) => ({ + display: `display-${n}`, + value: `value-${n}`, + percent: 25, + count: 100, + })), + error: '', + exists: 100, + total: 150, +})); + +const getFields = (name) => { + return new IndexPatternField( + { + name, + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + name + ); +}; + +describe('visBuilder sidebar field selector', function () { + const defaultProps = { + filterManager: {} as FilterManager, + getDetailsByField: mockGetDetailsByField, + header: 'mockHeader', + id: 'mockID', + }; + describe('FieldGroup', () => { + it('renders an empty accordion if no fields specified', async () => { + const { container } = render(); + + expect(container).toHaveTextContent(defaultProps.header); + expect(container).toHaveTextContent('0'); + expect(screen.queryAllByTestId('field-popover').length).toBeFalsy(); + + await fireEvent.click(screen.getByText(defaultProps.header)); + + expect(mockGetDetailsByField).not.toHaveBeenCalled(); + }); + + it('renders an accordion with Fields if fields provided', async () => { + const props = { + ...defaultProps, + fields: ['bytes', 'machine.ram', 'memory', 'phpmemory'].map(getFields), + }; + const { container } = render(); + + expect(container).toHaveTextContent(props.header); + expect(container).toHaveTextContent(props.fields.length.toString()); + expect(screen.queryAllByTestId('field-popover').length).toBe(props.fields.length); + + await fireEvent.click(screen.getByText('memory')); + + expect(mockGetDetailsByField).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx index 6d3831363c1..5c82419d553 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx @@ -3,21 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; -import { FieldSearch } from './field_search'; -import { - IndexPatternField, - OPENSEARCH_FIELD_TYPES, - OSD_FIELD_TYPES, -} from '../../../../../data/public'; -import { FieldSelectorField } from './field_selector_field'; +import { IndexPattern, IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; -import './field_selector.scss'; +import { COUNT_FIELD } from '../../utils/drag_drop'; import { useTypedSelector } from '../../utils/state_management'; -import { useIndexPatterns } from '../../utils/use'; -import { getAvailableFields } from './utils'; +import { useIndexPatterns, useSampleHits } from '../../utils/use'; +import { FieldSearch } from './field_search'; +import { Field, DraggableFieldButton } from './field'; +import { FieldDetails } from './types'; +import { getAvailableFields, getDetails } from './utils'; +import './field_selector.scss'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -25,22 +23,18 @@ interface IFieldCategories { meta: IndexPatternField[]; } -const META_FIELDS: string[] = [ - OPENSEARCH_FIELD_TYPES._ID, - OPENSEARCH_FIELD_TYPES._INDEX, - OPENSEARCH_FIELD_TYPES._SOURCE, - OPENSEARCH_FIELD_TYPES._TYPE, -]; - export const FieldSelector = () => { const indexPattern = useIndexPatterns().selected; const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + // TODO: instead of a single fetch of sampled hits for all fields, we should just use the agg service to get top hits or terms per field: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2780 + const hits = useSampleHits(); const [filteredFields, setFilteredFields] = useState([]); useEffect(() => { - const indexFields = indexPattern?.fields ?? []; + const indexFields = indexPattern?.fields.getAll() ?? []; const filteredSubset = getAvailableFields(indexFields).filter((field) => - field.displayName.includes(fieldSearchValue) + // case-insensitive field search + field.displayName.toLowerCase().includes(fieldSearchValue.toLowerCase()) ); setFilteredFields(filteredSubset); @@ -51,7 +45,7 @@ export const FieldSelector = () => { () => filteredFields?.reduce( (fieldGroups, currentField) => { - const category = getFieldCategory(currentField); + const category = getFieldCategory(currentField, indexPattern); fieldGroups[category].push(currentField); return fieldGroups; @@ -62,7 +56,14 @@ export const FieldSelector = () => { meta: [], } ), - [filteredFields] + [filteredFields, indexPattern] + ); + + const getDetailsByField = useCallback( + (ipField: IndexPatternField) => { + return getDetails(ipField, hits, indexPattern); + }, + [hits, indexPattern] ); return ( @@ -74,20 +75,30 @@ export const FieldSelector = () => {
{/* Count Field */} - + + - -
); @@ -95,37 +106,44 @@ export const FieldSelector = () => { interface FieldGroupProps { fields?: IndexPatternField[]; + getDetailsByField: (ipField: IndexPatternField) => FieldDetails; header: string; id: string; } -const FieldGroup = ({ fields, header, id }: FieldGroupProps) => ( - - {header} - - } - extraAction={ - - {fields?.length || 0} - - } - initialIsOpen - > - {fields?.map((field, i) => ( - - - - ))} - -); +export const FieldGroup = ({ fields, header, id, getDetailsByField }: FieldGroupProps) => { + return ( + + {header} + + } + extraAction={ + + {fields?.length || 0} + + } + initialIsOpen + > + {fields?.map((field, i) => ( + + + + ))} + + ); +}; -function getFieldCategory(field: IndexPatternField): keyof IFieldCategories { - if (META_FIELDS.includes(field.name)) return 'meta'; - if (field.type === OSD_FIELD_TYPES.NUMBER) return 'numerical'; +export const getFieldCategory = ( + { name, type }: IndexPatternField, + indexPattern: IndexPattern | undefined +): keyof IFieldCategories => { + const { metaFields = [] } = indexPattern ?? {}; + if (metaFields.includes(name)) return 'meta'; + if (type === OSD_FIELD_TYPES.NUMBER) return 'numerical'; return 'categorical'; -} +}; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector_field.tsx b/src/plugins/vis_builder/public/application/components/data_tab/field_selector_field.tsx deleted file mode 100644 index a87e2d184ee..00000000000 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector_field.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { useState } from 'react'; -import { IndexPatternField } from '../../../../../data/public'; -import { FieldButton, FieldIcon } from '../../../../../opensearch_dashboards_react/public'; -import { useDrag } from '../../utils/drag_drop/drag_drop_context'; -import { COUNT_FIELD } from '../../utils/drag_drop/types'; - -import './field_selector_field.scss'; - -export interface FieldSelectorFieldProps { - field: Partial & Pick; -} - -// TODO: -// 1. Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) -// 2. Add popover for fields stats from discover as well -export const FieldSelectorField = ({ field }: FieldSelectorFieldProps) => { - const [infoIsOpen, setOpen] = useState(false); - const [dragProps] = useDrag({ - namespace: 'field-data', - value: field.name || COUNT_FIELD, - }); - - function togglePopover() { - setOpen(!infoIsOpen); - } - - function wrapOnDot(str?: string) { - // u200B is a non-width white-space character, which allows - // the browser to efficiently word-wrap right after the dot - // without us having to draw a lot of extra DOM elements, etc - return str ? str.replace(/\./g, '.\u200B') : ''; - } - - const fieldName = ( - - {wrapOnDot(field.displayName)} - - ); - - return ( - } - // fieldAction={actionButton} - fieldName={fieldName} - {...dragProps} - /> - ); -}; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx index 385fcda9a80..18a1991f6d8 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx @@ -3,37 +3,42 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { Component, useCallback, useMemo, useState } from 'react'; import { cloneDeep, get } from 'lodash'; import { useDebounce } from 'react-use'; +import { i18n } from '@osd/i18n'; +import { EuiCallOut } from '@elastic/eui'; import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public'; import { Title } from './title'; import { useIndexPatterns, useVisualizationType } from '../../utils/use'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { + OpenSearchDashboardsContextProvider, + useOpenSearchDashboards, +} from '../../../../../opensearch_dashboards_react/public'; import { VisBuilderServices } from '../../../types'; import { AggParam, IAggType, IFieldParamType } from '../../../../../data/public'; import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_slice'; -import { setValidity } from '../../utils/state_management/metadata_slice'; +import { setError } from '../../utils/state_management/metadata_slice'; +import { Storage } from '../../../../../opensearch_dashboards_utils/public'; -const EDITOR_KEY = 'CONFIG_PANEL'; +const PANEL_KEY = 'SECONDARY_PANEL'; export function SecondaryPanel() { const { draftAgg, aggConfigParams } = useTypedSelector( (state) => state.visualization.activeVisualization! ); - const isEditorValid = useTypedSelector((state) => state.metadata.editor.validity[EDITOR_KEY]); + const isEditorValid = useTypedSelector((state) => !state.metadata.editor.errors[PANEL_KEY]); const [touched, setTouched] = useState(false); const dispatch = useTypedDispatch(); const vizType = useVisualizationType(); const indexPattern = useIndexPatterns().selected; + const { services } = useOpenSearchDashboards(); const { - services: { - data: { - search: { aggs: aggService }, - }, + data: { + search: { aggs: aggService }, }, - } = useOpenSearchDashboards(); + } = services; const schemas = vizType.ui.containerConfig.data.schemas.all; const aggConfigs = useMemo(() => { @@ -71,9 +76,9 @@ export function SecondaryPanel() { (isValid: boolean) => { // Set validity state globally dispatch( - setValidity({ - key: EDITOR_KEY, - valid: isValid, + setError({ + key: PANEL_KEY, + error: !isValid, }) ); }, @@ -98,49 +103,98 @@ export function SecondaryPanel() {
{showAggParamEditor && ( - <DefaultEditorAggParams - className="vbConfig__aggEditor" - agg={aggConfig!} - indexPattern={indexPattern!} - setValidity={handleSetValid} - setTouched={setTouched} - schemas={schemas} - formIsTouched={touched} - groupName={selectedSchema?.group ?? 'none'} - metricAggs={metricAggs} - state={{ - data: {}, - description: '', - title: '', + <OpenSearchDashboardsContextProvider + services={{ + ...services, + storage: new Storage(window.localStorage), // This is necessary for filters }} - setAggParamValue={function <T extends string | number | symbol>( - aggId: string, - paramName: T, - value: any - ): void { - aggConfig.params[paramName] = value; - dispatch(editDraftAgg(aggConfig.serialize())); - }} - onAggTypeChange={function (aggId: string, aggType: IAggType): void { - aggConfig.type = aggType; - - // Persist field if the new agg type supports the existing field - const fieldParam = (aggType.params as AggParam[]).find(({ type }) => type === 'field'); - if (fieldParam) { - const availableFields = (fieldParam as IFieldParamType).getAvailableFields(aggConfig); - const indexField = availableFields.find( - ({ name }) => name === get(draftAgg, 'params.field') - ); - - if (indexField) { - aggConfig.params.field = indexField; - } - } - - dispatch(editDraftAgg(aggConfig.serialize())); - }} - /> + > + <EditorErrorBoundary> + <DefaultEditorAggParams + className="vbConfig__aggEditor" + agg={aggConfig!} + indexPattern={indexPattern!} + setValidity={handleSetValid} + setTouched={setTouched} + schemas={schemas} + formIsTouched={touched} + groupName={selectedSchema?.group ?? 'none'} + metricAggs={metricAggs} + state={{ + data: {}, + description: '', + title: '', + }} + setAggParamValue={function <T extends string | number | symbol>( + aggId: string, + paramName: T, + value: any + ): void { + aggConfig.params[paramName] = value; + dispatch(editDraftAgg(aggConfig.serialize())); + }} + onAggTypeChange={function (aggId: string, aggType: IAggType): void { + aggConfig.type = aggType; + + // Persist field if the new agg type supports the existing field + const fieldParam = (aggType.params as AggParam[]).find( + ({ type }) => type === 'field' + ); + if (fieldParam) { + const availableFields = (fieldParam as IFieldParamType).getAvailableFields( + aggConfig + ); + const indexField = availableFields.find( + ({ name }) => name === get(draftAgg, 'params.field') + ); + + if (indexField) { + aggConfig.params.field = indexField; + } + } + + dispatch(editDraftAgg(aggConfig.serialize())); + }} + /> + </EditorErrorBoundary> + </OpenSearchDashboardsContextProvider> )} </div> ); } + +class EditorErrorBoundary extends Component<{}, { error?: any }> { + state = { + error: undefined, + }; + + static getDerivedStateFromError(error: any) { + return { error }; + } + + componentDidCatch(error) { + // eslint-disable-next-line no-console + console.error(error); + } + + render() { + if (this.state.error) { + return ( + <EuiCallOut + title={i18n.translate('visBuilder.aggParamsEditor.errorTitle', { + defaultMessage: 'Error', + })} + color="danger" + iconType="alert" + > + <p> + {i18n.translate('visBuilder.aggParamsEditor.errorMsg', { + defaultMessage: 'Something went wrong while editing the aggregation', + })} + </p> + </EuiCallOut> + ); + } + return this.props.children; + } +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/types.ts b/src/plugins/vis_builder/public/application/components/data_tab/types.ts new file mode 100644 index 00000000000..c7e0327070e --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface FieldDetails { + buckets: Bucket[]; + error: string; + exists: number; + total: number; +} + +export interface FieldValueCounts extends Partial<FieldDetails> { + missing?: number; +} + +export interface Bucket { + count: number; + display: string; + percent: number; + value: string; +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.test.ts b/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.test.ts new file mode 100644 index 00000000000..4f1dfd98fc3 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.test.ts @@ -0,0 +1,268 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +// @ts-ignore +import realHits from 'fixtures/real_hits.js'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { coreMock } from '../../../../../../../core/public/mocks'; +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; +import { Bucket } from '../types'; +import { + groupValues, + getFieldValues, + getFieldValueCounts, + FieldValueCountsParams, +} from './field_calculator'; + +let indexPattern: IndexPattern; + +describe('field_calculator', function () { + beforeEach(function () { + indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + }); + + describe('groupValues', function () { + let groups: Record<string, any>; + let grouped: boolean; + let values: any[]; + beforeEach(function () { + values = [ + ['foo', 'bar'], + 'foo', + 'foo', + undefined, + ['foo', 'bar'], + 'bar', + 'baz', + null, + null, + null, + 'foo', + undefined, + ]; + groups = groupValues(values, grouped); + }); + + it('should return an object', function () { + expect(groups).toBeInstanceOf(Object); + }); + + it('should throw an error if any value is a plain object', function () { + expect(function () { + groupValues([{}, true, false], grouped); + }).toThrowError(); + }); + + it('should handle values with dots in them', function () { + values = ['0', '0.........', '0.......,.....']; + groups = groupValues(values, grouped); + expect(groups[values[0]].count).toBe(1); + expect(groups[values[1]].count).toBe(1); + expect(groups[values[2]].count).toBe(1); + }); + + it('should have a key for value in the array when not grouping array terms', function () { + expect(_.keys(groups).length).toBe(3); + expect(groups.foo).toBeInstanceOf(Object); + expect(groups.bar).toBeInstanceOf(Object); + expect(groups.baz).toBeInstanceOf(Object); + }); + + it('should count array terms independently', function () { + expect(groups['foo,bar']).toBeUndefined(); + expect(groups.foo.count).toBe(5); + expect(groups.bar.count).toBe(3); + expect(groups.baz.count).toBe(1); + }); + + describe('grouped array terms', function () { + beforeEach(function () { + grouped = true; + groups = groupValues(values, grouped); + }); + + it('should group array terms when grouped is true', function () { + expect(_.keys(groups).length).toBe(4); + expect(groups['foo,bar']).toBeInstanceOf(Object); + }); + + it('should contain the original array as the value', function () { + expect(groups['foo,bar'].value).toEqual(['foo', 'bar']); + }); + + it('should count the pairs separately from the values they contain', function () { + expect(groups['foo,bar'].count).toBe(2); + expect(groups.foo.count).toBe(3); + expect(groups.bar.count).toBe(1); + }); + }); + }); + + describe('getFieldValues', function () { + let hits: any; + + beforeEach(function () { + hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); + }); + + it('should return an array of values for _source fields', function () { + const extensions = getFieldValues({ + hits, + field: indexPattern.fields.getByName('extension') as IndexPatternField, + indexPattern, + }); + expect(extensions).toBeInstanceOf(Array); + expect( + _.filter(extensions, function (v) { + return v === 'html'; + }).length + ).toBe(8); + expect(_.uniq(_.clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']); + }); + + it('should return an array of values for core meta fields', function () { + const types = getFieldValues({ + hits, + field: indexPattern.fields.getByName('_type') as IndexPatternField, + indexPattern, + }); + expect(types).toBeInstanceOf(Array); + expect( + _.filter(types, function (v) { + return v === 'apache'; + }).length + ).toBe(18); + expect(_.uniq(_.clone(types)).sort()).toEqual(['apache', 'nginx']); + }); + }); + + describe('getFieldValueCounts', function () { + let params: FieldValueCountsParams; + beforeEach(function () { + params = { + hits: _.cloneDeep(realHits), + field: indexPattern.fields.getByName('extension') as IndexPatternField, + count: 3, + indexPattern, + }; + }); + + it('counts the top 5 values by default', function () { + params.hits = params.hits.map((hit: Record<string, any>, i) => ({ + ...hit, + _source: { + extension: `${hit._source.extension}-${i}`, + }, + })); + params.count = undefined; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(5); + expect(extensions.error).toBeUndefined(); + }); + + it('counts only distinct values if less than default', function () { + params.count = undefined; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(4); + expect(extensions.error).toBeUndefined(); + }); + + it('counts only distinct values if less than specified count', function () { + params.count = 10; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(4); + expect(extensions.error).toBeUndefined(); + }); + + it('counts the top 3 values', function () { + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(3); + expect(_.map(buckets, 'value')).toEqual(['html', 'gif', 'php']); + expect(extensions.error).toBeUndefined(); + }); + + it('fails to analyze geo and attachment types', function () { + params.field = indexPattern.fields.getByName('point') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + + params.field = indexPattern.fields.getByName('area') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + + params.field = indexPattern.fields.getByName('request_body') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + }); + + it('fails to analyze fields that are in the mapping, but not the hits', function () { + params.field = indexPattern.fields.getByName('ip') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + }); + + it('counts the total hits', function () { + expect(getFieldValueCounts(params).total).toBe(params.hits.length); + }); + + it('counts the hits the field exists in', function () { + params.field = indexPattern.fields.getByName('phpmemory') as IndexPatternField; + expect(getFieldValueCounts(params).exists).toBe(5); + }); + + it('catches and returns errors', function () { + params.hits = params.hits.map((hit: Record<string, any>) => ({ + ...hit, + _source: { + extension: { foo: hit._source.extension }, + }, + })); + params.grouped = true; + expect(typeof getFieldValueCounts(params).error).toBe('string'); + }); + }); +}); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.ts b/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.ts new file mode 100644 index 00000000000..bd3cde945d9 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.ts @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { FieldValueCounts } from '../types'; + +const NO_ANALYSIS_TYPES = ['geo_point', 'geo_shape', 'attachment']; + +interface FieldValuesParams { + hits: Array<Record<string, unknown>>; + field: IndexPatternField; + indexPattern: IndexPattern; +} + +interface FieldValueCountsParams extends FieldValuesParams { + count?: number; + grouped?: boolean; +} + +const getFieldValues = ({ hits, field, indexPattern }: FieldValuesParams) => { + // For multi-value fields, we want to flatten based on the parent name instead + const name = field.subType?.multi?.parent ?? field.name; + const flattenHit = indexPattern.flattenHit; + return hits.map((hit) => flattenHit(hit)[name]); +}; + +const getFieldValueCounts = (params: FieldValueCountsParams): FieldValueCounts => { + const { hits, field, indexPattern, count = 5, grouped = false } = params; + const { type: fieldType } = field; + + if (NO_ANALYSIS_TYPES.includes(fieldType)) { + return { + error: i18n.translate( + 'visBuilder.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage', + { + defaultMessage: 'Analysis is not available for {fieldType} fields.', + values: { + fieldType, + }, + } + ), + }; + } + + const allValues = getFieldValues({ hits, field, indexPattern }); + const missing = allValues.filter((v) => v === undefined || v === null).length; + + try { + const groups = groupValues(allValues, grouped); + const counts = Object.keys(groups) + .sort((a, b) => groups[b].count - groups[a].count) + .slice(0, count) + .map((key) => ({ + value: groups[key].value, + count: groups[key].count, + percent: (groups[key].count / (hits.length - missing)) * 100, + display: indexPattern.getFormatterForField(field).convert(groups[key].value), + })); + + if (hits.length === missing) { + return { + error: i18n.translate( + 'visBuilder.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage', + { + defaultMessage: + 'This field is present in your OpenSearch mapping but not in the {hitsLength} documents sampled. You may still be able to visualize it.', + values: { + hitsLength: hits.length, + }, + } + ), + }; + } + + return { + total: hits.length, + exists: hits.length - missing, + missing, + buckets: counts, + }; + } catch (e) { + return { + error: e instanceof Error ? e.message : String(e), + }; + } +}; + +const groupValues = ( + allValues: any[], + grouped?: boolean +): Record<string, { value: any; count: number }> => { + const values = grouped ? allValues : allValues.flat(); + + return values + .filter((v) => { + if (v instanceof Object && !Array.isArray(v)) { + throw new Error( + i18n.translate( + 'visBuilder.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage', + { + defaultMessage: 'Analysis is not available for object fields.', + } + ) + ); + } + return v !== undefined && v !== null; + }) + .reduce((groups, value) => { + if (groups.hasOwnProperty(value)) { + groups[value].count++; + } else { + groups[value] = { + value, + count: 1, + }; + } + return groups; + }, {}); +}; + +export { FieldValueCountsParams, groupValues, getFieldValues, getFieldValueCounts }; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.test.ts b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.test.ts new file mode 100644 index 00000000000..60cb780245c --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FieldSpec } from '../../../../../../data/common'; +import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../../data/public'; +import { getAvailableFields } from './get_available_fields'; + +describe('getAvailableFields', () => { + const createIndexFields = (fields: Array<Partial<FieldSpec>>) => + fields.map( + (field) => + new IndexPatternField( + { + aggregatable: true, + name: 'field 1', + searchable: false, + type: OSD_FIELD_TYPES.STRING, + ...field, + }, + field.name || 'field' + ) + ); + + test('should return only aggregateable fields by default', () => { + const fields = createIndexFields([ + { + name: 'field 1', + }, + { + aggregatable: false, + name: 'field 2', + }, + { + scripted: true, + name: 'field 3', + }, + { + name: 'field 4', + subType: { + nested: { path: 'something' }, + }, + }, + ]); + + expect(getAvailableFields(fields).length).toBe(1); + }); + + test('should return all fields if filterFieldTypes was not specified', () => { + const fields = createIndexFields([ + { + name: 'field 1', + }, + { + name: 'field 2', + }, + ]); + + expect(getAvailableFields(fields).length).toBe(2); + }); + + test('should filterFieldTypes', () => { + const fields = createIndexFields([ + { + name: 'field 1', + }, + { + name: 'field 2', + type: OSD_FIELD_TYPES.BOOLEAN, + }, + { + name: 'field 3', + type: OSD_FIELD_TYPES.BOOLEAN, + }, + ]); + + expect(getAvailableFields(fields, OSD_FIELD_TYPES.BOOLEAN).length).toBe(2); + }); +}); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.test.ts b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.test.ts new file mode 100644 index 00000000000..6f9b9f0f856 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// @ts-ignore +import realHits from 'fixtures/real_hits.js'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { coreMock } from '../../../../../../../core/public/mocks'; +import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; +import { getFieldDetails } from './get_field_details'; +import _ from 'lodash'; + +describe('getFieldDetails', () => { + const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + + test('should have error if index pattern is missing', () => { + const details = getFieldDetails(indexPattern.fields[0], []); + + expect(details.total).toBe(0); + expect(details.error).toMatchInlineSnapshot(`"Index pattern not specified."`); + }); + + test('should have error if there are no hits', () => { + const details = getFieldDetails(indexPattern.fields[0], [], indexPattern); + + expect(details.total).toBe(0); + expect(details.error).toMatchInlineSnapshot( + `"No documents match the selected query and filters. Try increasing time range or removing filters."` + ); + }); + + test('should show details if hits are available for the index pattern field', () => { + const details = getFieldDetails( + indexPattern.fields[0], + _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)), + indexPattern + ); + + expect(details.exists).toBe(20); + expect(details.total).toBe(20); + expect(details.buckets.length).toBe(5); + expect(details.error).toBe(''); + }); +}); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.ts b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.ts new file mode 100644 index 00000000000..75b8b60c0c6 --- /dev/null +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.ts @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; + +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { FieldDetails } from '../types'; + +import { getFieldValueCounts } from './field_calculator'; + +export function getFieldDetails( + field: IndexPatternField, + hits: Array<Record<string, unknown>>, + indexPattern?: IndexPattern +): FieldDetails { + const defaultDetails = { + error: '', + exists: 0, + total: 0, + buckets: [], + }; + if (!indexPattern) { + return { + ...defaultDetails, + error: i18n.translate('visBuilder.fieldSelector.noIndexPatternSelectedErrorMessage', { + defaultMessage: 'Index pattern not specified.', + }), + }; + } + if (!hits.length) { + return { + ...defaultDetails, + error: i18n.translate('visBuilder.fieldSelector.noHits', { + defaultMessage: + 'No documents match the selected query and filters. Try increasing time range or removing filters.', + }), + }; + } + const details = { + ...defaultDetails, + ...getFieldValueCounts({ + hits, + field, + indexPattern, + count: 5, + grouped: false, + }), + }; + if (details.buckets) { + for (const bucket of details.buckets) { + bucket.display = indexPattern.getFormatterForField(field).convert(bucket.value); + } + } + return details; +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/index.ts b/src/plugins/vis_builder/public/application/components/data_tab/utils/index.ts index dd0cdea3e23..2900a66d8f1 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/utils/index.ts +++ b/src/plugins/vis_builder/public/application/components/data_tab/utils/index.ts @@ -4,3 +4,4 @@ */ export { getAvailableFields } from './get_available_fields'; +export { getFieldDetails as getDetails } from './get_field_details'; diff --git a/src/plugins/vis_builder/public/application/components/right_nav.tsx b/src/plugins/vis_builder/public/application/components/right_nav.tsx index d7e5586b3e9..c98638da28f 100644 --- a/src/plugins/vis_builder/public/application/components/right_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/right_nav.tsx @@ -17,7 +17,12 @@ import { useVisualizationType } from '../utils/use'; import './side_nav.scss'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { VisBuilderServices } from '../../types'; -import { setActiveVisualization, useTypedDispatch } from '../utils/state_management'; +import { + setActiveVisualization, + useTypedDispatch, + useTypedSelector, +} from '../utils/state_management'; +import { usePersistedAggParams } from '../utils/use/use_persisted_agg_params'; export const RightNav = () => { const [newVisType, setNewVisType] = useState<string>(); @@ -28,6 +33,15 @@ export const RightNav = () => { const dispatch = useTypedDispatch(); const StyleSection = ui.containerConfig.style.render; + const { activeVisualization } = useTypedSelector((state) => state.visualization); + const aggConfigParams = activeVisualization?.aggConfigParams ?? []; + const persistedAggParams = usePersistedAggParams( + types, + aggConfigParams, + activeVisName, + newVisType + ); + const options: Array<EuiSuperSelectOption<string>> = types.all().map(({ name, icon, title }) => ({ value: name, inputDisplay: <OptionItem icon={icon} title={title} />, @@ -68,6 +82,7 @@ export const RightNav = () => { setActiveVisualization({ name: newVisType, style: types.get(newVisType)?.ui.containerConfig.style.defaults, + aggConfigParams: persistedAggParams, }) ); diff --git a/src/plugins/vis_builder/public/application/components/top_nav.tsx b/src/plugins/vis_builder/public/application/components/top_nav.tsx index 62d3bb78cc5..768f2db3546 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx @@ -6,7 +6,6 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useUnmount } from 'react-use'; -import { PLUGIN_ID } from '../../../common'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getTopNavConfig } from '../utils/get_top_nav_config'; import { VisBuilderServices } from '../../types'; @@ -18,6 +17,7 @@ import { setEditorState } from '../utils/state_management/metadata_slice'; import { useCanSave } from '../utils/use/use_can_save'; import { saveStateToSavedObject } from '../../saved_visualizations/transforms'; import { TopNavMenuData } from '../../../../navigation/public'; +import { opensearchFilters, connectStorageToQueryState } from '../../../../data/public'; export const TopNav = () => { // id will only be set for the edit route @@ -28,14 +28,22 @@ export const TopNav = () => { navigation: { ui: { TopNavMenu }, }, + appName, } = services; const rootState = useTypedSelector((state) => state); const dispatch = useTypedDispatch(); const saveDisabledReason = useCanSave(); const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl); + connectStorageToQueryState(services.data.query, services.osdUrlStateStorage, { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + }); const { selected: indexPattern } = useIndexPatterns(); const [config, setConfig] = useState<TopNavMenuData[] | undefined>(); + const originatingApp = useTypedSelector((state) => { + return state.metadata.originatingApp; + }); useEffect(() => { const getConfig = () => { @@ -47,6 +55,7 @@ export const TopNav = () => { savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern), saveDisabledReason, dispatch, + originatingApp, }, services ); @@ -61,6 +70,7 @@ export const TopNav = () => { saveDisabledReason, dispatch, indexPattern, + originatingApp, ]); // reset validity before component destroyed @@ -71,7 +81,7 @@ export const TopNav = () => { return ( <div className="vbTopNav"> <TopNavMenu - appName={PLUGIN_ID} + appName={appName} config={config} setMenuMountPoint={setHeaderActionMenu} indexPatterns={indexPattern ? [indexPattern] : []} diff --git a/src/plugins/vis_builder/public/application/components/workspace.scss b/src/plugins/vis_builder/public/application/components/workspace.scss index c06e6060766..1d47dccd0e2 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.scss +++ b/src/plugins/vis_builder/public/application/components/workspace.scss @@ -30,6 +30,7 @@ $keyframe-multiplier: 1 / $animation-multiplier; animation: vbDragAnimation #{$total-duartion}s ease-in-out infinite forwards; position: absolute; top: 34.5%; + width: 50% !important; } } diff --git a/src/plugins/vis_builder/public/application/components/workspace.tsx b/src/plugins/vis_builder/public/application/components/workspace.tsx index 23c9f081fde..6e337140435 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.tsx +++ b/src/plugins/vis_builder/public/application/components/workspace.tsx @@ -3,14 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel } from '@elastic/eui'; import React, { FC, useState, useMemo, useEffect, useLayoutEffect } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { IExpressionLoaderParams } from '../../../../expressions/public'; import { VisBuilderServices } from '../../types'; -import { validateSchemaState } from '../utils/validate_schema_state'; +import { validateSchemaState, validateAggregations } from '../utils/validations'; import { useTypedSelector } from '../utils/state_management'; -import { useVisualizationType } from '../utils/use'; +import { useAggs, useVisualizationType } from '../utils/use'; import { PersistedState } from '../../../../visualizations/public'; import hand_field from '../../assets/hand_field.svg'; @@ -28,6 +29,7 @@ export const Workspace: FC = ({ children }) => { }, } = useOpenSearchDashboards<VisBuilderServices>(); const { toExpression, ui } = useVisualizationType(); + const { aggConfigs } = useAggs(); const [expression, setExpression] = useState<string>(); const [searchContext, setSearchContext] = useState<IExpressionLoaderParams['searchContext']>({ query: data.query.queryString.getQuery(), @@ -41,13 +43,17 @@ export const Workspace: FC = ({ children }) => { useEffect(() => { async function loadExpression() { const schemas = ui.containerConfig.data.schemas; - const [valid, errorMsg] = validateSchemaState(schemas, rootState.visualization); - if (!valid) { - if (errorMsg) { - toasts.addWarning(errorMsg); - } + const noAggs = aggConfigs?.aggs?.length === 0; + const schemaValidation = validateSchemaState(schemas, rootState.visualization); + const aggValidation = validateAggregations(aggConfigs?.aggs || []); + + if (noAggs || !aggValidation.valid || !schemaValidation.valid) { + const err = schemaValidation.errorMsg || aggValidation.errorMsg; + + if (err) toasts.addWarning(err); setExpression(undefined); + return; } @@ -56,7 +62,7 @@ export const Workspace: FC = ({ children }) => { } loadExpression(); - }, [rootState, toExpression, toasts, ui.containerConfig.data.schemas, searchContext]); + }, [rootState, toExpression, toasts, ui.containerConfig.data.schemas, searchContext, aggConfigs]); useLayoutEffect(() => { const subscription = data.query.state$.subscribe(({ state }) => { @@ -89,18 +95,29 @@ export const Workspace: FC = ({ children }) => { ) : ( <EuiFlexItem className="vbWorkspace__empty" data-test-subj="emptyWorkspace"> <EuiEmptyPrompt - title={<h2>Add a field to start</h2>} + title={ + <h2> + {i18n.translate('visBuilder.workSpace.empty.title', { + defaultMessage: 'Add a field to start', + })} + </h2> + } body={ <> - <p>Drag a field to the configuration panel to generate a visualization.</p> - <span className="vbWorkspace__container"> + <p> + {i18n.translate('visBuilder.workSpace.empty.description', { + defaultMessage: + 'Drag a field to the configuration panel to generate a visualization.', + })} + </p> + <div className="vbWorkspace__container"> <EuiIcon className="vbWorkspace__fieldSvg" type={fields_bg} size="original" /> <EuiIcon className="vbWorkspace__handFieldSvg" type={hand_field} size="original" /> - </span> + </div> </> } /> diff --git a/src/plugins/vis_builder/public/application/utils/drag_drop/drag_drop_context.tsx b/src/plugins/vis_builder/public/application/utils/drag_drop/drag_drop_context.tsx index c0f8725a501..b5c809c9b35 100644 --- a/src/plugins/vis_builder/public/application/utils/drag_drop/drag_drop_context.tsx +++ b/src/plugins/vis_builder/public/application/utils/drag_drop/drag_drop_context.tsx @@ -14,7 +14,7 @@ import React, { } from 'react'; import { DragDataType } from './types'; -// TODO: Replace any with corret type +// TODO: Replace any with correct type // TODO: Split into separate files interface IDragDropContext { data: DragDataType; diff --git a/src/plugins/vis_builder/public/application/utils/drag_drop/index.ts b/src/plugins/vis_builder/public/application/utils/drag_drop/index.ts index 3799a2eb605..4516a90575a 100644 --- a/src/plugins/vis_builder/public/application/utils/drag_drop/index.ts +++ b/src/plugins/vis_builder/public/application/utils/drag_drop/index.ts @@ -4,3 +4,4 @@ */ export * from './drag_drop_context'; +export * from './types'; diff --git a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx index c88bb13f3cb..9df32182285 100644 --- a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx @@ -46,22 +46,24 @@ export interface TopNavConfigParams { savedVisBuilderVis: VisBuilderVisSavedObject; saveDisabledReason?: string; dispatch: AppDispatch; + originatingApp?: string; } export const getTopNavConfig = ( - { visualizationIdFromUrl, savedVisBuilderVis, saveDisabledReason, dispatch }: TopNavConfigParams, + { + visualizationIdFromUrl, + savedVisBuilderVis, + saveDisabledReason, + dispatch, + originatingApp, + }: TopNavConfigParams, services: VisBuilderServices ) => { const { i18n: { Context: I18nContext }, embeddable, - scopedHistory, } = services; - const { originatingApp, embeddableId } = - embeddable - .getStateTransfer(scopedHistory) - .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const stateTransfer = embeddable.getStateTransfer(); const topNavConfig: TopNavMenuData[] = [ @@ -105,7 +107,7 @@ export const getTopNavConfig = ( showSaveModal(saveModal, I18nContext); }, }, - ...(originatingApp && ((savedVisBuilderVis && savedVisBuilderVis.id) || embeddableId) + ...(originatingApp && savedVisBuilderVis && savedVisBuilderVis.id ? [ { id: 'saveAndReturn', diff --git a/src/plugins/vis_builder/public/application/utils/mocks.ts b/src/plugins/vis_builder/public/application/utils/mocks.ts index df0687efaa6..25b9847986d 100644 --- a/src/plugins/vis_builder/public/application/utils/mocks.ts +++ b/src/plugins/vis_builder/public/application/utils/mocks.ts @@ -9,6 +9,7 @@ import { dataPluginMock } from '../../../../data/public/mocks'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; import { expressionsPluginMock } from '../../../../expressions/public/mocks'; import { navigationPluginMock } from '../../../../navigation/public/mocks'; +import { createOsdUrlStateStorage } from '../../../../opensearch_dashboards_utils/public'; import { VisBuilderServices } from '../../types'; export const createVisBuilderServicesMock = () => { @@ -16,10 +17,11 @@ export const createVisBuilderServicesMock = () => { const toastNotifications = coreStartMock.notifications.toasts; const applicationMock = coreStartMock.application; const i18nContextMock = coreStartMock.i18n.Context; - const indexPatternMock = dataPluginMock.createStartContract().indexPatterns; + const indexPatternMock = dataPluginMock.createStartContract(); const embeddableMock = embeddablePluginMock.createStartContract(); const navigationMock = navigationPluginMock.createStartContract(); const expressionMock = expressionsPluginMock.createStartContract(); + const osdUrlStateStorageMock = createOsdUrlStateStorage({ useHash: false }); const visBuilderServicesMock = { ...coreStartMock, @@ -39,6 +41,22 @@ export const createVisBuilderServicesMock = () => { data: indexPatternMock, embeddable: embeddableMock, scopedHistory: (scopedHistoryMock.create() as unknown) as ScopedHistory, + osdUrlStateStorage: osdUrlStateStorageMock, + types: { + all: () => [ + { + name: 'viz', + ui: { + containerConfig: { + style: { + defaults: 'style default states', + }, + }, + }, + }, + ], + get: jest.fn(), + }, }; return (visBuilderServicesMock as unknown) as jest.Mocked<VisBuilderServices>; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts b/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts new file mode 100644 index 00000000000..279a6cf4368 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { setEditorState } from '../metadata_slice'; +import { RootState, Store } from '../store'; + +export const handlerEditorState = (store: Store, state: RootState, previousState: RootState) => { + const { metadata, ...renderState } = state; + const { metadata: prevMetadata, ...prevRenderState } = previousState; + + // Need to make sure the editorStates are in the clean states(not the initial states) to indicate the viz finished loading + // Because when loading a saved viz from saved object, the previousStore will differ from + // the currentStore even tho there is no changes applied ( aggParams will + // first be empty, and it then will change to not empty once the viz finished loading) + if ( + prevMetadata.editor.state === 'clean' && + metadata.editor.state === 'clean' && + JSON.stringify(renderState) !== JSON.stringify(prevRenderState) + ) { + store.dispatch(setEditorState({ state: 'dirty' })); + } +}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts b/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts new file mode 100644 index 00000000000..255699852c8 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { findLast } from 'lodash'; +import { BUCKET_TYPES, IMetricAggType, search } from '../../../../../../data/public'; +import { VisBuilderServices } from '../../../../types'; +import { RootState, Store } from '../store'; +import { setAggParamValue } from '../visualization_slice'; + +/** + * Parent pipeline aggs when combined with histogram aggs need `min_doc_count` to be set appropriately to avoid an error + * on opensearch engine https://opensearch.org/docs/2.4/opensearch/pipeline-agg/#parent-aggregations + */ +export const handlerParentAggs = async ( + store: Store, + state: RootState, + services: VisBuilderServices +) => { + const { + visualization: { activeVisualization, indexPattern = '' }, + } = state; + + const { + data: { + indexPatterns, + search: { aggs: aggService }, + }, + } = services; + + if (!activeVisualization) return state; + + const aggConfigs = aggService.createAggConfigs( + await indexPatterns.get(indexPattern), + activeVisualization.aggConfigParams + ); + + // Pipeline aggs should have a valid bucket agg + const metricAggs = aggConfigs.aggs.filter((agg) => agg.schema === 'metric'); + const lastParentPipelineAgg = findLast( + metricAggs, + ({ type }: { type: IMetricAggType }) => type.subtype === search.aggs.parentPipelineType + ); + const lastBucket = findLast(aggConfigs.aggs, (agg) => agg.type.type === 'buckets'); + + aggConfigs.aggs.forEach((agg) => { + const isLastBucket = lastBucket?.id === agg.id; + // When a Parent Pipeline agg is selected and this agg is the last bucket. + const isLastBucketAgg = isLastBucket && lastParentPipelineAgg && agg.type; + + if ( + isLastBucketAgg && + ([BUCKET_TYPES.DATE_HISTOGRAM, BUCKET_TYPES.HISTOGRAM] as any).includes(agg.type.name) + ) { + store.dispatch( + setAggParamValue({ + aggId: agg.id, + paramName: 'min_doc_count', + // "histogram" agg has an editor for "min_doc_count" param, which accepts boolean + // "date_histogram" agg doesn't have an editor for "min_doc_count" param, it should be set as a numeric value + value: agg.type.name === 'histogram' ? true : 0, + }) + ); + } + }); +}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts index 8cc71804f12..05ceb324aaa 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts @@ -15,26 +15,34 @@ type EditorState = 'loading' | 'clean' | 'dirty'; export interface MetadataState { editor: { - validity: { - // Validity for each section in the editor + errors: { + // Errors for each section in the editor [key: string]: boolean; }; state: EditorState; }; + originatingApp?: string; } const initialState: MetadataState = { editor: { - validity: {}, + errors: {}, state: 'loading', }, + originatingApp: undefined, }; export const getPreloadedState = async ({ types, data, + embeddable, + scopedHistory, }: VisBuilderServices): Promise<MetadataState> => { - const preloadedState = { ...initialState }; + const { originatingApp } = + embeddable + .getStateTransfer(scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; + const preloadedState = { ...initialState, originatingApp }; return preloadedState; }; @@ -43,13 +51,16 @@ export const slice = createSlice({ name: 'metadata', initialState, reducers: { - setValidity: (state, action: PayloadAction<{ key: string; valid: boolean }>) => { - const { key, valid } = action.payload; - state.editor.validity[key] = valid; + setError: (state, action: PayloadAction<{ key: string; error: boolean }>) => { + const { key, error } = action.payload; + state.editor.errors[key] = error; }, setEditorState: (state, action: PayloadAction<{ state: EditorState }>) => { state.editor.state = action.payload.state; }, + setOriginatingApp: (state, action: PayloadAction<{ state?: string }>) => { + state.originatingApp = action.payload.state; + }, setState: (_state, action: PayloadAction<MetadataState>) => { return action.payload; }, @@ -57,4 +68,4 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setValidity, setEditorState, setState } = slice.actions; +export const { setError, setEditorState, setOriginatingApp, setState } = slice.actions; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx new file mode 100644 index 00000000000..91f760bbf23 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisBuilderServices } from '../../../types'; +import { createVisBuilderServicesMock } from '../mocks'; +import { loadReduxState, persistReduxState } from './redux_persistence'; +import { RootState } from './store'; + +describe('test redux state persistence', () => { + let mockServices: jest.Mocked<VisBuilderServices>; + let reduxStateParams: any; + + beforeEach(() => { + mockServices = createVisBuilderServicesMock(); + reduxStateParams = { + style: 'style', + visualization: 'visualization', + metadata: 'metadata', + }; + }); + + test('test load redux state when url is empty', async () => { + const defaultStates: RootState = { + style: 'style default states', + visualization: { + searchField: '', + activeVisualization: { name: 'viz', aggConfigParams: [] }, + indexPattern: 'id', + }, + metadata: { + editor: { errors: {}, state: 'loading' }, + originatingApp: undefined, + }, + }; + + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(defaultStates); + }); + + test('test load redux state', async () => { + mockServices.osdUrlStateStorage.set('_a', reduxStateParams, { replace: true }); + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(reduxStateParams); + }); + + test('test persist redux state', () => { + persistReduxState(reduxStateParams, mockServices); + const urlStates = mockServices.osdUrlStateStorage.get('_a'); + expect(urlStates).toStrictEqual(reduxStateParams); + }); +}); diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts new file mode 100644 index 00000000000..a531986a9ac --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisBuilderServices } from '../../../types'; +import { getPreloadedState } from './preload'; +import { RootState } from './store'; + +export const loadReduxState = async (services: VisBuilderServices) => { + try { + const serializedState = services.osdUrlStateStorage.get<RootState>('_a'); + if (serializedState !== null) return serializedState; + } catch (err) { + /* eslint-disable no-console */ + console.error(err); + /* eslint-enable no-console */ + } + + return await getPreloadedState(services); +}; + +export const persistReduxState = ( + { style, visualization, metadata }, + services: VisBuilderServices +) => { + try { + services.osdUrlStateStorage.set<RootState>( + '_a', + { style, visualization, metadata }, + { + replace: true, + } + ); + } catch (err) { + return; + } +}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/shared_actions.ts b/src/plugins/vis_builder/public/application/utils/state_management/shared_actions.ts index f1dc6ee027b..ebff4f91803 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/shared_actions.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/shared_actions.ts @@ -4,11 +4,13 @@ */ import { createAction } from '@reduxjs/toolkit'; +import { CreateAggConfigParams } from '../../../../../data/common'; import { VisualizationType } from '../../../services/type_service/visualization_type'; interface ActiveVisPayload { name: VisualizationType['name']; style: VisualizationType['ui']['containerConfig']['style']['defaults']; + aggConfigParams: CreateAggConfigParams[]; } export const setActiveVisualization = createAction<ActiveVisPayload>('setActiveVisualzation'); diff --git a/src/plugins/vis_builder/public/application/utils/state_management/store.ts b/src/plugins/vis_builder/public/application/utils/state_management/store.ts index f02fc5e946d..f1b1c0eeae2 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/store.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/store.ts @@ -4,12 +4,14 @@ */ import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; import { reducer as styleReducer } from './style_slice'; import { reducer as visualizationReducer } from './visualization_slice'; import { reducer as metadataReducer } from './metadata_slice'; import { VisBuilderServices } from '../../..'; -import { getPreloadedState } from './preload'; -import { setEditorState } from './metadata_slice'; +import { loadReduxState, persistReduxState } from './redux_persistence'; +import { handlerEditorState } from './handlers/editor_state'; +import { handlerParentAggs } from './handlers/parent_aggs'; const rootReducer = combineReducers({ style: styleReducer, @@ -25,43 +27,23 @@ export const configurePreloadedStore = (preloadedState: PreloadedState<RootState }; export const getPreloadedStore = async (services: VisBuilderServices) => { - const preloadedState = await getPreloadedState(services); + const preloadedState = await loadReduxState(services); const store = configurePreloadedStore(preloadedState); - const { metadata: metadataState, style: styleState, visualization: vizState } = store.getState(); - let previousStore = { - viz: vizState, - style: styleState, - }; - let previousMetadata = metadataState; + let previousState = store.getState(); // Listen to changes const handleChange = () => { - const { - metadata: currentMetadataState, - style: currentStyleState, - visualization: currentVizState, - } = store.getState(); - const currentStore = { - viz: currentVizState, - style: currentStyleState, - }; - const currentMetadata = currentMetadataState; + const state = store.getState(); + persistReduxState(state, services); + + if (isEqual(state, previousState)) return; - // Need to make sure the editorStates are in the clean states(not the initial states) to indicate the viz finished loading - // Because when loading a saved viz from saved object, the previousStore will differ from - // the currentStore even tho there is no changes applied ( aggParams will - // first be empty, and it then will change to not empty once the viz finished loading) - if ( - previousMetadata.editor.state === 'clean' && - currentMetadata.editor.state === 'clean' && - JSON.stringify(currentStore) !== JSON.stringify(previousStore) - ) { - store.dispatch(setEditorState({ state: 'dirty' })); - } + // Side effects to apply after changes to the store are made + handlerEditorState(store, state, previousState); + handlerParentAggs(store, state, services); - previousStore = currentStore; - previousMetadata = currentMetadata; + previousState = state; }; // the store subscriber will automatically detect changes and call handleChange function @@ -73,7 +55,7 @@ export const getPreloadedStore = async (services: VisBuilderServices) => { // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType<typeof rootReducer>; export type RenderState = Omit<RootState, 'metadata'>; // Remaining state after auxillary states are removed -type Store = ReturnType<typeof configurePreloadedStore>; +export type Store = ReturnType<typeof configurePreloadedStore>; export type AppDispatch = Store['dispatch']; export { setState as setStyleState, StyleState } from './style_slice'; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts index 2039c93e8ad..6662f9f43d7 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts @@ -98,6 +98,23 @@ export const slice = createSlice({ updateAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams[]>) => { state.activeVisualization!.aggConfigParams = action.payload; }, + setAggParamValue: ( + state, + action: PayloadAction<{ + aggId: string; + paramName: string; + value: any; + }> + ) => { + const aggIndex = state.activeVisualization!.aggConfigParams.findIndex( + (agg) => agg.id === action.payload.aggId + ); + + state.activeVisualization!.aggConfigParams[aggIndex].params = { + ...state.activeVisualization!.aggConfigParams[aggIndex].params, + [action.payload.paramName]: action.payload.value, + }; + }, setState: (_state, action: PayloadAction<VisualizationState>) => { return action.payload; }, @@ -106,7 +123,7 @@ export const slice = createSlice({ builder.addCase(setActiveVisualization, (state, action) => { state.activeVisualization = { name: action.payload.name, - aggConfigParams: [], + aggConfigParams: action.payload.aggConfigParams, }; }); }, @@ -119,6 +136,7 @@ export const { editDraftAgg, saveDraftAgg, updateAggConfigParams, + setAggParamValue, reorderAgg, setState, } = slice.actions; diff --git a/src/plugins/vis_builder/public/application/utils/use/index.ts b/src/plugins/vis_builder/public/application/utils/use/index.ts index 3ba3ca35907..1cc0b28dc89 100644 --- a/src/plugins/vis_builder/public/application/utils/use/index.ts +++ b/src/plugins/vis_builder/public/application/utils/use/index.ts @@ -4,6 +4,8 @@ */ export { useAggs } from './use_aggs'; -export { useVisualizationType } from './use_visualization_type'; export { useIndexPatterns } from './use_index_pattern'; +export { useOnAddFilter } from './use_on_add_filter'; +export { useSampleHits } from './use_sample_hits'; export { useSavedVisBuilderVis } from './use_saved_vis_builder_vis'; +export { useVisualizationType } from './use_visualization_type'; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts b/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts new file mode 100644 index 00000000000..791521fccad --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback } from 'react'; +import { IndexPatternField, opensearchFilters } from '../../../../../data/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../../../types'; +import { useIndexPatterns } from './use_index_pattern'; + +export const useOnAddFilter = () => { + const { + services: { + data: { + query: { filterManager }, + }, + }, + } = useOpenSearchDashboards<VisBuilderServices>(); + const indexPattern = useIndexPatterns().selected; + const { id = '' } = indexPattern ?? {}; + return useCallback( + (fieldToFilter: IndexPatternField | string, value: string, operation: '+' | '-') => { + const newFilters = opensearchFilters.generateFilters( + filterManager, + fieldToFilter, + value, + operation, + id + ); + return filterManager.addFilters(newFilters); + }, + [filterManager, id] + ); +}; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_persisted_agg_params.test.tsx b/src/plugins/vis_builder/public/application/utils/use/use_persisted_agg_params.test.tsx new file mode 100644 index 00000000000..6011ac33f71 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_persisted_agg_params.test.tsx @@ -0,0 +1,352 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CreateAggConfigParams } from '../../../../../data/common'; +import { AggGroupNames } from '../../../../../data/common'; +import { Schema } from '../../../../../vis_default_editor/public'; +import { VisBuilderServices } from '../../../types'; +import { createVisBuilderServicesMock } from '../mocks'; +import { + AggMapping, + getSchemaMapping, + updateAggParams, + usePersistedAggParams, +} from './use_persisted_agg_params'; + +describe('new usePersistedAggParams', () => { + const types = { + get: jest.fn(), + }; + let schemaMetricTemplate: Schema; + let schemaBucketTemplate: Schema; + let oldVisualizationType: Schema[]; + let newVisualizationType: Schema[]; + let aggConfigParam: CreateAggConfigParams; + let aggConfigParams: CreateAggConfigParams[]; + + beforeEach(() => { + schemaMetricTemplate = { + aggFilter: [], + editor: '', + group: AggGroupNames.Metrics, + max: 1, + min: 1, + name: '', + params: [], + title: '', + defaults: '', + }; + schemaBucketTemplate = { + aggFilter: [], + editor: '', + group: AggGroupNames.Buckets, + max: 1, + min: 1, + name: '', + params: [], + title: '', + defaults: '', + }; + aggConfigParam = { + type: '', + schema: '', + }; + }); + + test('return the correct metric-to-metric, bucket-to-bucket mapping and correct persisted aggregations', () => { + oldVisualizationType = [ + { ...schemaMetricTemplate, name: 'old metric 1' }, + { ...schemaBucketTemplate, name: 'old bucket 1' }, + { ...schemaMetricTemplate, name: 'old metric 2' }, + { ...schemaBucketTemplate, name: 'old bucket 2' }, + { ...schemaBucketTemplate, name: 'old bucket 3' }, + ]; + newVisualizationType = [ + { ...schemaMetricTemplate, name: 'new metric 1', max: 1 }, + { ...schemaMetricTemplate, name: 'new metric 2', max: 2 }, + { ...schemaBucketTemplate, name: 'new bucket 1', max: 4 }, + { ...schemaBucketTemplate, name: 'new bucket 2', max: 5 }, + { ...schemaBucketTemplate, name: 'new bucket 3', max: 6 }, + ]; + types.get + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: oldVisualizationType, + }, + }, + }, + }, + }) + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: newVisualizationType, + }, + }, + }, + }, + }); + aggConfigParams = [ + { ...aggConfigParam, schema: 'old metric 1' }, + { ...aggConfigParam, schema: 'old bucket 1' }, + { ...aggConfigParam, schema: 'old metric 2' }, + { ...aggConfigParam, schema: 'old bucket 2' }, + { ...aggConfigParam, schema: 'old bucket 3' }, + { ...aggConfigParam, schema: 'old metric 2' }, + ]; + + const mappingResult = getSchemaMapping(oldVisualizationType, newVisualizationType); + expect(mappingResult).toMatchInlineSnapshot(` + Map { + "old metric 1" => Object { + "currentCount": 0, + "maxCount": 1, + "name": "new metric 1", + }, + "old metric 2" => Object { + "currentCount": 0, + "maxCount": 2, + "name": "new metric 2", + }, + "old bucket 1" => Object { + "currentCount": 0, + "maxCount": 4, + "name": "new bucket 1", + }, + "old bucket 2" => Object { + "currentCount": 0, + "maxCount": 5, + "name": "new bucket 2", + }, + "old bucket 3" => Object { + "currentCount": 0, + "maxCount": 6, + "name": "new bucket 3", + }, + } + `); + const persistResult = usePersistedAggParams( + types, + aggConfigParams, + 'old vis type', + 'new vis type' + ); + expect(persistResult).toMatchInlineSnapshot(` + Array [ + Object { + "schema": "new metric 1", + "type": "", + }, + Object { + "schema": "new bucket 1", + "type": "", + }, + Object { + "schema": "new metric 2", + "type": "", + }, + Object { + "schema": "new bucket 2", + "type": "", + }, + Object { + "schema": "new bucket 3", + "type": "", + }, + Object { + "schema": "new metric 2", + "type": "", + }, + ] + `); + }); + + test('drop the schema fields when it can not be mapped or do not belong to either metric or bucket group', () => { + oldVisualizationType = [ + { ...schemaMetricTemplate, name: 'old metric 1' }, + { ...schemaMetricTemplate, name: 'old metric 2' }, + { ...schemaBucketTemplate, name: 'old bucket 1' }, + { ...schemaMetricTemplate, name: 'undefined group', group: AggGroupNames.None }, + ]; + newVisualizationType = [ + { ...schemaMetricTemplate, name: 'new metric 1', max: 1 }, + { ...schemaBucketTemplate, name: 'new bucket 2', max: 1 }, + ]; + types.get + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: oldVisualizationType, + }, + }, + }, + }, + }) + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: newVisualizationType, + }, + }, + }, + }, + }); + aggConfigParams = [ + { ...aggConfigParam, schema: 'old metric 1' }, + { ...aggConfigParam, schema: 'old bucket 1' }, + ]; + + const mappingResult = getSchemaMapping(oldVisualizationType, newVisualizationType); + expect(mappingResult).toMatchInlineSnapshot(` + Map { + "old metric 1" => Object { + "currentCount": 0, + "maxCount": 1, + "name": "new metric 1", + }, + "old bucket 1" => Object { + "currentCount": 0, + "maxCount": 1, + "name": "new bucket 2", + }, + } + `); + const persistResult = usePersistedAggParams( + types, + aggConfigParams, + 'old vis type', + 'new vis type' + ); + expect(persistResult).toMatchInlineSnapshot(` + Array [ + Object { + "schema": "new metric 1", + "type": "", + }, + Object { + "schema": "new bucket 2", + "type": "", + }, + ] + `); + }); + + test('aggregations with undefined schema remain undefined; schema will be set to undefined if aggregations that exceeds the max amount', () => { + oldVisualizationType = [{ ...schemaMetricTemplate, name: 'old metric 1' }]; + newVisualizationType = [{ ...schemaMetricTemplate, name: 'new metric 1', max: 1 }]; + types.get + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: oldVisualizationType, + }, + }, + }, + }, + }) + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: newVisualizationType, + }, + }, + }, + }, + }); + aggConfigParams = [ + { ...aggConfigParam, schema: undefined }, + { ...aggConfigParam, schema: 'old metric 1' }, + { ...aggConfigParam, schema: 'old metric 1' }, + ]; + + const mappingResult = getSchemaMapping(oldVisualizationType, newVisualizationType); + expect(mappingResult).toMatchInlineSnapshot(` + Map { + "old metric 1" => Object { + "currentCount": 0, + "maxCount": 1, + "name": "new metric 1", + }, + } + `); + const persistResult = usePersistedAggParams( + types, + aggConfigParams, + 'old vis type', + 'new vis type' + ); + expect(persistResult).toMatchInlineSnapshot(` + Array [ + Object { + "schema": undefined, + "type": "", + }, + Object { + "schema": "new metric 1", + "type": "", + }, + Object { + "schema": undefined, + "type": "", + }, + ] + `); + }); + + test('return an empty array when there are no aggregations for persistence', () => { + oldVisualizationType = [{ ...schemaMetricTemplate, name: 'old metric 1' }]; + newVisualizationType = [{ ...schemaMetricTemplate, name: 'new metric 1', max: 1 }]; + types.get + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: oldVisualizationType, + }, + }, + }, + }, + }) + .mockReturnValueOnce({ + ui: { + containerConfig: { + data: { + schemas: { + all: newVisualizationType, + }, + }, + }, + }, + }); + + aggConfigParams = []; + const persistResult = usePersistedAggParams( + types, + aggConfigParams, + 'old vis type', + 'new vis type' + ); + expect(persistResult).toMatchInlineSnapshot(`Array []`); + }); + + test('return an empty array when there are no new vis type or old vis type', () => { + const persistResult = usePersistedAggParams(types, aggConfigParams); + expect(persistResult).toMatchInlineSnapshot(`Array []`); + }); +}); diff --git a/src/plugins/vis_builder/public/application/utils/use/use_persisted_agg_params.ts b/src/plugins/vis_builder/public/application/utils/use/use_persisted_agg_params.ts new file mode 100644 index 00000000000..980c5f3e9f3 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_persisted_agg_params.ts @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AggGroupNames, CreateAggConfigParams } from '../../../../../data/common'; +import { Schema } from '../../../../../vis_default_editor/public'; + +export const usePersistedAggParams = ( + types, + aggConfigParams: CreateAggConfigParams[], + oldVisType?: string, + newVisType?: string +): CreateAggConfigParams[] => { + if (oldVisType && newVisType) { + const oldVisualizationType = types.get(oldVisType)?.ui.containerConfig.data.schemas.all; + const newVisualizationType = types.get(newVisType)?.ui.containerConfig.data.schemas.all; + const aggMapping = getSchemaMapping(oldVisualizationType, newVisualizationType); + const updatedAggConfigParams = aggConfigParams.map((aggConfigParam: CreateAggConfigParams) => + updateAggParams(aggConfigParam, aggMapping) + ); + return updatedAggConfigParams; + } + return []; +}; + +// Map metric fields to metric fields, bucket fields to bucket fields +export const getSchemaMapping = ( + oldVisualizationType: Schema[], + newVisualizationType: Schema[] +): Map<string, AggMapping> => { + const aggMap = new Map<string, AggMapping>(); + + // currently Metrics, Buckets, and None are the three groups. We simply drop the aggregations that belongs to the None group + mapAggParamsSchema(oldVisualizationType, newVisualizationType, AggGroupNames.Metrics, aggMap); + mapAggParamsSchema(oldVisualizationType, newVisualizationType, AggGroupNames.Buckets, aggMap); + + return aggMap; +}; + +export interface AggMapping { + name: string; + maxCount: number; + currentCount: number; +} + +export const mapAggParamsSchema = ( + oldVisualizationType: Schema[], + newVisualizationType: Schema[], + aggGroup: string, + map: Map<string, AggMapping> +) => { + const oldSchemas = oldVisualizationType.filter((type) => type.group === aggGroup); + const newSchemas = newVisualizationType.filter((type) => type.group === aggGroup); + + oldSchemas.forEach((oldSchema, index) => { + if (newSchemas[index]) { + const mappedNewSchema = { + name: newSchemas[index].name, + maxCount: newSchemas[index].max, + currentCount: 0, + }; + map.set(oldSchema.name, mappedNewSchema); + } + }); +}; + +export const updateAggParams = ( + oldAggParam: CreateAggConfigParams, + aggMap: Map<string, AggMapping> +) => { + const newAggParam = { ...oldAggParam }; + if (oldAggParam.schema) { + const newSchema = aggMap.get(oldAggParam.schema); + newAggParam.schema = newSchema + ? newSchema.currentCount < newSchema.maxCount + ? assignNewSchemaType(oldAggParam, aggMap, newSchema) + : undefined + : undefined; + } + return newAggParam; +}; + +export const assignNewSchemaType = ( + oldAggParam: any, + aggMap: Map<string, AggMapping>, + newSchema: AggMapping +) => { + aggMap.set(oldAggParam.schema, { + name: newSchema.name, + maxCount: newSchema.maxCount, + currentCount: newSchema.currentCount + 1, + }); + return aggMap.get(oldAggParam.schema)?.name; +}; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts b/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts new file mode 100644 index 00000000000..f3ed75a4dd6 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useLayoutEffect, useState } from 'react'; +import { SortDirection } from '../../../../../data/public'; +import { IExpressionLoaderParams } from '../../../../../expressions/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../../../types'; +import { useIndexPatterns } from './use_index_pattern'; + +export const useSampleHits = () => { + const { + services: { + data: { + query: { + filterManager, + queryString, + state$, + timefilter: { timefilter }, + }, + search: { searchSource }, + }, + uiSettings: config, + }, + } = useOpenSearchDashboards<VisBuilderServices>(); + const indexPattern = useIndexPatterns().selected; + const [hits, setHits] = useState<Array<Record<string, any>>>([]); + const [searchContext, setSearchContext] = useState<IExpressionLoaderParams['searchContext']>({ + query: queryString.getQuery(), + filters: filterManager.getFilters(), + }); + + useEffect(() => { + async function getData() { + if (indexPattern && searchContext) { + const newSearchSource = await searchSource.create(); + const timeRangeFilter = timefilter.createFilter(indexPattern); + + newSearchSource + .setField('index', indexPattern) + .setField('size', config.get('discover:sampleSize') ?? 500) + .setField('sort', [{ [indexPattern.timeFieldName || '_score']: 'desc' as SortDirection }]) + .setField('filter', [ + ...(searchContext.filters ?? []), + ...(timeRangeFilter ? [timeRangeFilter] : []), + ]); + + if (searchContext.query) { + const contextQuery = + searchContext.query instanceof Array ? searchContext.query[0] : searchContext.query; + + newSearchSource.setField('query', contextQuery); + } + + const searchResponse = await newSearchSource.fetch(); + + setHits(searchResponse.hits.hits); + } + } + + getData(); + }, [config, searchContext, searchSource, indexPattern, timefilter]); + + useLayoutEffect(() => { + const subscription = state$.subscribe(({ state }) => { + setSearchContext({ + query: state.query, + filters: state.filters, + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [state$]); + + return hits; +}; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts index d7840b92f8a..29c14dc07b0 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts @@ -13,7 +13,6 @@ import { } from '../../../../../opensearch_dashboards_utils/public'; import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; import { VisBuilderServices } from '../../../types'; -import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz_type'; import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; import { getSavedVisBuilderVis } from '../get_saved_vis_builder_vis'; import { @@ -24,7 +23,7 @@ import { } from '../state_management'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { setEditorState } from '../state_management/metadata_slice'; -import { validateVisBuilderState } from '../vis_builder_state_validation'; +import { validateVisBuilderState } from '../validations/vis_builder_state_validation'; // This function can be used when instantiating a saved vis or creating a new one // using url parameters, embedding and destroying it in DOM @@ -41,7 +40,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined http: { basePath }, toastNotifications, } = services; - const toastNotification = (message) => { + const toastNotification = (message: string) => { toastNotifications.addDanger({ title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { defaultMessage: 'Failed to load the visualization', @@ -49,6 +48,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined text: message, }); }; + const loadSavedVisBuilderVis = async () => { try { const savedVisBuilderVis = await getSavedVisBuilderVis(services, visualizationIdFromUrl); @@ -74,14 +74,16 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined const validateResult = validateVisBuilderState({ styleState, visualizationState }); if (!validateResult.valid) { - const err = validateResult.errors; - if (err) { - const errMsg = err[0].instancePath + ' ' + err[0].message; - throw new InvalidJSONProperty(errMsg); - } + throw new InvalidJSONProperty( + validateResult.errorMsg || + i18n.translate('visBuilder.useSavedVisBuilderVis.genericJSONError', { + defaultMessage: + 'Something went wrong while loading your saved object. The object may be corrupted or does not match the latest schema', + }) + ); } - dispatch(setStyleState<MetricOptionsDefaults>(styleState)); + dispatch(setStyleState(styleState)); dispatch(setVisualizationState(visualizationState)); } diff --git a/src/plugins/vis_builder/public/application/utils/validations/index.ts b/src/plugins/vis_builder/public/application/utils/validations/index.ts new file mode 100644 index 00000000000..2986b354f66 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/validations/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './validate_aggregations'; +export * from './validate_schema_state'; +export * from './vis_builder_state_validation'; diff --git a/src/plugins/vis_builder/public/application/utils/validations/types.ts b/src/plugins/vis_builder/public/application/utils/validations/types.ts new file mode 100644 index 00000000000..2763c476f2d --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/validations/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface ValidationResult<T = boolean> { + errorMsg?: string; + valid: T; +} diff --git a/src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.test.ts b/src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.test.ts new file mode 100644 index 00000000000..bec1ae50692 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BUCKET_TYPES, IndexPattern, METRIC_TYPES } from '../../../../../data/public'; +import { dataPluginMock } from '../../../../../data/public/mocks'; +import { validateAggregations } from './validate_aggregations'; + +describe('validateAggregations', () => { + const fields = [ + { + name: '@timestamp', + }, + { + name: 'bytes', + }, + ]; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: (name: string) => fields.find((f) => f.name === name), + filter: () => fields, + }, + } as any; + + const dataStart = dataPluginMock.createStartContract(); + + test('Pipeline aggs should have a bucket agg as the last agg', () => { + const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [ + { + id: '1', + enabled: true, + type: METRIC_TYPES.CUMULATIVE_SUM, + schema: 'metric', + params: {}, + }, + ]); + + const { valid, errorMsg } = validateAggregations(aggConfigs.aggs); + + expect(valid).toBe(false); + expect(errorMsg).toMatchInlineSnapshot( + `"Add a bucket with \\"Date Histogram\\" or \\"Histogram\\" aggregation."` + ); + }); + + test('Pipeline aggs should have a valid bucket agg', () => { + const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [ + { + id: '0', + enabled: true, + type: BUCKET_TYPES.SIGNIFICANT_TERMS, + schema: 'segment', + params: {}, + }, + { + id: '1', + enabled: true, + type: METRIC_TYPES.CUMULATIVE_SUM, + schema: 'metric', + params: {}, + }, + ]); + + const { valid, errorMsg } = validateAggregations(aggConfigs.aggs); + + expect(valid).toBe(false); + expect(errorMsg).toMatchInlineSnapshot( + `"Last bucket aggregation must be \\"Date Histogram\\" or \\"Histogram\\" when using \\"Cumulative Sum\\" metric aggregation."` + ); + }); + + test('Valid pipeline aggconfigs', () => { + const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [ + { + id: '0', + enabled: true, + type: BUCKET_TYPES.DATE_HISTOGRAM, + schema: 'segment', + params: {}, + }, + { + id: '1', + enabled: true, + type: METRIC_TYPES.CUMULATIVE_SUM, + schema: 'metric', + params: {}, + }, + ]); + + const { valid, errorMsg } = validateAggregations(aggConfigs.aggs); + + expect(valid).toBe(true); + expect(errorMsg).not.toBeDefined(); + }); +}); diff --git a/src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.ts b/src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.ts new file mode 100644 index 00000000000..470c83e9689 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { findLast } from 'lodash'; +import { AggConfig, BUCKET_TYPES, IMetricAggType } from '../../../../../data/common'; +import { search } from '../../../../../data/public'; +import { ValidationResult } from './types'; + +/** + * Validate if the aggregations to perform are possible + * @param aggs Aggregations to be performed + * @returns ValidationResult + */ +export const validateAggregations = (aggs: AggConfig[]): ValidationResult => { + // Pipeline aggs should have a valid bucket agg + const metricAggs = aggs.filter((agg) => agg.schema === 'metric'); + const lastParentPipelineAgg = findLast( + metricAggs, + ({ type }: { type: IMetricAggType }) => type.subtype === search.aggs.parentPipelineType + ); + const lastBucket = findLast(aggs, (agg) => agg.type.type === 'buckets'); + + if (!lastBucket && lastParentPipelineAgg) { + return { + valid: false, + errorMsg: i18n.translate('visBuilder.aggregation.mustHaveBucketErrorMessage', { + defaultMessage: 'Add a bucket with "Date Histogram" or "Histogram" aggregation.', + description: 'Date Histogram and Histogram should not be translated', + }), + }; + } + + // Last bucket in a Pipeline aggs should be either a date histogram or histogram + if ( + lastBucket && + lastParentPipelineAgg && + !([BUCKET_TYPES.DATE_HISTOGRAM, BUCKET_TYPES.HISTOGRAM] as any).includes(lastBucket.type.name) + ) { + return { + valid: false, + errorMsg: i18n.translate('visBuilder.aggregation.wrongLastBucketTypeErrorMessage', { + defaultMessage: + 'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation.', + values: { type: (lastParentPipelineAgg as AggConfig).type.title }, + description: 'Date Histogram and Histogram should not be translated', + }), + }; + } + + return { valid: true }; +}; diff --git a/src/plugins/vis_builder/public/application/utils/validations/validate_schema_state.test.ts b/src/plugins/vis_builder/public/application/utils/validations/validate_schema_state.test.ts new file mode 100644 index 00000000000..a0c017cec3c --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/validations/validate_schema_state.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Schemas } from '../../../../../vis_default_editor/public'; +import { VisualizationState } from '../state_management'; +import { validateSchemaState } from './validate_schema_state'; + +describe('validateSchemaState', () => { + const schemas = new Schemas([ + { + name: 'metrics', + group: 'metrics', + min: 1, + }, + { + name: 'buckets', + group: 'buckets', + }, + ]); + + test('should error if schema min agg requirement not met', () => { + const visState: VisualizationState = { + searchField: '', + activeVisualization: { + name: 'Test vis', + aggConfigParams: [], + }, + }; + + const { valid, errorMsg } = validateSchemaState(schemas, visState); + + expect(valid).toBe(false); + expect(errorMsg).toMatchInlineSnapshot( + `"The Test vis visualization needs at least 1 field(s) in the agg type \\"metrics\\""` + ); + }); + + test('should be valid if schema requirements are met', () => { + const visState: VisualizationState = { + searchField: '', + activeVisualization: { + name: 'Test vis', + aggConfigParams: [ + { + type: 'count', + schema: 'metrics', + }, + ], + }, + }; + + const { valid, errorMsg } = validateSchemaState(schemas, visState); + + expect(valid).toBe(true); + expect(errorMsg).not.toBeDefined(); + }); +}); diff --git a/src/plugins/vis_builder/public/application/utils/validate_schema_state.ts b/src/plugins/vis_builder/public/application/utils/validations/validate_schema_state.ts similarity index 65% rename from src/plugins/vis_builder/public/application/utils/validate_schema_state.ts rename to src/plugins/vis_builder/public/application/utils/validations/validate_schema_state.ts index 87dc19a3024..38139768a8f 100644 --- a/src/plugins/vis_builder/public/application/utils/validate_schema_state.ts +++ b/src/plugins/vis_builder/public/application/utils/validations/validate_schema_state.ts @@ -4,28 +4,24 @@ */ import { countBy } from 'lodash'; -import { Schemas } from '../../../../vis_default_editor/public'; -import { VisualizationState } from './state_management'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { VisualizationState } from '../state_management'; +import { ValidationResult } from './types'; /** * Validate if the visualization state fits the vis type schema criteria * @param schemas Visualization type config Schema objects * @param state visualization state - * @returns [Validity, 'Message'] + * @returns ValidationResult */ export const validateSchemaState = ( schemas: Schemas, state: VisualizationState -): [boolean, string?] => { +): ValidationResult => { const activeViz = state.activeVisualization; const vizName = activeViz?.name; const aggs = activeViz?.aggConfigParams; - // Check if any aggreagations exist - if (aggs?.length === 0) { - return [false]; - } - // Check if each schema's min agg requirement is met const aggSchemaCount = countBy(aggs, (agg) => agg.schema); const invalidsSchemas = schemas.all.filter((schema) => { @@ -36,11 +32,11 @@ export const validateSchemaState = ( }); if (invalidsSchemas.length > 0) { - return [ - false, - `The ${vizName} visualization needs at least ${invalidsSchemas[0].min} field(s) in the agg type "${invalidsSchemas[0].name}"`, - ]; + return { + valid: false, + errorMsg: `The ${vizName} visualization needs at least ${invalidsSchemas[0].min} field(s) in the agg type "${invalidsSchemas[0].name}"`, + }; } - return [true, '']; + return { valid: true }; }; diff --git a/src/plugins/vis_builder/public/application/utils/vis_builder_state_validation.test.ts b/src/plugins/vis_builder/public/application/utils/validations/vis_builder_state_validation.test.ts similarity index 83% rename from src/plugins/vis_builder/public/application/utils/vis_builder_state_validation.test.ts rename to src/plugins/vis_builder/public/application/utils/validations/vis_builder_state_validation.test.ts index c2d6d41a834..550e59c65f2 100644 --- a/src/plugins/vis_builder/public/application/utils/vis_builder_state_validation.test.ts +++ b/src/plugins/vis_builder/public/application/utils/validations/vis_builder_state_validation.test.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { RootState } from '../state_management'; import { validateVisBuilderState } from './vis_builder_state_validation'; describe('visBuilder state validation', () => { @@ -12,7 +13,8 @@ describe('visBuilder state validation', () => { legendPosition: '', type: 'metric', }; - const validVisualizationState = { + + const validVisualizationState: RootState['visualization'] = { activeVisualization: { name: 'metric', aggConfigParams: [], @@ -20,6 +22,7 @@ describe('visBuilder state validation', () => { indexPattern: '', searchField: '', }; + describe('correct return when validation suceeds', () => { test('with correct visBuilder state', () => { const validationResult = validateVisBuilderState({ @@ -27,9 +30,10 @@ describe('visBuilder state validation', () => { visualizationState: validVisualizationState, }); expect(validationResult.valid).toBeTruthy(); - expect(validationResult.errors).toBeNull(); + expect(validationResult.errorMsg).toBeUndefined(); }); }); + describe('correct return with errors when validation fails', () => { test('with non object type styleStyle', () => { const validationResult = validateVisBuilderState({ @@ -37,7 +41,7 @@ describe('visBuilder state validation', () => { visualizationState: validVisualizationState, }); expect(validationResult.valid).toBeFalsy(); - expect(validationResult.errors).toBeDefined(); + expect(validationResult.errorMsg).toBeDefined(); }); }); }); diff --git a/src/plugins/vis_builder/public/application/utils/validations/vis_builder_state_validation.ts b/src/plugins/vis_builder/public/application/utils/validations/vis_builder_state_validation.ts new file mode 100644 index 00000000000..e1d85f9ff06 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/validations/vis_builder_state_validation.ts @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Ajv from 'ajv'; +import visBuilderStateSchema from '../schema.json'; +import { ValidationResult } from './types'; + +const ajv = new Ajv(); +const validateState = ajv.compile(visBuilderStateSchema); + +export const validateVisBuilderState = (visBuilderState: any): ValidationResult => { + const isVisBuilderStateValid = validateState(visBuilderState); + const errorMsg = validateState.errors + ? validateState.errors[0].instancePath + ' ' + validateState.errors[0].message + : undefined; + + return { + valid: isVisBuilderStateValid, + errorMsg, + }; +}; diff --git a/src/plugins/vis_builder/public/application/utils/vis_builder_state_validation.ts b/src/plugins/vis_builder/public/application/utils/vis_builder_state_validation.ts deleted file mode 100644 index 9a601e82594..00000000000 --- a/src/plugins/vis_builder/public/application/utils/vis_builder_state_validation.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import Ajv from 'ajv'; -import visBuilderStateSchema from './schema.json'; - -const ajv = new Ajv(); -const validateState = ajv.compile(visBuilderStateSchema); - -export const validateVisBuilderState = (visBuilderState) => { - const isVisBuilderStateValid = validateState(visBuilderState); - - return { - valid: isVisBuilderStateValid, - errors: validateState.errors, - }; -}; diff --git a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx index a8c41df6cc4..6282845372a 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx @@ -27,7 +27,7 @@ import { TimefilterContract, TimeRange, } from '../../../data/public'; -import { validateSchemaState } from '../application/utils/validate_schema_state'; +import { validateSchemaState } from '../application/utils/validations/validate_schema_state'; import { getExpressionLoader, getTypeService } from '../plugin_services'; import { PersistedState } from '../../../visualizations/public'; import { RenderState, VisualizationState } from '../application/utils/state_management'; @@ -139,7 +139,7 @@ export class VisBuilderEmbeddable extends Embeddable<SavedObjectEmbeddableInput, } const { toExpression, ui } = visualizationType; const schemas = ui.containerConfig.data.schemas; - const [valid, errorMsg] = validateSchemaState(schemas, visualizationState); + const { valid, errorMsg } = validateSchemaState(schemas, visualizationState); if (!valid) { if (errorMsg) { diff --git a/src/plugins/vis_builder/public/plugin.test.ts b/src/plugins/vis_builder/public/plugin.test.ts index f5fac728420..35e17865649 100644 --- a/src/plugins/vis_builder/public/plugin.test.ts +++ b/src/plugins/vis_builder/public/plugin.test.ts @@ -28,6 +28,7 @@ describe('VisBuilderPlugin', () => { const setupDeps = { visualizations: visualizationsPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createSetupContract(), + data: dataPluginMock.createSetupContract(), }; const setup = plugin.setup(coreSetup, setupDeps); diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 8e90a04784c..3995c1246de 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -4,13 +4,17 @@ */ import { i18n } from '@osd/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { AppMountParameters, AppNavLinkStatus, + AppUpdater, CoreSetup, CoreStart, Plugin, PluginInitializerContext, + ScopedHistory, } from '../../../core/public'; import { VisBuilderPluginSetupDependencies, @@ -41,10 +45,17 @@ import { setUISettings, setTypeService, setReactExpressionRenderer, + setQueryService, } from './plugin_services'; import { createSavedVisBuilderLoader } from './saved_visualizations'; import { registerDefaultTypes } from './visualizations'; import { ConfigSchema } from '../config'; +import { + createOsdUrlStateStorage, + createOsdUrlTracker, + withNotifyOnErrors, +} from '../../opensearch_dashboards_utils/public'; +import { opensearchFilters } from '../../data/public'; export class VisBuilderPlugin implements @@ -55,13 +66,45 @@ export class VisBuilderPlugin VisBuilderPluginStartDependencies > { private typeService = new TypeService(); + private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({})); + private stopUrlTracking?: () => void; + private currentHistory?: ScopedHistory; constructor(public initializerContext: PluginInitializerContext<ConfigSchema>) {} public setup( core: CoreSetup<VisBuilderPluginStartDependencies, VisBuilderStart>, - { embeddable, visualizations }: VisBuilderPluginSetupDependencies + { embeddable, visualizations, data }: VisBuilderPluginSetupDependencies ) { + const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend(`/app/${PLUGIN_ID}`), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:${PLUGIN_ID}`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + + // Register Default Visualizations const typeService = this.typeService; registerDefaultTypes(typeService.setup()); @@ -70,43 +113,63 @@ export class VisBuilderPlugin id: PLUGIN_ID, title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, - async mount(params: AppMountParameters) { + defaultPath: '#/', + mount: async (params: AppMountParameters) => { // Load application bundle const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json const [coreStart, pluginsStart, selfStart] = await core.getStartServices(); - const { data, savedObjects, navigation, expressions } = pluginsStart; + const { savedObjects, navigation, expressions } = pluginsStart; + this.currentHistory = params.history; // make sure the index pattern list is up to date - data.indexPatterns.clearCache(); + pluginsStart.data.indexPatterns.clearCache(); // make sure a default index pattern exists // if not, the page will be redirected to management and visualize won't be rendered // TODO: Add the redirect await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); - // Register Default Visualizations + appMounted(); + + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = this.currentHistory.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); const services: VisBuilderServices = { ...coreStart, + appName: PLUGIN_ID, + scopedHistory: this.currentHistory, + history: this.currentHistory, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: this.currentHistory, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), toastNotifications: coreStart.notifications.toasts, - data, + data: pluginsStart.data, savedObjectsPublic: savedObjects, navigation, expressions, - history: params.history, setHeaderActionMenu: params.setHeaderActionMenu, types: typeService.start(), savedVisBuilderLoader: selfStart.savedVisBuilderLoader, embeddable: pluginsStart.embeddable, - scopedHistory: params.history, + dashboard: pluginsStart.dashboard, }; // Instantiate the store const store = await getPreloadedStore(services); + const unmount = renderApp(params, services, store); // Render the application - return renderApp(params, services, store); + return () => { + unlistenParentHistory(); + unmount(); + appUnMounted(); + }; }, }); @@ -154,7 +217,7 @@ export class VisBuilderPlugin public start( core: CoreStart, - { data, expressions }: VisBuilderPluginStartDependencies + { expressions, data }: VisBuilderPluginStartDependencies ): VisBuilderStart { const typeService = this.typeService.start(); @@ -176,6 +239,7 @@ export class VisBuilderPlugin setTimeFilter(data.query.timefilter.timefilter); setTypeService(typeService); setUISettings(core.uiSettings); + setQueryService(data.query); return { ...typeService, @@ -183,5 +247,9 @@ export class VisBuilderPlugin }; } - public stop() {} + public stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } } diff --git a/src/plugins/vis_builder/public/plugin_services.ts b/src/plugins/vis_builder/public/plugin_services.ts index f979f3a22b1..c5583e3c5e4 100644 --- a/src/plugins/vis_builder/public/plugin_services.ts +++ b/src/plugins/vis_builder/public/plugin_services.ts @@ -37,3 +37,7 @@ export const [getTimeFilter, setTimeFilter] = createGetterSetter<TimefilterContr export const [getTypeService, setTypeService] = createGetterSetter<TypeServiceStart>('TypeService'); export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings'); + +export const [getQueryService, setQueryService] = createGetterSetter< + DataPublicPluginStart['query'] +>('Query'); diff --git a/src/plugins/vis_builder/public/types.ts b/src/plugins/vis_builder/public/types.ts index 131c9cc1f6b..e79762bedc1 100644 --- a/src/plugins/vis_builder/public/types.ts +++ b/src/plugins/vis_builder/public/types.ts @@ -14,6 +14,8 @@ import { DataPublicPluginStart } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; import { SavedObjectLoader } from '../../saved_objects/public'; import { AppMountParameters, CoreStart, ToastsStart, ScopedHistory } from '../../../core/public'; +import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; +import { DataPublicPluginSetup } from '../../data/public'; export type VisBuilderSetup = TypeServiceSetup; export interface VisBuilderStart extends TypeServiceStart { @@ -23,6 +25,7 @@ export interface VisBuilderStart extends TypeServiceStart { export interface VisBuilderPluginSetupDependencies { embeddable: EmbeddableSetup; visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; } export interface VisBuilderPluginStartDependencies { embeddable: EmbeddableStart; @@ -34,6 +37,7 @@ export interface VisBuilderPluginStartDependencies { } export interface VisBuilderServices extends CoreStart { + appName: string; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedVisBuilderLoader: VisBuilderStart['savedVisBuilderLoader']; toastNotifications: ToastsStart; @@ -45,6 +49,8 @@ export interface VisBuilderServices extends CoreStart { history: History; embeddable: EmbeddableStart; scopedHistory: ScopedHistory; + osdUrlStateStorage: IOsdUrlStateStorage; + dashboard: DashboardStart; } export interface ISavedVis { diff --git a/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts b/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts index 069666677d6..f50ab9172cd 100644 --- a/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts +++ b/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts @@ -9,8 +9,12 @@ import { ExpressionFunctionOpenSearchDashboards } from '../../../../expressions' import { buildExpressionFunction } from '../../../../expressions/public'; import { VisualizationState } from '../../application/utils/state_management'; import { getSearchService, getIndexPatterns } from '../../plugin_services'; +import { StyleState } from '../../application/utils/state_management'; -export const getAggExpressionFunctions = async (visualization: VisualizationState) => { +export const getAggExpressionFunctions = async ( + visualization: VisualizationState, + style?: StyleState +) => { const { activeVisualization, indexPattern: indexId = '' } = visualization; const { aggConfigParams } = activeVisualization || {}; @@ -32,8 +36,8 @@ export const getAggExpressionFunctions = async (visualization: VisualizationStat 'opensearchaggs', { index: indexId, - metricsAtAllLevels: false, - partialRows: false, + metricsAtAllLevels: style?.showMetricsAtAllLevels || false, + partialRows: style?.showPartialRows || false, aggConfigs: JSON.stringify(aggConfigs.aggs), includeFormatHints: false, } diff --git a/src/plugins/vis_builder/public/visualizations/index.ts b/src/plugins/vis_builder/public/visualizations/index.ts index 6787c28a6ff..c867e570143 100644 --- a/src/plugins/vis_builder/public/visualizations/index.ts +++ b/src/plugins/vis_builder/public/visualizations/index.ts @@ -5,6 +5,7 @@ import type { TypeServiceSetup } from '../services/type_service'; import { createMetricConfig } from './metric'; +import { createTableConfig } from './table'; import { createHistogramConfig, createLineConfig, createAreaConfig } from './vislib'; export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { @@ -13,6 +14,7 @@ export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { createLineConfig, createAreaConfig, createMetricConfig, + createTableConfig, ]; visualizationTypes.forEach((createTypeConfig) => { diff --git a/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx new file mode 100644 index 00000000000..a77a0811e60 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import produce from 'immer'; +import { Draft } from 'immer'; +import { EuiIconTip } from '@elastic/eui'; +import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; +import { + useTypedDispatch, + useTypedSelector, + setStyleState, +} from '../../../application/utils/state_management'; +import { TableOptionsDefaults } from '../table_viz_type'; +import { Option } from '../../../application/app'; + +function TableVizOptions() { + const styleState = useTypedSelector((state) => state.style) as TableOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft<typeof styleState>) => void) => { + const newState = produce(styleState, callback); + dispatch(setStyleState<TableOptionsDefaults>(newState)); + }, + [dispatch, styleState] + ); + + const isPerPageValid = styleState.perPage === '' || styleState.perPage > 0; + + return ( + <> + <Option + title={i18n.translate('visTypeTableNewNew.params.settingsTitle', { + defaultMessage: 'Settings', + })} + initialIsOpen + > + <NumberInputOption + label={ + <> + {i18n.translate('visTypeTableNewNew.params.perPageLabel', { + defaultMessage: 'Max rows per page', + })} + <EuiIconTip + content={ + <FormattedMessage + id="visTypeTableNewNews.field.emptyTooltip" + defaultMessage="Leaving this field empty means it will use number of buckets from the response." + /> + } + position="right" + /> + </> + } + isInvalid={!isPerPageValid} + min={1} + paramName="perPage" + value={styleState.perPage} + setValue={(_, value) => + setOption((draft) => { + draft.perPage = value; + }) + } + /> + + <SwitchOption + label={i18n.translate('visTypeTableNewNew.params.showMetricsLabel', { + defaultMessage: 'Show metrics for every bucket/level', + })} + paramName="showMetricsAtAllLevels" + value={styleState.showMetricsAtAllLevels} + setValue={(_, value) => + setOption((draft) => { + draft.showMetricsAtAllLevels = value; + }) + } + data-test-subj="showMetricsAtAllLevels" + /> + + <SwitchOption + label={i18n.translate('visTypeTableNewNew.params.showPartialRowsLabel', { + defaultMessage: 'Show partial rows', + })} + tooltip={i18n.translate('visTypeTableNewNew.params.showPartialRowsTip', { + defaultMessage: + 'Show rows that have partial data. This will still calculate metrics for every bucket/level, even if they are not displayed.', + })} + paramName="showPartialRows" + value={styleState.showPartialRows} + setValue={(_, value) => + setOption((draft) => { + draft.showPartialRows = value; + }) + } + data-test-subj="showPartialRows" + /> + </Option> + </> + ); +} + +export { TableVizOptions }; diff --git a/src/plugins/vis_builder/public/visualizations/table/index.ts b/src/plugins/vis_builder/public/visualizations/table/index.ts new file mode 100644 index 00000000000..51fd19d291e --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createTableConfig } from './table_viz_type'; diff --git a/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts new file mode 100644 index 00000000000..733ad986f28 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/table_viz_type.ts @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../vis_default_editor/public'; +import { AggGroupNames } from '../../../../data/public'; +import { TableVizOptions } from './components/table_viz_options'; +import { VisualizationTypeOptions } from '../../services/type_service'; +import { toExpression } from './to_expression'; + +export interface TableOptionsDefaults { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; +} + +export const createTableConfig = (): VisualizationTypeOptions<TableOptionsDefaults> => ({ + name: 'table', + title: 'Table', + icon: 'visTable', + description: 'Display table visualizations', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + min: 1, + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + defaults: { + aggTypes: ['avg', 'cardinality'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'split_row', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table in rows', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'split_column', + title: i18n.translate('visTypeTableNewNew.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table in columns', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + ]), + }, + style: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + }, + render: TableVizOptions, + }, + }, + }, +}); diff --git a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts new file mode 100644 index 00000000000..bbec4c1cc7e --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts @@ -0,0 +1,130 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SchemaConfig } from '../../../../visualizations/public'; +import { TableVisExpressionFunctionDefinition } from '../../../../vis_type_table/public'; +import { AggConfigs, IAggConfig } from '../../../../data/common'; +import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; +import { RenderState } from '../../application/utils/state_management'; +import { TableOptionsDefaults } from './table_viz_type'; +import { getAggExpressionFunctions } from '../common/expression_helpers'; + +// TODO: Update to the common getShemas from src/plugins/visualizations/public/legacy/build_pipeline.ts +// And move to a common location accessible by all the visualizations +const getVisSchemas = (aggConfigs: AggConfigs, showMetricsAtAllLevels: boolean): any => { + const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { + const hasSubAgg = [ + 'derivative', + 'moving_avg', + 'serial_diff', + 'cumulative_sum', + 'sum_bucket', + 'avg_bucket', + 'min_bucket', + 'max_bucket', + ].includes(agg.type.name); + + const formatAgg = hasSubAgg + ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) + : agg; + + const params = {}; + + const label = agg.makeLabel && agg.makeLabel(); + + return { + accessor, + format: formatAgg.toSerializedFieldFormat(), + params, + label, + aggType: agg.type.name, + }; + }; + + let cnt = 0; + const schemas: any = { + metric: [], + }; + + if (!aggConfigs) { + return schemas; + } + + const responseAggs = aggConfigs.getResponseAggs().filter((agg: IAggConfig) => agg.enabled); + const metrics = responseAggs.filter((agg: IAggConfig) => agg.type.type === 'metrics'); + + responseAggs.forEach((agg) => { + let skipMetrics = false; + const schemaName = agg.schema; + + if (!schemaName) { + cnt++; + return; + } + + if (schemaName === 'split_row' || schemaName === 'split_column') { + skipMetrics = responseAggs.length - metrics.length > 1; + } + + if (!schemas[schemaName]) { + schemas[schemaName] = []; + } + + if (!showMetricsAtAllLevels || agg.type.type !== 'metrics') { + schemas[schemaName]!.push(createSchemaConfig(cnt++, agg)); + } + + if ( + showMetricsAtAllLevels && + (agg.type.type !== 'metrics' || metrics.length === responseAggs.length) + ) { + metrics.forEach((metric: any) => { + const schemaConfig = createSchemaConfig(cnt++, metric); + if (!skipMetrics) { + schemas.metric.push(schemaConfig); + } + }); + } + }); + + return schemas; +}; + +export interface TableRootState extends RenderState { + style: TableOptionsDefaults; +} + +export const toExpression = async ({ style: styleState, visualization }: TableRootState) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, styleState); + const { showPartialRows, showMetricsAtAllLevels } = styleState; + + const schemas = getVisSchemas(aggConfigs, showMetricsAtAllLevels); + + const metrics = + schemas.bucket && showPartialRows && !showMetricsAtAllLevels + ? schemas.metric.slice(-1 * (schemas.metric.length / schemas.bucket.length)) + : schemas.metric; + + const tableData = { + metrics, + buckets: schemas.bucket || [], + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const visConfig = { + ...styleState, + ...tableData, + }; + + const tableVis = buildExpressionFunction<TableVisExpressionFunctionDefinition>( + 'opensearch_dashboards_table', + { + visConfig: JSON.stringify(visConfig), + } + ); + + return buildExpression([...expressionFns, tableVis]).toString(); +}; diff --git a/src/plugins/vis_builder/server/index.ts b/src/plugins/vis_builder/server/index.ts index 417e6d2e317..f04ba546623 100644 --- a/src/plugins/vis_builder/server/index.ts +++ b/src/plugins/vis_builder/server/index.ts @@ -17,8 +17,5 @@ export function plugin(initializerContext: PluginInitializerContext) { export { VisBuilderPluginSetup, VisBuilderPluginStart } from './types'; export const config: PluginConfigDescriptor<ConfigSchema> = { - exposeToBrowser: { - enabled: true, - }, schema: configSchema, }; diff --git a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx index c3b244de14f..1b4508549a7 100644 --- a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -139,7 +139,7 @@ function DateRangesParamEditor({ <EuiFormRow display={'rowCompressed'} fullWidth> <> <EuiText size="xs"> - <EuiLink href={services.docLinks.links.noDocumentation.dateMath} target="_blank"> + <EuiLink href={services.docLinks.links.opensearch.dateMath} target="_blank"> <FormattedMessage id="visDefaultEditor.controls.dateRanges.acceptedDateFormatsLinkText" defaultMessage="Acceptable date formats" diff --git a/src/plugins/vis_type_table/README.md b/src/plugins/vis_type_table/README.md index cf37e133ed1..42304c39e93 100644 --- a/src/plugins/vis_type_table/README.md +++ b/src/plugins/vis_type_table/README.md @@ -1 +1,35 @@ -Contains the data table visualization, that allows presenting data in a simple table format. \ No newline at end of file +# Data Table + +This is an OpenSearch Dashboards plugin that is used to visualize data and aggregations in tabular format. + +## Create Data Table +To create a data table in OpenSearch Dashboards, first select `Visualize` from the navigation menu. Then click `Create Visualization` and choose `Data Table` as the visualization type. + +## Select Metrics + +### Metrics Aggregation +At the `Metrics`, select the metric aggregation type from the menu and configure it accordingly. You could also add multiple metrics and each metrics is a separate column in table visualization. + +### Buckets Aggregation +At the `Buckets`, configure the columns to be displayed in the table visualization. +- `Split Rows` is used when you want to divide one row into more based on some category. It splits one row into multiple and add columns based on the categories you choose to split. For example, if you split the data based on gender then you want to know more on each gender's clothing preference. You could click `Split Rows` and input `clothing.category` in terms. Each gender's data is now split across to multiple rows based on a new added column `clothing.category`. +- `Split Table` splits the table into separate tables for the aggregation you choose. It is similar to `Split Rows`, but this time each row becomes a single table with aggregation columns arranged horizontally or vertically. + +## Select Options +In the `Options` tab, you can configure more options. +- `Max rows per page` is the maximum number of rows displayed per page. +- `Show metrics for every bucket/level` adds metrics aggregation to every column. +- `Show partial rows` will include data with missing columns. +- `Show total` calculates the selected metrics per column and displays the result at the bottom. Warning - depending on your columns and the total aggregation function selected, this may generate statistically invalid results. For example, avoid summing or averaging averages. +- `Percentage column` adds one percentage column based on the chosen metrics aggregation. + +## Example + +Below is an example of creating a table visualization using sample ecommerce data. + +- Create a new data table visualization and set a relative time 15 weeks ago. +- Compute the count of ecommerce: Choose `Count` in Metrics Aggregation. +- Split the rows on the top 5 of `manufacturer.keyword` ordered by `Metric:Count` in descending and add a label "manufacturer". +- Split the table in rows on the top 5 of `geoip.city_name` ordered by `Metric:Count` in ascending order. +- Click the `Save` button on the top left and save the visualization as "Top manufacturers by count per city". +- Choose a table and click the download icon to download the table. diff --git a/src/plugins/vis_type_table/opensearch_dashboards.json b/src/plugins/vis_type_table/opensearch_dashboards.json index e2f050534c1..ba0ebb1bc4c 100644 --- a/src/plugins/vis_type_table/opensearch_dashboards.json +++ b/src/plugins/vis_type_table/opensearch_dashboards.json @@ -6,11 +6,11 @@ "requiredPlugins": [ "expressions", "visualizations", - "data", - "opensearchDashboardsLegacy" + "data" ], "requiredBundles": [ "opensearchDashboardsUtils", + "opensearchDashboardsReact", "share", "charts", "visDefaultEditor" diff --git a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap index dc6571de969..a32609c2e3d 100644 --- a/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap +++ b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#table returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "table_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "dimensions": Object { "buckets": Array [], diff --git a/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 00000000000..296f33f90c7 --- /dev/null +++ b/src/plugins/vis_type_table/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis toExpressionAst function with customized params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "opensearchaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"perPage\\":5,\\"percentageCol\\":\\"Count\\",\\"showPartialRows\\":false,\\"showMetricsAtAllLevels\\":false,\\"showTotal\\":true,\\"totalFunc\\":\\"min\\",\\"metrics\\":[],\\"buckets\\":[]}", + ], + }, + "function": "opensearch_dashboards_table", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`table vis toExpressionAst function with default params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "opensearchaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"perPage\\":10,\\"percentageCol\\":\\"\\",\\"showPartialRows\\":false,\\"showMetricsAtAllLevels\\":false,\\"showTotal\\":false,\\"totalFunc\\":\\"sum\\",\\"metrics\\":[],\\"buckets\\":[]}", + ], + }, + "function": "opensearch_dashboards_table", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`table vis toExpressionAst function without params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[]", + ], + "includeFormatHints": Array [ + false, + ], + "index": Array [ + "123", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "opensearchaggs", + "type": "function", + }, + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"metrics\\":[],\\"buckets\\":[]}", + ], + }, + "function": "opensearch_dashboards_table", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/_table_vis.scss deleted file mode 100644 index ea4b4d0d1c9..00000000000 --- a/src/plugins/vis_type_table/public/_table_vis.scss +++ /dev/null @@ -1,23 +0,0 @@ -// SASSTODO: Update naming to BEM -// This chart is actively being re-written to React and EUI -// Putting off renaming to avoid conflicts -.table-vis { - display: flex; - flex-direction: column; - flex: 1 0 100%; - overflow: auto; -} - -.table-vis-container { - osd-agg-table-group > .table > tbody > tr > td { - border-top: 0; - } - - .pagination-other-pages { - justify-content: flex-end; - } - - .pagination-size { - display: none; - } -} diff --git a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss deleted file mode 100644 index 156db063c8d..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/_agg_table.scss +++ /dev/null @@ -1,42 +0,0 @@ -osd-agg-table, -osd-agg-table-group { - display: block; -} - -.osdAggTable { - display: flex; - flex: 1 1 auto; - flex-direction: column; -} - -.osdAggTable__paginated { - flex: 1 1 auto; - overflow: auto; - - th { - text-align: left; - font-weight: $euiFontWeightBold; - } - - tr:hover td, - .osdTableCellFilter { - background-color: $euiColorLightestShade; - } -} - -.osdAggTable__controls { - flex: 0 0 auto; - display: flex; - align-items: center; - margin: $euiSizeS $euiSizeXS; - - > paginate-controls { - flex: 1 0 auto; - margin: 0; - padding: 0; - } -} - -.small { - font-size: 0.9em !important; -} diff --git a/src/plugins/vis_type_table/public/agg_table/_index.scss b/src/plugins/vis_type_table/public/agg_table/_index.scss deleted file mode 100644 index ed94e844912..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "./agg_table"; diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.html b/src/plugins/vis_type_table/public/agg_table/agg_table.html deleted file mode 100644 index 8e8aafa83fd..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.html +++ /dev/null @@ -1,34 +0,0 @@ -<paginated-table - ng-if="rows.length" - table="table" - rows="rows" - columns="formattedColumns" - per-page="perPage" - sort="sort" - show-total="showTotal" - percentage-col="percentageCol" - filter="filter" - totalFunc="totalFunc"> - - <div class="osdAggTable__controls"> - <small - i18n-id="visTypeTable.aggTable.exportLabel" - i18n-default-message="Export:" - ></small>   - <a class="small" ng-click="aggTable.exportAsCsv(false)"> - <span - i18n-id="visTypeTable.aggTable.rawLabel" - i18n-default-message="Raw" - ></span> - <i aria-hidden="true" class="fa fa-download"></i> - </a>    - <a class="small" ng-click="aggTable.exportAsCsv(true)"> - <span - i18n-id="visTypeTable.aggTable.formattedLabel" - i18n-default-message="Formatted" - ></span> - <i aria-hidden="true" class="fa fa-download"></i> - </a> - <paginate-controls></paginate-controls> - </div> -</paginated-table> diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js deleted file mode 100644 index a00aea27869..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.js +++ /dev/null @@ -1,295 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; -import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; -import { i18n } from '@osd/i18n'; - -export function OsdAggTable(config, RecursionHelper) { - return { - restrict: 'E', - template: aggTableTemplate, - scope: { - table: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - controllerAs: 'aggTable', - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el); - }, - controller: function ($scope) { - const self = this; - - self._saveAs = require('@elastic/filesaver').saveAs; - self.csv = { - separator: config.get(CSV_SEPARATOR_SETTING), - quoteValues: config.get(CSV_QUOTE_VALUES_SETTING), - }; - - self.exportAsCsv = function (formatted) { - const csv = new Blob([self.toCsv(formatted)], { type: 'text/csv;charset=utf-8' }); - self._saveAs(csv, self.csv.filename); - }; - - self.toCsv = function (formatted) { - const rows = formatted ? $scope.rows : $scope.table.rows; - const columns = formatted ? [...$scope.formattedColumns] : [...$scope.table.columns]; - - if ($scope.splitRow && formatted) { - columns.unshift($scope.splitRow); - } - - const nonAlphaNumRE = /[^a-zA-Z0-9]/; - const allDoubleQuoteRE = /"/g; - - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; - } - - let csvRows = []; - for (const row of rows) { - const rowArray = []; - for (const col of columns) { - const value = row[col.id]; - const formattedValue = - formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value); - rowArray.push(formattedValue); - } - csvRows = [...csvRows, rowArray]; - } - - // add the columns to the rows - csvRows.unshift( - columns.map(function (col) { - return escape(formatted ? col.title : col.name); - }) - ); - - return csvRows - .map(function (row) { - return row.join(self.csv.separator) + '\r\n'; - }) - .join(''); - }; - - $scope.$watchMulti( - ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], - function () { - const { table, exportTitle, percentageCol } = $scope; - const showPercentage = percentageCol !== ''; - - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - $scope.splitRow = null; - return; - } - - self.csv.filename = (exportTitle || table.title || 'unsaved') + '.csv'; - $scope.rows = table.rows; - $scope.formattedColumns = []; - - if (typeof $scope.dimensions === 'undefined') return; - - const { buckets, metrics, splitColumn, splitRow } = $scope.dimensions; - - $scope.formattedColumns = table.columns - .map(function (col, i) { - const isBucket = buckets.find((bucket) => bucket.accessor === i); - const isSplitColumn = splitColumn - ? splitColumn.find((splitColumn) => splitColumn.accessor === i) - : undefined; - const isSplitRow = splitRow - ? splitRow.find((splitRow) => splitRow.accessor === i) - : undefined; - const dimension = - isBucket || isSplitColumn || metrics.find((metric) => metric.accessor === i); - - const formatter = dimension - ? getFormatService().deserialize(dimension.format) - : undefined; - - const formattedColumn = { - id: col.id, - title: col.name, - formatter: formatter, - filterable: !!isBucket, - }; - - if (isSplitRow) { - $scope.splitRow = formattedColumn; - } - - if (!dimension) return; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = - dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = (tableRows) => { - return _.reduce( - tableRows, - function (prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows).map(col.id).min().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows).map(col.id).max().value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter((column) => column); - - if (showPercentage) { - const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); - - // column to show percentage for was removed - if (insertAtIndex < 0) return; - - const { cols, rows } = addPercentageCol( - $scope.formattedColumns, - percentageCol, - table.rows, - insertAtIndex - ); - $scope.rows = rows; - $scope.formattedColumns = cols; - } - } - ); - }, - }; -} - -/** - * @param {Object[]} columns - the formatted columns that will be displayed - * @param {String} title - the title of the column to add to - * @param {Object[]} rows - the row data for the columns - * @param {Number} insertAtIndex - the index to insert the percentage column at - * @returns {Object} - cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol(columns, title, rows, insertAtIndex) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - }); - const newRows = rows.map((row) => ({ - [newId]: row[id] / sumTotal, - ...row, - })); - - return { cols: newCols, rows: newRows }; -} - -function insert(arr, index, ...items) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, ...items); - return newArray; -} diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/agg_table/agg_table.test.js deleted file mode 100644 index 14d0c7fe795..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table.test.js +++ /dev/null @@ -1,512 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import moment from 'moment'; -import angular from 'angular'; -import 'angular-mocks'; -import sinon from 'sinon'; -import { round } from 'lodash'; - -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../opensearch_dashboards_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { UI_SETTINGS } from '../../../data/public/'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; - -import { setFormatService } from '../services'; -import { getInnerAngular } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -const uiSettings = new Map(); - -describe('Table Vis - AggTable Directive', function () { - const core = coreMock.createStart(); - - core.uiSettings.set = jest.fn((key, value) => { - uiSettings.set(key, value); - }); - - core.uiSettings.get = jest.fn((key) => { - const defaultValues = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - 'dateFormat:tz': 'UTC', - [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, - [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', - [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', - [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, - [CSV_SEPARATOR_SETTING]: ',', - [CSV_QUOTE_VALUES_SETTING]: true, - }; - - return defaultValues[key] || uiSettings.get(key); - }); - - let $rootScope; - let $compile; - let settings; - - const initLocalAngular = () => { - const tableVisModule = getInnerAngular('opensearch-dashboards/table_vis', core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(() => { - setUiSettings(core.uiSettings); - setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); - initLocalAngular(); - angular.mock.module('opensearch-dashboards/table_vis'); - angular.mock.inject(($injector, config) => { - settings = config; - - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }); - }); - - let $scope; - beforeEach(function () { - $scope = $rootScope.$new(); - }); - afterEach(function () { - $scope.$destroy(); - }); - - test('renders a simple response properly', function () { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.table = tabifiedData.metricOnly.tables[0]; - - const $el = $compile('<osd-agg-table table="table" dimensions="dimensions"></osd-agg-table>')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(1); - expect($el.find('td').length).toBe(1); - expect($el.find('td').text()).toEqual('1,000'); - }); - - test('renders nothing if the table is empty', function () { - $scope.dimensions = {}; - $scope.table = null; - const $el = $compile('<osd-agg-table table="table" dimensions="dimensions"></osd-agg-table>')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(0); - }); - - test('renders a complex response properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - const $el = $('<osd-agg-table table="table" dimensions="dimensions"></osd-agg-table>'); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tbody').length).toBe(1); - - const $rows = $el.find('tbody tr'); - expect($rows.length).toBeGreaterThan(0); - - function validBytes(str) { - const num = str.replace(/,/g, ''); - if (num !== '-') { - expect(num).toMatch(/^\d+$/); - } - } - - $rows.each(function () { - // 6 cells in every row - const $cells = $(this).find('td'); - expect($cells.length).toBe(6); - - const txts = $cells.map(function () { - return $(this).text().trim(); - }); - - // two character country code - expect(txts[0]).toMatch(/^(png|jpg|gif|html|css)$/); - validBytes(txts[1]); - - // country - expect(txts[2]).toMatch(/^\w\w$/); - validBytes(txts[3]); - - // os - expect(txts[4]).toMatch(/^(win|mac|linux)$/); - validBytes(txts[5]); - }); - }); - - describe('renders totals row', function () { - async function totalsRowTest(totalFunc, expected) { - function setDefaultTimezone() { - moment.tz.setDefault(settings.get('dateFormat:tz')); - } - - const oldTimezoneSetting = settings.get('dateFormat:tz'); - settings.set('dateFormat:tz', 'UTC'); - setDefaultTimezone(); - - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.showTotal = true; - $scope.totalFunc = totalFunc; - const $el = $(`<osd-agg-table - table="table" - show-total="showTotal" - total-func="totalFunc" - dimensions="dimensions"></osd-agg-table>`); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tfoot').length).toBe(1); - - const $rows = $el.find('tfoot tr'); - expect($rows.length).toBe(1); - - const $cells = $($rows[0]).find('th'); - expect($cells.length).toBe(6); - - for (let i = 0; i < 6; i++) { - expect($($cells[i]).text().trim()).toBe(expected[i]); - } - settings.set('dateFormat:tz', oldTimezoneSetting); - setDefaultTimezone(); - } - test('as count', async function () { - await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); - }); - test('as min', async function () { - await totalsRowTest('min', [ - '', - '2014-09-28', - '9,283', - 'Sep 28, 2014 @ 00:00:00.000', - '1', - '11', - ]); - }); - test('as max', async function () { - await totalsRowTest('max', [ - '', - '2014-10-03', - '220,943', - 'Oct 3, 2014 @ 00:00:00.000', - '239', - '837', - ]); - }); - test('as avg', async function () { - await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); - }); - test('as sum', async function () { - await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); - }); - }); - - describe('aggTable.toCsv()', function () { - test('escapes rows and columns properly', function () { - const $el = $compile('<osd-agg-table table="table" dimensions="dimensions"></osd-agg-table>')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - expect(aggTable.toCsv()).toBe( - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' - ); - }); - - test('exports rows and columns properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('<osd-agg-table table="table" dimensions="dimensions"></osd-agg-table>')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - const raw = aggTable.toCsv(false); - expect(raw).toBe( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - 'png,412032,IT,9299,win,0' + - '\r\n' + - 'png,412032,IT,9299,mac,9299' + - '\r\n' + - 'png,412032,US,8293,linux,3992' + - '\r\n' + - 'png,412032,US,8293,mac,3029' + - '\r\n' + - 'css,412032,MX,9299,win,4992' + - '\r\n' + - 'css,412032,MX,9299,mac,5892' + - '\r\n' + - 'css,412032,US,8293,linux,3992' + - '\r\n' + - 'css,412032,US,8293,mac,3029' + - '\r\n' + - 'html,412032,CN,9299,win,4992' + - '\r\n' + - 'html,412032,CN,9299,mac,5892' + - '\r\n' + - 'html,412032,FR,8293,win,3992' + - '\r\n' + - 'html,412032,FR,8293,mac,3029' + - '\r\n' - ); - }); - - test('exports formatted rows and columns properly', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('<osd-agg-table table="table" dimensions="dimensions"></osd-agg-table>')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - // Create our own converter since the ones we use for tests don't actually transform the provided value - $tableScope.formattedColumns[0].formatter.convert = (v) => `${v}_formatted`; - - const formatted = aggTable.toCsv(true); - expect(formatted).toBe( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - '"png_formatted",412032,IT,9299,win,0' + - '\r\n' + - '"png_formatted",412032,IT,9299,mac,9299' + - '\r\n' + - '"png_formatted",412032,US,8293,linux,3992' + - '\r\n' + - '"png_formatted",412032,US,8293,mac,3029' + - '\r\n' + - '"css_formatted",412032,MX,9299,win,4992' + - '\r\n' + - '"css_formatted",412032,MX,9299,mac,5892' + - '\r\n' + - '"css_formatted",412032,US,8293,linux,3992' + - '\r\n' + - '"css_formatted",412032,US,8293,mac,3029' + - '\r\n' + - '"html_formatted",412032,CN,9299,win,4992' + - '\r\n' + - '"html_formatted",412032,CN,9299,mac,5892' + - '\r\n' + - '"html_formatted",412032,FR,8293,win,3992' + - '\r\n' + - '"html_formatted",412032,FR,8293,mac,3029' + - '\r\n' - ); - }); - }); - - test('renders percentage columns', async function () { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.percentageCol = 'Average bytes'; - - const $el = $(`<osd-agg-table - table="table" - dimensions="dimensions" - percentage-col="percentageCol" - ></osd-agg-table>`); - - $compile($el)($scope); - $scope.$digest(); - - const $headings = $el.find('th'); - expect($headings.length).toBe(7); - expect($headings.eq(3).text().trim()).toBe('Average bytes percentages'); - - const countColId = $scope.table.columns.find((col) => col.name === $scope.percentageCol).id; - const counts = $scope.table.rows.map((row) => row[countColId]); - const total = counts.reduce((sum, curr) => sum + curr, 0); - const $percentageColValues = $el.find('tbody tr').map((i, el) => $(el).find('td').eq(3).text()); - - $percentageColValues.each((i, value) => { - const percentage = `${round((counts[i] / total) * 100, 3)}%`; - expect(value).toBe(percentage); - }); - }); - - describe('aggTable.exportAsCsv()', function () { - let origBlob; - function FakeBlob(slices, opts) { - this.slices = slices; - this.opts = opts; - } - - beforeEach(function () { - origBlob = window.Blob; - window.Blob = FakeBlob; - }); - - afterEach(function () { - window.Blob = origBlob; - }); - - test('calls _saveAs properly', function () { - const $el = $compile('<osd-agg-table table="table" dimensions="dimensions">')($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - - const saveAs = sinon.stub(aggTable, '_saveAs'); - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - aggTable.csv.filename = 'somefilename.csv'; - aggTable.exportAsCsv(); - - expect(saveAs.callCount).toBe(1); - const call = saveAs.getCall(0); - expect(call.args[0]).toBeInstanceOf(FakeBlob); - expect(call.args[0].slices).toEqual([ - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', - ]); - expect(call.args[0].opts).toEqual({ - type: 'text/csv;charset=utf-8', - }); - expect(call.args[1]).toBe('somefilename.csv'); - }); - - test('should use the export-title attribute', function () { - const expected = 'export file name'; - const $el = $compile( - `<osd-agg-table table="table" dimensions="dimensions" export-title="exportTitle">` - )($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [], - rows: [], - }; - $tableScope.exportTitle = expected; - $scope.$digest(); - - expect(aggTable.csv.filename).toEqual(`${expected}.csv`); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/agg_table/agg_table_group.html deleted file mode 100644 index 2dcf7f125f6..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.html +++ /dev/null @@ -1,77 +0,0 @@ -<table ng-if="rows" class="table osdAggTable__group" ng-repeat="table in rows"> - <thead> - <tr> - <th ng-if="table.tables" scope="col"> - <span class="osdAggTable__groupHeader">{{ table.title }}</span> - </th> - </tr> - </thead> - <tbody> - <tr> - <td> - <osd-agg-table-group - ng-if="table.tables" - group="table" - filter="filter" - dimensions="dimensions" - per-page="perPage" - sort="sort" - export-title="exportTitle" - percentage-col="percentageCol" - show-total="showTotal" - total-func="totalFunc"></osd-agg-table-group> - <osd-agg-table - ng-if="table.rows" - filter="filter" - table="table" - dimensions="dimensions" - export-title="exportTitle" - per-page="perPage" - sort="sort" - percentage-col="percentageCol" - show-total="showTotal" - total-func="totalFunc"> - </osd-agg-table> - </td> - </tr> - </tbody> -</table> - -<table ng-if="columns" class="table osdAggTable__group"> - <thead> - <tr> - <th ng-repeat="table in columns" ng-if="table.tables" scope="col"> - <span class="osdAggTable__groupHeader">{{ table.title }}</span> - </th> - </tr> - </thead> - <tbody> - <tr> - <td ng-repeat="table in columns"> - <osd-agg-table-group - ng-if="table.tables" - filter="filter" - group="table" - dimensions="dimensions" - per-page="perPage" - sort="sort" - export-title="exportTitle" - show-total="showTotal" - percentage-col="percentageCol" - total-func="totalFunc"></osd-agg-table-group> - <osd-agg-table - ng-if="table.rows" - filter="filter" - table="table" - dimensions="dimensions" - export-title="exportTitle" - per-page="perPage" - sort="sort" - show-total="showTotal" - percentage-col="percentageCol" - total-func="totalFunc"> - </osd-agg-table> - </td> - </tr> - </tbody> -</table> diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.js deleted file mode 100644 index 133b20800a1..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import aggTableGroupTemplate from './agg_table_group.html'; - -export function OsdAggTableGroup(RecursionHelper) { - return { - restrict: 'E', - template: aggTableGroupTemplate, - scope: { - group: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el, { - post: function ($scope) { - $scope.$watch('group', function (group) { - // clear the previous "state" - $scope.rows = $scope.columns = false; - - if (!group || !group.tables.length) return; - - const childLayout = group.direction === 'row' ? 'rows' : 'columns'; - - $scope[childLayout] = group.tables; - }); - }, - }); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js deleted file mode 100644 index 18a48e92211..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/agg_table_group.test.js +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import angular from 'angular'; -import 'angular-mocks'; -import expect from '@osd/expect'; - -import { getFieldFormatsRegistry } from '../../../data/public/test_utils'; -import { coreMock } from '../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../opensearch_dashboards_legacy/public'; -import { setUiSettings } from '../../../data/public/services'; -import { setFormatService } from '../services'; -import { getInnerAngular } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -const uiSettings = new Map(); - -describe('Table Vis - AggTableGroup Directive', function () { - const core = coreMock.createStart(); - let $rootScope; - let $compile; - - core.uiSettings.set = jest.fn((key, value) => { - uiSettings.set(key, value); - }); - - core.uiSettings.get = jest.fn((key) => { - return uiSettings.get(key); - }); - - const initLocalAngular = () => { - const tableVisModule = getInnerAngular('opensearch-dashboards/table_vis', core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(() => { - setUiSettings(core.uiSettings); - setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); - initLocalAngular(); - angular.mock.module('opensearch-dashboards/table_vis'); - angular.mock.inject(($injector) => { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }); - }); - - let $scope; - beforeEach(function () { - $scope = $rootScope.$new(); - }); - afterEach(function () { - $scope.$destroy(); - }); - - it('renders a simple split response properly', function () { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.group = tabifiedData.metricOnly; - $scope.sort = { - columnIndex: null, - direction: null, - }; - const $el = $( - '<osd-agg-table-group dimensions="dimensions" group="group"></osd-agg-table-group>' - ); - - $compile($el)($scope); - $scope.$digest(); - - // should create one sub-tbale - expect($el.find('osd-agg-table').length).to.be(1); - }); - - it('renders nothing if the table list is empty', function () { - const $el = $( - '<osd-agg-table-group dimensions="dimensions" group="group"></osd-agg-table-group>' - ); - - $scope.group = { - tables: [], - }; - - $compile($el)($scope); - $scope.$digest(); - - const $subTables = $el.find('osd-agg-table'); - expect($subTables.length).to.be(0); - }); - - it('renders a complex response properly', function () { - $scope.dimensions = { - splitRow: [{ accessor: 0, params: {} }], - buckets: [ - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - const group = ($scope.group = tabifiedData.threeTermBucketsWithSplit); - const $el = $( - '<osd-agg-table-group dimensions="dimensions" group="group"></osd-agg-table-group>' - ); - $compile($el)($scope); - $scope.$digest(); - - const $subTables = $el.find('osd-agg-table'); - expect($subTables.length).to.be(3); - - const $subTableHeaders = $el.find('.osdAggTable__groupHeader'); - expect($subTableHeaders.length).to.be(3); - - $subTableHeaders.each(function (i) { - expect($(this).text()).to.be(group.tables[i].title); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/agg_table/tabified_data.js b/src/plugins/vis_type_table/public/agg_table/tabified_data.js deleted file mode 100644 index ce344d5c48b..00000000000 --- a/src/plugins/vis_type_table/public/agg_table/tabified_data.js +++ /dev/null @@ -1,806 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const tabifiedData = { - metricOnly: { - tables: [ - { - columns: [ - { - id: 'col-0-1', - name: 'Count', - }, - ], - rows: [ - { - 'col-0-1': 1000, - }, - ], - }, - ], - }, - threeTermBuckets: { - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_1', - name: 'Average bytes', - }, - { - id: 'col-2-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - { - id: 'col-4-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-5-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'IT', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'IT', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'linux', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'MX', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'MX', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'linux', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-2-agg_3': 'US', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'CN', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'CN', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 9299, - 'col-5-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'FR', - 'col-4-agg_4': 'win', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-2-agg_3': 'FR', - 'col-4-agg_4': 'mac', - 'col-1-agg_1': 412032, - 'col-3-agg_1': 8293, - 'col-5-agg_1': 3029, - }, - ], - }, - ], - }, - threeTermBucketsWithSplit: { - tables: [ - { - title: 'png: extension: Descending', - name: 'extension: Descending', - key: 'png', - column: 0, - row: 0, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - { - title: 'css: extension: Descending', - name: 'extension: Descending', - key: 'css', - column: 0, - row: 4, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - { - title: 'html: extension: Descending', - name: 'extension: Descending', - key: 'html', - column: 0, - row: 8, - table: { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 0, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'IT', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 9299, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'png', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'MX', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'linux', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'css', - 'col-1-agg_3': 'US', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - tables: [ - { - columns: [ - { - id: 'col-0-agg_2', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_3', - name: 'geo.src: Descending', - }, - { - id: 'col-2-agg_4', - name: 'machine.os: Descending', - }, - { - id: 'col-3-agg_1', - name: 'Average bytes', - }, - ], - rows: [ - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 4992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'CN', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 5892, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'win', - 'col-3-agg_1': 3992, - }, - { - 'col-0-agg_2': 'html', - 'col-1-agg_3': 'FR', - 'col-2-agg_4': 'mac', - 'col-3-agg_1': 3029, - }, - ], - }, - ], - }, - ], - direction: 'row', - }, - oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative: { - tables: [ - { - columns: [ - { - id: 'col-0-agg_3', - name: 'extension: Descending', - }, - { - id: 'col-1-agg_4', - name: '@timestamp per day', - }, - { - id: 'col-2-agg_1', - name: 'Average bytes', - }, - { - id: 'col-3-agg_2', - name: 'Min @timestamp', - }, - { - id: 'col-4-agg_5', - name: 'Derivative of Count', - }, - { - id: 'col-5-agg_6', - name: 'Last bytes', - }, - ], - rows: [ - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 23, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 203, - 'col-5-agg_6': 39, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 200, - 'col-5-agg_6': 329, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 103, - 'col-5-agg_6': 22, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 153, - 'col-5-agg_6': 93, - }, - { - 'col-0-agg_3': 'png', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 239, - 'col-5-agg_6': 72, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 75, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 10, - 'col-5-agg_6': 11, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 24, - 'col-5-agg_6': 238, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 49, - 'col-5-agg_6': 343, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 100, - 'col-5-agg_6': 837, - }, - { - 'col-0-agg_3': 'css', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 23, - 'col-5-agg_6': 302, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1411862400000, - 'col-2-agg_1': 9283, - 'col-3-agg_2': 1411862400000, - 'col-5-agg_6': 30, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1411948800000, - 'col-2-agg_1': 28349, - 'col-3-agg_2': 1411948800000, - 'col-4-agg_5': 1, - 'col-5-agg_6': 43, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412035200000, - 'col-2-agg_1': 84330, - 'col-3-agg_2': 1412035200000, - 'col-4-agg_5': 5, - 'col-5-agg_6': 88, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412121600000, - 'col-2-agg_1': 34992, - 'col-3-agg_2': 1412121600000, - 'col-4-agg_5': 10, - 'col-5-agg_6': 91, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412208000000, - 'col-2-agg_1': 145432, - 'col-3-agg_2': 1412208000000, - 'col-4-agg_5': 43, - 'col-5-agg_6': 534, - }, - { - 'col-0-agg_3': 'html', - 'col-1-agg_4': 1412294400000, - 'col-2-agg_1': 220943, - 'col-3-agg_2': 1412294400000, - 'col-4-agg_5': 1, - 'col-5-agg_6': 553, - }, - ], - }, - ], - }, -}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.scss b/src/plugins/vis_type_table/public/components/table_vis_app.scss new file mode 100644 index 00000000000..af6558774da --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_app.scss @@ -0,0 +1,14 @@ +.visTable__group { + padding: $euiSizeS; + margin-bottom: $euiSizeL; + + > h3 { + text-align: center; + } +} + +.visTable__groupInColumns { + display: flex; + flex-direction: row; + align-items: flex-start; +} diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.tsx b/src/plugins/vis_type_table/public/components/table_vis_app.tsx new file mode 100644 index 00000000000..af10500a1a9 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_app.tsx @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './table_vis_app.scss'; +import React, { useEffect, useState } from 'react'; +import classNames from 'classnames'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { I18nProvider } from '@osd/i18n/react'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { TableContext } from '../table_vis_response_handler'; +import { TableVisConfig, ColumnSort, ColumnWidth, TableUiState } from '../types'; +import { TableVisComponent } from './table_vis_component'; +import { TableVisComponentGroup } from './table_vis_component_group'; + +interface TableVisAppProps { + services: CoreStart; + visData: TableContext; + visConfig: TableVisConfig; + handlers: IInterpreterRenderHandlers; +} + +export const TableVisApp = ({ + services, + visData: { table, tableGroups, direction }, + visConfig, + handlers, +}: TableVisAppProps) => { + // Rendering is asynchronous, completed by handlers.done() + useEffect(() => { + handlers.done(); + }, [handlers]); + + const className = classNames('visTable', { + // eslint-disable-next-line @typescript-eslint/naming-convention + visTable__groupInColumns: direction === 'column', + }); + + // TODO: remove duplicate sort and width state + // Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2704#issuecomment-1299380818 + const [sort, setSort] = useState<ColumnSort>({ colIndex: null, direction: null }); + const [width, setWidth] = useState<ColumnWidth[]>([]); + + const tableUiState: TableUiState = { sort, setSort, width, setWidth }; + + return ( + <I18nProvider> + <OpenSearchDashboardsContextProvider services={services}> + <div className={className} data-test-subj="visTable"> + {table ? ( + <TableVisComponent + table={table} + visConfig={visConfig} + event={handlers.event} + uiState={tableUiState} + /> + ) : ( + <TableVisComponentGroup + tableGroups={tableGroups} + visConfig={visConfig} + event={handlers.event} + uiState={tableUiState} + /> + )} + </div> + </OpenSearchDashboardsContextProvider> + </I18nProvider> + ); +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_component.tsx b/src/plugins/vis_type_table/public/components/table_vis_component.tsx new file mode 100644 index 00000000000..4576e3420e2 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component.tsx @@ -0,0 +1,162 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo } from 'react'; +import { orderBy } from 'lodash'; +import dompurify from 'dompurify'; +import { EuiDataGridProps, EuiDataGrid, EuiDataGridSorting, EuiTitle } from '@elastic/eui'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { TableVisConfig, ColumnWidth, ColumnSort, TableUiState } from '../types'; +import { getDataGridColumns } from './table_vis_grid_columns'; +import { usePagination } from '../utils'; +import { convertToFormattedData } from '../utils/convert_to_formatted_data'; +import { TableVisControl } from './table_vis_control'; + +interface TableVisComponentProps { + title?: string; + table: Table; + visConfig: TableVisConfig; + event: IInterpreterRenderHandlers['event']; + uiState: TableUiState; +} + +export const TableVisComponent = ({ + title, + table, + visConfig, + event, + uiState, +}: TableVisComponentProps) => { + const { formattedRows: rows, formattedColumns: columns } = convertToFormattedData( + table, + visConfig + ); + + const pagination = usePagination(visConfig, rows.length); + + const sortedRows = useMemo(() => { + return uiState.sort.colIndex !== null && + columns[uiState.sort.colIndex].id && + uiState.sort.direction + ? orderBy(rows, columns[uiState.sort.colIndex].id, uiState.sort.direction) + : rows; + }, [columns, rows, uiState]); + + const renderCellValue = useMemo(() => { + return (({ rowIndex, columnId }) => { + const rawContent = sortedRows[rowIndex][columnId]; + const colIndex = columns.findIndex((col) => col.id === columnId); + const htmlContent = columns[colIndex].formatter.convert(rawContent, 'html'); + const formattedContent = ( + /* + * Justification for dangerouslySetInnerHTML: + * This is one of the visualizations which makes use of the HTML field formatters. + * Since these formatters produce raw HTML, this visualization needs to be able to render them as-is, relying + * on the field formatter to only produce safe HTML. + * `htmlContent` is created by converting raw data via HTML field formatter, so we need to make sure this value never contains + * any unsafe HTML (e.g. by bypassing the field formatter). + */ + <div dangerouslySetInnerHTML={{ __html: dompurify.sanitize(htmlContent) }} /> // eslint-disable-line react/no-danger + ); + return sortedRows.hasOwnProperty(rowIndex) ? formattedContent || null : null; + }) as EuiDataGridProps['renderCellValue']; + }, [sortedRows, columns]); + + const dataGridColumns = getDataGridColumns(sortedRows, columns, table, event, uiState.width); + + const sortedColumns = useMemo(() => { + return uiState.sort.colIndex !== null && + dataGridColumns[uiState.sort.colIndex].id && + uiState.sort.direction + ? [{ id: dataGridColumns[uiState.sort.colIndex].id, direction: uiState.sort.direction }] + : []; + }, [dataGridColumns, uiState]); + + const onSort = useCallback( + (sortingCols: EuiDataGridSorting['columns'] | []) => { + const nextSortValue = sortingCols[sortingCols.length - 1]; + const nextSort: ColumnSort = + sortingCols.length > 0 + ? { + colIndex: dataGridColumns.findIndex((col) => col.id === nextSortValue?.id), + direction: nextSortValue.direction, + } + : { + colIndex: null, + direction: null, + }; + uiState.setSort(nextSort); + return nextSort; + }, + [dataGridColumns, uiState] + ); + + const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( + ({ columnId, width }) => { + const curWidth: ColumnWidth[] = uiState.width; + const nextWidth = [...curWidth]; + const nextColIndex = columns.findIndex((col) => col.id === columnId); + const curColIndex = curWidth.findIndex((col) => col.colIndex === nextColIndex); + const nextColWidth = { colIndex: nextColIndex, width }; + + // if updated column index is not found, then add it to nextWidth + // else reset it in nextWidth + if (curColIndex < 0) nextWidth.push(nextColWidth); + else nextWidth[curColIndex] = nextColWidth; + + // update uiState.width + uiState.setWidth(nextWidth); + }, + [columns, uiState] + ); + + const ariaLabel = title || visConfig.title || 'tableVis'; + + const footerCellValue = visConfig.showTotal + ? ({ columnId }: { columnId: any }) => { + return columns.find((col) => col.id === columnId)?.formattedTotal || null; + } + : undefined; + + return ( + <> + {title && ( + <EuiTitle size="xs"> + <h3>{title}</h3> + </EuiTitle> + )} + <EuiDataGrid + aria-label={ariaLabel} + columns={dataGridColumns} + columnVisibility={{ + visibleColumns: columns.map(({ id }) => id), + setVisibleColumns: () => {}, + }} + rowCount={rows.length} + renderCellValue={renderCellValue} + sorting={{ columns: sortedColumns, onSort }} + onColumnResize={onColumnResize} + pagination={pagination} + gridStyle={{ + border: 'horizontal', + header: 'underline', + }} + minSizeForControls={1} + renderFooterCellValue={footerCellValue} + toolbarVisibility={{ + showColumnSelector: false, + showSortSelector: false, + showFullScreenSelector: false, + showStyleSelector: false, + additionalControls: ( + <TableVisControl filename={visConfig.title} rows={sortedRows} columns={columns} /> + ), + }} + /> + </> + ); +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx new file mode 100644 index 00000000000..633b9d2230b --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo } from 'react'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { TableGroup } from '../table_vis_response_handler'; +import { TableVisConfig, TableUiState } from '../types'; +import { TableVisComponent } from './table_vis_component'; + +interface TableVisGroupComponentProps { + tableGroups: TableGroup[]; + visConfig: TableVisConfig; + event: IInterpreterRenderHandlers['event']; + uiState: TableUiState; +} + +export const TableVisComponentGroup = memo( + ({ tableGroups, visConfig, event, uiState }: TableVisGroupComponentProps) => { + return ( + <> + {tableGroups.map(({ tables, title }) => ( + <div key={title} className="visTable__group"> + <TableVisComponent + title={title} + table={tables[0]} + visConfig={visConfig} + event={event} + uiState={uiState} + /> + </div> + ))} + </> + ); + } +); diff --git a/src/plugins/vis_type_table/public/components/table_vis_control.tsx b/src/plugins/vis_type_table/public/components/table_vis_control.tsx new file mode 100644 index 00000000000..1e11610b0c2 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_control.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { exportAsCsv } from '../utils/convert_to_csv_data'; +import { FormattedColumn } from '../types'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; + +interface TableVisControlProps { + filename?: string; + rows: OpenSearchDashboardsDatatableRow[]; + columns: FormattedColumn[]; +} + +export const TableVisControl = (props: TableVisControlProps) => { + const { + services: { uiSettings }, + } = useOpenSearchDashboards<CoreStart>(); + const [isPopoverOpen, setPopover] = useState(false); + + return ( + <EuiPopover + id="dataTableExportData" + button={ + <EuiButtonEmpty size="xs" iconType="download" onClick={() => setPopover((open) => !open)} /> + } + isOpen={isPopoverOpen} + closePopover={() => setPopover(false)} + panelPaddingSize="none" + > + <EuiContextMenuPanel + size="s" + items={[ + <EuiContextMenuItem + key="rawCsv" + onClick={() => exportAsCsv(false, { ...props, uiSettings })} + > + Raw + </EuiContextMenuItem>, + <EuiContextMenuItem + key="formattedCsv" + onClick={() => exportAsCsv(true, { ...props, uiSettings })} + > + Formatted + </EuiContextMenuItem>, + ]} + /> + </EuiPopover> + ); +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx b/src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx new file mode 100644 index 00000000000..77d496a1eb4 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_grid_columns.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { ColumnWidth, FormattedColumn } from '../types'; + +export const getDataGridColumns = ( + rows: OpenSearchDashboardsDatatableRow[], + cols: FormattedColumn[], + table: Table, + event: IInterpreterRenderHandlers['event'], + columnWidths: ColumnWidth[] +) => { + const filterBucket = (rowIndex: number, columnIndex: number, negate: boolean) => { + const foramttedColumnId = cols[columnIndex].id; + const rawColumnIndex = table.columns.findIndex((col) => col.id === foramttedColumnId); + event({ + name: 'filterBucket', + data: { + data: [ + { + table: { + columns: table.columns, + rows, + }, + row: rowIndex, + column: rawColumnIndex, + }, + ], + negate, + }, + }); + }; + + return cols.map((col, colIndex) => { + const cellActions = col.filterable + ? [ + ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { + const filterValue = rows[rowIndex][columnId]; + const filterContent = col.formatter?.convert(filterValue); + + const filterForValueText = i18n.translate( + 'visTypeTable.tableVisFilter.filterForValue', + { + defaultMessage: 'Filter for value', + } + ); + const filterForValueLabel = i18n.translate( + 'visTypeTable.tableVisFilter.filterForValueLabel', + { + defaultMessage: 'Filter for value: {filterContent}', + values: { + filterContent, + }, + } + ); + + return ( + filterValue != null && ( + <Component + onClick={() => { + filterBucket(rowIndex, colIndex, false); + closePopover(); + }} + iconType="plusInCircle" + aria-label={filterForValueLabel} + data-test-subj="filterForValue" + > + {filterForValueText} + </Component> + ) + ); + }, + ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { + const filterValue = rows[rowIndex][columnId]; + const filterContent = col.formatter?.convert(filterValue); + + const filterOutValueText = i18n.translate( + 'visTypeTable.tableVisFilter.filterOutValue', + { + defaultMessage: 'Filter out value', + } + ); + const filterOutValueLabel = i18n.translate( + 'visTypeTable.tableVisFilter.filterOutValueLabel', + { + defaultMessage: 'Filter out value: {filterContent}', + values: { + filterContent, + }, + } + ); + + return ( + filterValue != null && ( + <Component + onClick={() => { + filterBucket(rowIndex, colIndex, true); + closePopover(); + }} + iconType="minusInCircle" + aria-label={filterOutValueLabel} + data-test-subj="filterOutValue" + > + {filterOutValueText} + </Component> + ) + ); + }, + ] + : undefined; + + const initialWidth = columnWidths.find((c) => c.colIndex === colIndex); + + const dataGridColumn: EuiDataGridColumn = { + id: col.id, + display: col.title, + displayAsText: col.title, + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: { + label: i18n.translate('visTypeTable.tableVisSort.ascSortLabel', { + defaultMessage: 'Sort asc', + }), + }, + showSortDesc: { + label: i18n.translate('visTypeTable.tableVisSort.descSortLabel', { + defaultMessage: 'Sort desc', + }), + }, + }, + cellActions, + }; + if (initialWidth) { + dataGridColumn.initialWidth = initialWidth.width; + } + return dataGridColumn; + }); +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index e4cebe69fb4..ff99972c83a 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -132,6 +132,7 @@ function TableOptions({ paramName="showTotal" value={stateParams.showTotal} setValue={setValue} + data-test-subj="showTotal" /> <SelectOption @@ -143,6 +144,7 @@ function TableOptions({ paramName="totalFunc" value={stateParams.totalFunc} setValue={setValue} + data-test-subj="totalFunctionOptions" /> <SelectOption @@ -153,7 +155,7 @@ function TableOptions({ paramName="percentageCol" value={stateParams.percentageCol} setValue={setValue} - id="datatableVisualizationPercentageCol" + data-test-subj="datatableVisualizationPercentageCol" /> </EuiPanel> ); diff --git a/src/plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts deleted file mode 100644 index 7f42984d7c0..00000000000 --- a/src/plugins/vis_type_table/public/get_inner_angular.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// inner angular imports -// these are necessary to bootstrap the local angular. -// They can stay even after NP cutover -import angular from 'angular'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; -import 'angular-recursion'; -import { i18nDirective, i18nFilter, I18nProvider } from '@osd/i18n/angular'; -import { - CoreStart, - IUiSettingsClient, - PluginInitializerContext, -} from 'opensearch-dashboards/public'; -import { - initAngularBootstrap, - PaginateDirectiveProvider, - PaginateControlsDirectiveProvider, - PrivateProvider, - watchMultiDecorator, - OsdAccessibleClickProvider, -} from '../../opensearch_dashboards_legacy/public'; - -initAngularBootstrap(); - -const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; - -export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { - const uiModule = getInnerAngular(name, core); - return uiModule; -} - -let initialized = false; - -export function getInnerAngular(name = 'opensearch-dashboards/table_vis', core: CoreStart) { - if (!initialized) { - createLocalPrivateModule(); - createLocalI18nModule(); - createLocalConfigModule(core.uiSettings); - createLocalPaginateModule(); - initialized = true; - } - return angular - .module(name, [ - ...thirdPartyAngularDependencies, - 'tableVisPaginate', - 'tableVisConfig', - 'tableVisPrivate', - 'tableVisI18n', - ]) - .config(watchMultiDecorator) - .directive('osdAccessibleClick', OsdAccessibleClickProvider); -} - -function createLocalPrivateModule() { - angular.module('tableVisPrivate', []).provider('Private', PrivateProvider); -} - -function createLocalConfigModule(uiSettings: IUiSettingsClient) { - angular.module('tableVisConfig', []).provider('config', function () { - return { - $get: () => ({ - get: (value: string) => { - return uiSettings ? uiSettings.get(value) : undefined; - }, - // set method is used in agg_table mocha test - set: (key: string, value: string) => { - return uiSettings ? uiSettings.set(key, value) : undefined; - }, - }), - }; - }); -} - -function createLocalI18nModule() { - angular - .module('tableVisI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createLocalPaginateModule() { - angular - .module('tableVisPaginate', []) - .directive('paginate', PaginateDirectiveProvider) - .directive('paginateControls', PaginateControlsDirectiveProvider); -} diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/index.scss deleted file mode 100644 index d21bf526260..00000000000 --- a/src/plugins/vis_type_table/public/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Prefix all styles with "tbv" to avoid conflicts. -// Examples -// tbvChart -// tbvChart__legend -// tbvChart__legend--small -// tbvChart__legend-isLoading - -@import "./agg_table/index"; -@import "./paginated_table/index"; -@import "./table_vis"; diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts index dc7e7a16b6c..774af319b92 100644 --- a/src/plugins/vis_type_table/public/index.ts +++ b/src/plugins/vis_type_table/public/index.ts @@ -1,37 +1,14 @@ /* + * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. */ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './index.scss'; import { PluginInitializerContext } from 'opensearch-dashboards/public'; import { TableVisPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } + +/* Public Types */ +export { TableVisExpressionFunctionDefinition } from './table_vis_fn'; diff --git a/src/plugins/vis_type_table/public/paginated_table/_index.scss b/src/plugins/vis_type_table/public/paginated_table/_index.scss deleted file mode 100644 index 66275b5c7da..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "./table_cell_filter"; diff --git a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss deleted file mode 100644 index 3deece36b2c..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss +++ /dev/null @@ -1,30 +0,0 @@ -.osdTableCellFilter__hover { - position: relative; - - /** - * 1. Center vertically regardless of row height. - */ - .osdTableCellFilter { - position: absolute; - white-space: nowrap; - right: 0; - top: 50%; /* 1 */ - transform: translateY(-50%); /* 1 */ - display: none; - } - - &:hover { - .osdTableCellFilter { - display: inline; - } - - .osdTableCellFilter__hover-show { - visibility: visible; - } - } -} - -.osdTableCellFilter__hover-show { - // so that the cell doesn't change size on hover - visibility: hidden; -} diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.html b/src/plugins/vis_type_table/public/paginated_table/paginated_table.html deleted file mode 100644 index 83de29a1273..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.html +++ /dev/null @@ -1,55 +0,0 @@ -<paginate - ng-if="sortedRows.length" - list="sortedRows" - per-page-prop="perPage" - class="osdAggTable"> - <div class="osdAggTable__paginated"> - <table class="table table-condensed"> - <thead data-test-subj="paginated-table-header"> - <tr> - <th - scope="col" - ng-repeat="col in columns" - ng-click="paginatedTable.sortColumn($index)" - osd-accessible-click - tabindex="0" - class="{{ col.class }}"> - <span ng-bind="::col.title"></span> - - <icon-tip - ng-if="col.info" - content="'{{ col.info }}'" - ></icon-tip> - - <i - ng-if="col.sortable !== false" - class="fa" - ng-class="{ - 'fa-sort-asc': paginatedTable.sort.columnIndex === $index && paginatedTable.sort.direction === 'asc', - 'fa-sort-desc': paginatedTable.sort.columnIndex === $index && paginatedTable.sort.direction === 'desc', - 'fa-sort': paginatedTable.sort.columnIndex !== $index || paginatedTable.sort.direction === null - }"> - </i> - </th> - </tr> - </thead> - <tbody - data-test-subj="paginated-table-body" - osd-rows="page" - osd-rows-min="page.length" - > - </tbody> - <tfoot ng-if="showTotal"> - <tr> - <th scope="col" ng-repeat="col in columns" class="numeric-value"> - {{ col.formattedTotal }} - </th> - </tr> - </tfoot> - </table> - </div> - - <!-- auto-inserted by the paginate directive... --> - <!-- <paginate-controls></paginate-controls> --> - <div class="pagination-container" ng-transclude></div> -</paginate> diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/paginated_table/paginated_table.js deleted file mode 100644 index c97fa6c2658..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import paginatedTableTemplate from './paginated_table.html'; - -export function PaginatedTable($filter) { - const orderBy = $filter('orderBy'); - - return { - restrict: 'E', - template: paginatedTableTemplate, - transclude: true, - scope: { - table: '=', - rows: '=', - columns: '=', - linkToTop: '=', - perPage: '=?', - sortHandler: '=?', - sort: '=?', - showSelector: '=?', - showTotal: '=', - totalFunc: '=', - filter: '=', - percentageCol: '=', - }, - controllerAs: 'paginatedTable', - controller: function ($scope) { - const self = this; - self.sort = { - columnIndex: null, - direction: null, - }; - - self.sortColumn = function (colIndex, sortDirection = 'asc') { - const col = $scope.columns[colIndex]; - - if (!col) return; - if (col.sortable === false) return; - - if (self.sort.columnIndex === colIndex) { - const directions = { - null: 'asc', - asc: 'desc', - desc: null, - }; - sortDirection = directions[self.sort.direction]; - } - - self.sort.columnIndex = colIndex; - self.sort.direction = sortDirection; - if ($scope.sort) { - _.assign($scope.sort, self.sort); - } - }; - - function valueGetter(row) { - const col = $scope.columns[self.sort.columnIndex]; - let value = row[col.id]; - if (typeof value === 'boolean') value = value ? 0 : 1; - return value; - } - - // Set the sort state if it is set - if ($scope.sort && $scope.sort.columnIndex !== null) { - self.sortColumn($scope.sort.columnIndex, $scope.sort.direction); - } - - function resortRows() { - const newSort = $scope.sort; - if (newSort && !_.isEqual(newSort, self.sort)) { - self.sortColumn(newSort.columnIndex, newSort.direction); - } - - if (!$scope.rows || !$scope.columns) { - $scope.sortedRows = false; - return; - } - - const sort = self.sort; - if (sort.direction == null) { - $scope.sortedRows = $scope.rows.slice(0); - } else { - $scope.sortedRows = orderBy($scope.rows, valueGetter, sort.direction === 'desc'); - } - } - - // update the sortedRows result - $scope.$watchMulti(['rows', 'columns', '[]sort', '[]paginatedTable.sort'], resortRows); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts deleted file mode 100644 index cc86bda4657..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ /dev/null @@ -1,485 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { isNumber, times, identity, random } from 'lodash'; -import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; -import 'angular-sanitize'; -import 'angular-mocks'; - -import { getAngularModule } from '../get_inner_angular'; -import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../core/public/mocks'; - -jest.mock('../../../opensearch_dashboards_legacy/public/angular/angular_config', () => ({ - configureAppAngularModule: () => {}, -})); - -interface Sort { - columnIndex: number; - direction: string; -} - -interface Row { - [key: string]: number | string; -} - -interface Column { - id?: string; - title: string; - formatter?: { - convert?: (val: string) => string; - }; - sortable?: boolean; -} - -interface Table { - columns: Column[]; - rows: Row[]; -} - -interface PaginatedTableScope extends IScope { - table?: Table; - cols?: Column[]; - rows?: Row[]; - perPage?: number; - sort?: Sort; - linkToTop?: boolean; -} - -describe('Table Vis - Paginated table', () => { - let $el: JQuery<Element>; - let $rootScope: IRootScopeService; - let $compile: ICompileService; - let $scope: PaginatedTableScope; - const defaultPerPage = 10; - let paginatedTable: any; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule( - 'opensearch-dashboards/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('opensearch-dashboards/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - $scope = $rootScope.$new(); - }) - ); - - afterEach(() => { - $scope.$destroy(); - }); - - const makeData = (colCount: number | Column[], rowCount: number | string[][]) => { - let columns: Column[] = []; - let rows: Row[] = []; - - if (isNumber(colCount)) { - times(colCount, (i) => { - columns.push({ id: `${i}`, title: `column${i}`, formatter: { convert: identity } }); - }); - } else { - columns = colCount.map( - (col, i) => - ({ - id: `${i}`, - title: col.title, - formatter: col.formatter || { convert: identity }, - } as Column) - ); - } - - if (isNumber(rowCount)) { - times(rowCount, (row) => { - const rowItems: Row = {}; - - times(columns.length, (col) => { - rowItems[`${col}`] = `item-${col}-${row}`; - }); - - rows.push(rowItems); - }); - } else { - rows = rowCount.map((row: string[]) => { - const newRow: Row = {}; - row.forEach((v, i) => (newRow[i] = v)); - return newRow; - }); - } - - return { - columns, - rows, - }; - }; - - const renderTable = ( - table: { columns: Column[]; rows: Row[] } | null, - cols: Column[], - rows: Row[], - perPage?: number, - sort?: Sort, - linkToTop?: boolean - ) => { - $scope.table = table || { columns: [], rows: [] }; - $scope.cols = cols || []; - $scope.rows = rows || []; - $scope.perPage = perPage || defaultPerPage; - $scope.sort = sort; - $scope.linkToTop = linkToTop; - - const template = ` - <paginated-table - table="table" - columns="cols" - rows="rows" - per-page="perPage" - sort="sort" - link-to-top="linkToTop">`; - const element = $compile(template)($scope); - $el = $(element); - - $scope.$digest(); - paginatedTable = element.controller('paginatedTable'); - }; - - describe('rendering', () => { - test('should not display without rows', () => { - const cols: Column[] = [ - { - id: 'col-1-1', - title: 'test1', - }, - ]; - const rows: Row[] = []; - - renderTable(null, cols, rows); - expect($el.children().length).toBe(0); - }); - - test('should render columns and rows', () => { - const data = makeData(2, 2); - const cols = data.columns; - const rows = data.rows; - - renderTable(data, cols, rows); - expect($el.children().length).toBe(1); - const tableRows = $el.find('tbody tr'); - - // should contain the row data - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(rows[0][0]); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe(rows[0][1]); - expect(tableRows.eq(1).find('td').eq(0).text()).toBe(rows[1][0]); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe(rows[1][1]); - }); - - test('should paginate rows', () => { - // note: paginate truncates pages, so don't make too many - const rowCount = random(16, 24); - const perPageCount = random(5, 8); - const data = makeData(3, rowCount); - const pageCount = Math.ceil(rowCount / perPageCount); - - renderTable(data, data.columns, data.rows, perPageCount); - const tableRows = $el.find('tbody tr'); - expect(tableRows.length).toBe(perPageCount); - // add 2 for the first and last page links - expect($el.find('paginate-controls button').length).toBe(pageCount + 2); - }); - - test('should not show blank rows on last page', () => { - const rowCount = 7; - const perPageCount = 10; - const data = makeData(3, rowCount); - - renderTable(data, data.columns, data.rows, perPageCount); - const tableRows = $el.find('tbody tr'); - expect(tableRows.length).toBe(rowCount); - }); - - test('should not show link to top when not set', () => { - const data = makeData(5, 5); - renderTable(data, data.columns, data.rows, 10); - - const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]'); - expect(linkToTop.length).toBe(0); - }); - - test('should show link to top when set', () => { - const data = makeData(5, 5); - renderTable(data, data.columns, data.rows, 10, undefined, true); - - const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]'); - expect(linkToTop.length).toBe(1); - }); - }); - - describe('sorting', () => { - let data: Table; - let lastRowIndex: number; - - beforeEach(() => { - data = makeData(3, [ - ['bbbb', 'aaaa', 'zzzz'], - ['cccc', 'cccc', 'aaaa'], - ['zzzz', 'bbbb', 'bbbb'], - ['aaaa', 'zzzz', 'cccc'], - ]); - - lastRowIndex = data.rows.length - 1; - renderTable(data, data.columns, data.rows); - }); - - test('should not sort by default', () => { - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(data.rows[0][0]); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe(data.rows[lastRowIndex][0]); - }); - - test('should do nothing when sorting by invalid column id', () => { - // sortColumn - paginatedTable.sortColumn(999); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('zzzz'); - }); - - test('should do nothing when sorting by non sortable column', () => { - data.columns[0].sortable = false; - - // sortColumn - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('zzzz'); - }); - - test("should set the sort direction to asc when it's not explicitly set", () => { - paginatedTable.sortColumn(1); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(2).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe('bbbb'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - }); - - test('should allow you to explicitly set the sort direction', () => { - paginatedTable.sortColumn(1, 'desc'); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('zzzz'); - expect(tableRows.eq(1).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(2).find('td').eq(1).text()).toBe('bbbb'); - }); - - test('should sort ascending on first invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('aaaa'); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('zzzz'); - }); - - test('should sort descending on second invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('zzzz'); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('aaaa'); - }); - - test('should clear sorting on third invocation', () => { - // sortColumn - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - paginatedTable.sortColumn(0); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe(data.rows[0][0]); - expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).toBe('aaaa'); - }); - - test('should sort new column ascending', () => { - // sort by first column - paginatedTable.sortColumn(0); - $scope.$digest(); - - // sort by second column - paginatedTable.sortColumn(1); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('aaaa'); - expect(tableRows.eq(lastRowIndex).find('td').eq(1).text()).toBe('zzzz'); - }); - }); - - describe('sorting duplicate columns', () => { - let data; - const colText = 'test row'; - - beforeEach(() => { - const cols: Column[] = [{ title: colText }, { title: colText }, { title: colText }]; - const rows = [ - ['bbbb', 'aaaa', 'zzzz'], - ['cccc', 'cccc', 'aaaa'], - ['zzzz', 'bbbb', 'bbbb'], - ['aaaa', 'zzzz', 'cccc'], - ]; - data = makeData(cols, rows); - - renderTable(data, data.columns, data.rows); - }); - - test('should have duplicate column titles', () => { - const columns = $el.find('thead th span'); - columns.each((i, col) => { - expect($(col).text()).toBe(colText); - }); - }); - - test('should handle sorting on columns with the same name', () => { - // sort by the last column - paginatedTable.sortColumn(2); - $scope.$digest(); - - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('aaaa'); - expect(tableRows.eq(1).find('td').eq(2).text()).toBe('bbbb'); - expect(tableRows.eq(2).find('td').eq(2).text()).toBe('cccc'); - expect(tableRows.eq(3).find('td').eq(2).text()).toBe('zzzz'); - }); - - test('should sort correctly between columns', () => { - // sort by the last column - paginatedTable.sortColumn(2); - $scope.$digest(); - - let tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('cccc'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('aaaa'); - - // sort by the first column - paginatedTable.sortColumn(0); - $scope.$digest(); - - tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('td').eq(0).text()).toBe('aaaa'); - expect(tableRows.eq(0).find('td').eq(1).text()).toBe('zzzz'); - expect(tableRows.eq(0).find('td').eq(2).text()).toBe('cccc'); - - expect(tableRows.eq(1).find('td').eq(0).text()).toBe('bbbb'); - expect(tableRows.eq(2).find('td').eq(0).text()).toBe('cccc'); - expect(tableRows.eq(3).find('td').eq(0).text()).toBe('zzzz'); - }); - - test('should not sort duplicate columns', () => { - paginatedTable.sortColumn(1); - $scope.$digest(); - - const sorters = $el.find('thead th i'); - expect(sorters.eq(0).hasClass('fa-sort')).toBe(true); - expect(sorters.eq(1).hasClass('fa-sort')).toBe(false); - expect(sorters.eq(2).hasClass('fa-sort')).toBe(true); - }); - }); - - describe('object rows', () => { - let cols: Column[]; - let rows: any; - - beforeEach(() => { - cols = [ - { - title: 'object test', - id: '0', - formatter: { - convert: (val) => { - return val === 'zzz' ? '<h1>hello</h1>' : val; - }, - }, - }, - ]; - rows = [['aaaa'], ['zzz'], ['bbbb']]; - renderTable({ columns: cols, rows }, cols, rows); - }); - - test('should append object markup', () => { - const tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('h1').length).toBe(0); - expect(tableRows.eq(1).find('h1').length).toBe(1); - expect(tableRows.eq(2).find('h1').length).toBe(0); - }); - - test('should sort using object value', () => { - paginatedTable.sortColumn(0); - $scope.$digest(); - let tableRows = $el.find('tbody tr'); - expect(tableRows.eq(0).find('h1').length).toBe(0); - expect(tableRows.eq(1).find('h1').length).toBe(0); - // html row should be the last row - expect(tableRows.eq(2).find('h1').length).toBe(1); - - paginatedTable.sortColumn(0); - $scope.$digest(); - tableRows = $el.find('tbody tr'); - // html row should be the first row - expect(tableRows.eq(0).find('h1').length).toBe(1); - expect(tableRows.eq(1).find('h1').length).toBe(0); - expect(tableRows.eq(2).find('h1').length).toBe(0); - }); - }); -}); diff --git a/src/plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/paginated_table/rows.js deleted file mode 100644 index 5ed2de5de17..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/rows.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import angular from 'angular'; -import tableCellFilterHtml from './table_cell_filter.html'; - -export function OsdRows($compile) { - return { - restrict: 'A', - link: function ($scope, $el, attr) { - function addCell($tr, contents, column, row) { - function createCell() { - return $(document.createElement('td')); - } - - function createFilterableCell(value) { - const $template = $(tableCellFilterHtml); - $template.addClass('osdTableCellFilter__hover'); - - const scope = $scope.$new(); - - scope.onFilterClick = (event, negate) => { - // Don't add filter if a link was clicked. - if ($(event.target).is('a')) { - return; - } - - $scope.filter({ - data: [ - { - table: $scope.table, - row: $scope.rows.findIndex((r) => r === row), - column: $scope.table.columns.findIndex((c) => c.id === column.id), - value, - }, - ], - negate, - }); - }; - - return $compile($template)(scope); - } - - let $cell; - let $cellContent; - - const contentsIsDefined = contents !== null && contents !== undefined; - - if (column.filterable && contentsIsDefined) { - $cell = createFilterableCell(contents); - // in jest tests 'angular' is using jqLite. In jqLite the method find lookups only by tags. - // Because of this, we should change a way how we get cell content so that tests will pass. - $cellContent = angular.element($cell[0].querySelector('[data-cell-content]')); - } else { - $cell = $cellContent = createCell(); - } - - // An AggConfigResult can "enrich" cell contents by applying a field formatter, - // which we want to do if possible. - contents = contentsIsDefined ? column.formatter.convert(contents, 'html') : ''; - - if (_.isObject(contents)) { - if (contents.attr) { - $cellContent.attr(contents.attr); - } - - if (contents.class) { - $cellContent.addClass(contents.class); - } - - if (contents.scope) { - $cellContent = $compile($cellContent.prepend(contents.markup))(contents.scope); - } else { - $cellContent.prepend(contents.markup); - } - - if (contents.attr) { - $cellContent.attr(contents.attr); - } - } else { - if (contents === '') { - $cellContent.prepend(' '); - } else { - $cellContent.prepend(contents); - } - } - - $tr.append($cell); - } - - $scope.$watchMulti([attr.osdRows, attr.osdRowsMin], function (vals) { - let rows = vals[0]; - const min = vals[1]; - - $el.empty(); - - if (!Array.isArray(rows)) rows = []; - - if (isFinite(min) && rows.length < min) { - // clone the rows so that we can add elements to it without upsetting the original - rows = _.clone(rows); - // crate the empty row which will be pushed into the row list over and over - const emptyRow = {}; - // push as many empty rows into the row array as needed - _.times(min - rows.length, function () { - rows.push(emptyRow); - }); - } - - rows.forEach(function (row) { - const $tr = $(document.createElement('tr')).appendTo($el); - $scope.columns.forEach((column) => { - const value = row[column.id]; - addCell($tr, value, column, row); - }); - }); - }); - }, - }; -} diff --git a/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html deleted file mode 100644 index a9185884dae..00000000000 --- a/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html +++ /dev/null @@ -1,23 +0,0 @@ -<td> - <div - data-cell-content - class="osdTableCellFilter__hover" - > - <span class="osdTableCellFilter"> - <span - data-test-subj="filterForCellValue" - ng-click="onFilterClick($event, false)" - class="fa fa-search-plus" - tooltip="{{ ::'visTypeTable.directives.tableCellFilter.filterForValueTooltip' | i18n: {defaultMessage: 'Filter for value'} }}" - tooltip-append-to-body="1" - ></span> - - <span - ng-click="onFilterClick($event, true)" - class="fa fa-search-minus" - tooltip="{{ ::'visTypeTable.directives.tableCellFilter.filterOutValueTooltip' | i18n: {defaultMessage: 'Filter out value'} }}" - tooltip-append-to-body="1" - ></span> - </span> - </div> -</td> diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 8c01fee8841..0582ebbedeb 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -1,31 +1,6 @@ /* + * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. */ import { @@ -40,45 +15,38 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { createTableVisFn } from './table_vis_fn'; import { getTableVisTypeDefinition } from './table_vis_type'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setOpenSearchDashboardsLegacy } from './services'; -import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public'; - -/** @internal */ -export interface TablePluginSetupDependencies { +import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; +import { getTableVisRenderer } from './table_vis_renderer'; +export interface TableVisPluginSetupDependencies { expressions: ReturnType<ExpressionsPublicPlugin['setup']>; visualizations: VisualizationsSetup; } - -/** @internal */ -export interface TablePluginStartDependencies { +export interface TableVisPluginStartDependencies { data: DataPublicPluginStart; - opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart; } -/** @internal */ -export class TableVisPlugin implements Plugin<Promise<void>, void> { - initializerContext: PluginInitializerContext; - createBaseVisualization: any; - - constructor(initializerContext: PluginInitializerContext) { +const setupTableVis = async ( + core: CoreSetup, + { expressions, visualizations }: TableVisPluginSetupDependencies +) => { + const [coreStart] = await core.getStartServices(); + expressions.registerFunction(createTableVisFn); + expressions.registerRenderer(getTableVisRenderer(coreStart)); + visualizations.createBaseVisualization(getTableVisTypeDefinition()); +}; +export class TableVisPlugin implements Plugin<void, void> { + initializerContext: PluginInitializerContext<ConfigSchema>; + + constructor(initializerContext: PluginInitializerContext<ConfigSchema>) { this.initializerContext = initializerContext; } - public async setup( - core: CoreSetup, - { expressions, visualizations }: TablePluginSetupDependencies - ) { - expressions.registerFunction(createTableVisFn); - visualizations.createBaseVisualization( - getTableVisTypeDefinition(core, this.initializerContext) - ); + public async setup(core: CoreSetup, dependencies: TableVisPluginSetupDependencies) { + setupTableVis(core, dependencies); } - public start( - core: CoreStart, - { data, opensearchDashboardsLegacy }: TablePluginStartDependencies - ) { + public start(core: CoreStart, { data }: TableVisPluginStartDependencies) { setFormatService(data.fieldFormats); - setOpenSearchDashboardsLegacy(opensearchDashboardsLegacy); } } diff --git a/src/plugins/vis_type_table/public/services.ts b/src/plugins/vis_type_table/public/services.ts index 4fb56f6bfbd..f8ca4b57430 100644 --- a/src/plugins/vis_type_table/public/services.ts +++ b/src/plugins/vis_type_table/public/services.ts @@ -1,41 +1,11 @@ /* + * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. */ import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public'; export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('table data.fieldFormats'); - -export const [getOpenSearchDashboardsLegacy, setOpenSearchDashboardsLegacy] = createGetterSetter< - OpenSearchDashboardsLegacyStart ->('table opensearchDashboardsLegacy'); diff --git a/src/plugins/vis_type_table/public/table_vis.html b/src/plugins/vis_type_table/public/table_vis.html deleted file mode 100644 index 169b53390fe..00000000000 --- a/src/plugins/vis_type_table/public/table_vis.html +++ /dev/null @@ -1,29 +0,0 @@ -<div ng-controller="OsdTableVisController" class="table-vis"> - <div ng-if="!hasSomeRows && hasSomeRows !== null" class="visError"> - <div class="euiText euiText--extraSmall euiTextColor euiTextColor--subdued"> - <icon type="'visualizeApp'" size="'m'" color="'subdued'"></icon> - - <br> - <br> - - <p - i18n-id="visTypeTable.vis.noResultsFoundTitle" - i18n-default-message="No results found"> - </p> - </div> - </div> - - <div ng-if="tableGroups" class="table-vis-container" data-test-subj="tableVis"> - <osd-agg-table-group - filter="vis.API.events.filter" - dimensions="dimensions" - group="tableGroups" - export-title="visState.title" - per-page="visState.params.perPage" - sort="sort" - percentage-col="visState.params.percentageCol" - show-total="visState.params.showTotal" - total-func="visState.params.totalFunc"> - </osd-agg-table-group> - </div> -</div> diff --git a/src/plugins/vis_type_table/public/table_vis_controller.js b/src/plugins/vis_type_table/public/table_vis_controller.js deleted file mode 100644 index 9fa71534903..00000000000 --- a/src/plugins/vis_type_table/public/table_vis_controller.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { assign } from 'lodash'; - -export function TableVisController($scope) { - const uiStateSort = $scope.uiState ? $scope.uiState.get('vis.params.sort') : {}; - assign($scope.visParams.sort, uiStateSort); - - $scope.sort = $scope.visParams.sort; - $scope.$watchCollection('sort', function (newSort) { - $scope.uiState.set('vis.params.sort', newSort); - }); - - /** - * Recreate the entire table when: - * - the underlying data changes (opensearchResponse) - * - one of the view options changes (vis.params) - */ - $scope.$watch('renderComplete', function () { - let tableGroups = ($scope.tableGroups = null); - let hasSomeRows = ($scope.hasSomeRows = null); - - if ($scope.opensearchResponse) { - tableGroups = $scope.opensearchResponse; - - hasSomeRows = tableGroups.tables.some(function haveRows(table) { - if (table.tables) return table.tables.some(haveRows); - return table.rows.length > 0; - }); - } - - $scope.hasSomeRows = hasSomeRows; - if (hasSomeRows) { - $scope.dimensions = $scope.visParams.dimensions; - $scope.tableGroups = tableGroups; - } - $scope.renderComplete(); - }); -} diff --git a/src/plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts deleted file mode 100644 index db12e2b5142..00000000000 --- a/src/plugins/vis_type_table/public/table_vis_controller.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular, { IRootScopeService, IScope, ICompileService } from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import $ from 'jquery'; - -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { getTableVisTypeDefinition } from './table_vis_type'; -import { Vis } from '../../visualizations/public'; -import { stubFields } from '../../data/public/stubs'; -import { tableVisResponseHandler } from './table_vis_response_handler'; -import { coreMock } from '../../../core/public/mocks'; -import { IAggConfig, search } from '../../data/public'; -import { getStubIndexPattern } from '../../data/public/test_utils'; -// TODO: remove linting disable -import { searchServiceMock } from '../../data/public/search/mocks'; - -const { createAggConfigs } = searchServiceMock.createStartContract().aggs; - -const { tabifyAggResponse } = search; - -jest.mock('../../opensearch_dashboards_legacy/public/angular/angular_config', () => ({ - configureAppAngularModule: () => {}, -})); - -interface TableVisScope extends IScope { - [key: string]: any; -} - -const oneRangeBucket = { - hits: { - total: 6039, - max_score: 0, - hits: [], - }, - aggregations: { - agg_2: { - buckets: { - '0.0-1000.0': { - from: 0, - from_as_string: '0.0', - to: 1000, - to_as_string: '1000.0', - doc_count: 606, - }, - '1000.0-2000.0': { - from: 1000, - from_as_string: '1000.0', - to: 2000, - to_as_string: '2000.0', - doc_count: 298, - }, - }, - }, - }, -}; - -describe('Table Vis - Controller', () => { - let $rootScope: IRootScopeService & { [key: string]: any }; - let $compile: ICompileService; - let $scope: TableVisScope; - let $el: JQuery<HTMLElement>; - let tableAggResponse: any; - let tabifiedResponse: any; - let stubIndexPattern: any; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule( - 'opensearch-dashboards/table_vis', - coreMock.createStart(), - coreMock.createPluginInitializerContext() - ); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - beforeEach(angular.mock.module('opensearch-dashboards/table_vis')); - - beforeEach( - angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => { - $rootScope = _$rootScope_; - $compile = _$compile_; - tableAggResponse = tableVisResponseHandler; - }) - ); - - beforeEach(() => { - stubIndexPattern = getStubIndexPattern( - 'logstash-*', - (cfg: any) => cfg, - 'time', - stubFields, - coreMock.createSetup() - ); - }); - const tableVisTypeDefinition = getTableVisTypeDefinition( - coreMock.createSetup(), - coreMock.createPluginInitializerContext() - ); - - function getRangeVis(params?: object) { - return ({ - type: tableVisTypeDefinition, - params: Object.assign({}, tableVisTypeDefinition.visConfig?.defaults, params), - data: { - aggs: createAggConfigs(stubIndexPattern, [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 }, - ], - }, - }, - ]), - }, - } as unknown) as Vis; - } - - const dimensions = { - buckets: [ - { - accessor: 0, - }, - ], - metrics: [ - { - accessor: 1, - format: { id: 'range' }, - }, - ], - }; - - // basically a parameterized beforeEach - function initController(vis: Vis) { - vis.data.aggs!.aggs.forEach((agg: IAggConfig, i: number) => { - agg.id = 'agg_' + (i + 1); - }); - - tabifiedResponse = tabifyAggResponse(vis.data.aggs!, oneRangeBucket); - $rootScope.vis = vis; - $rootScope.visParams = vis.params; - $rootScope.uiState = { - get: jest.fn(), - set: jest.fn(), - }; - $rootScope.renderComplete = () => {}; - $rootScope.newScope = (scope: TableVisScope) => { - $scope = scope; - }; - - $el = $('<div>') - .attr('ng-controller', 'OsdTableVisController') - .attr('ng-init', 'newScope(this)'); - - $compile($el)($rootScope); - } - - // put a response into the controller - function attachOpenSearchResponseToScope(resp: object) { - $rootScope.opensearchResponse = resp; - $rootScope.$apply(); - } - - // remove the response from the controller - function removeOpenSearchResponseFromScope() { - delete $rootScope.opensearchResponse; - $rootScope.renderComplete = () => {}; - $rootScope.$apply(); - } - - test('exposes #tableGroups and #hasSomeRows when a response is attached to scope', async () => { - const vis: Vis = getRangeVis(); - initController(vis); - - expect(!$scope.tableGroups).toBeTruthy(); - expect(!$scope.hasSomeRows).toBeTruthy(); - - attachOpenSearchResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.hasSomeRows).toBeTruthy(); - expect($scope.tableGroups.tables).toBeDefined(); - expect($scope.tableGroups.tables.length).toBe(1); - expect($scope.tableGroups.tables[0].columns.length).toBe(2); - expect($scope.tableGroups.tables[0].rows.length).toBe(2); - }); - - test('clears #tableGroups and #hasSomeRows when the response is removed', async () => { - const vis = getRangeVis(); - initController(vis); - - attachOpenSearchResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - removeOpenSearchResponseFromScope(); - - expect(!$scope.hasSomeRows).toBeTruthy(); - expect(!$scope.tableGroups).toBeTruthy(); - }); - - test('sets the sort on the scope when it is passed as a vis param', async () => { - const sortObj = { - columnIndex: 1, - direction: 'asc', - }; - const vis = getRangeVis({ sort: sortObj }); - initController(vis); - - attachOpenSearchResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.sort.columnIndex).toEqual(sortObj.columnIndex); - expect($scope.sort.direction).toEqual(sortObj.direction); - }); - - test('sets #hasSomeRows properly if the table group is empty', async () => { - const vis = getRangeVis(); - initController(vis); - - tabifiedResponse.rows = []; - - attachOpenSearchResponseToScope(await tableAggResponse(tabifiedResponse, dimensions)); - - expect($scope.hasSomeRows).toBeFalsy(); - expect(!$scope.tableGroups).toBeTruthy(); - }); - - test('passes partialRows:true to tabify based on the vis params', () => { - const vis = getRangeVis({ showPartialRows: true }); - initController(vis); - - expect((vis.type.hierarchicalData as Function)(vis)).toEqual(true); - }); - - test('passes partialRows:false to tabify based on the vis params', () => { - const vis = getRangeVis({ showPartialRows: false }); - initController(vis); - - expect((vis.type.hierarchicalData as Function)(vis)).toEqual(false); - }); -}); diff --git a/src/plugins/vis_type_table/public/table_vis_fn.test.ts b/src/plugins/vis_type_table/public/table_vis_fn.test.ts index f7723456b75..b4a4ca4776b 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.test.ts @@ -84,6 +84,6 @@ describe('interpreter/functions#table', () => { it('calls response handler with correct values', async () => { await fn(context, { visConfig: JSON.stringify(visConfig) }, undefined); expect(tableVisResponseHandler).toHaveBeenCalledTimes(1); - expect(tableVisResponseHandler).toHaveBeenCalledWith(context, visConfig.dimensions); + expect(tableVisResponseHandler).toHaveBeenCalledWith(context, visConfig); }); }); diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index daf76580b59..4f0fb2c0ba1 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -1,31 +1,6 @@ /* + * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. */ import { i18n } from '@osd/i18n'; @@ -35,7 +10,7 @@ import { OpenSearchDashboardsDatatable, Render, } from '../../expressions/public'; -import { VisRenderValue } from '../../visualizations/public'; +import { TableVisConfig } from './types'; export type Input = OpenSearchDashboardsDatatable; @@ -43,17 +18,20 @@ interface Arguments { visConfig: string | null; } -interface RenderValue extends VisRenderValue { +export interface TableVisRenderValue { visData: TableContext; visType: 'table'; + visConfig: TableVisConfig; } -export const createTableVisFn = (): ExpressionFunctionDefinition< +export type TableVisExpressionFunctionDefinition = ExpressionFunctionDefinition< 'opensearch_dashboards_table', Input, Arguments, - Render<RenderValue> -> => ({ + Render<TableVisRenderValue> +>; + +export const createTableVisFn = (): TableVisExpressionFunctionDefinition => ({ name: 'opensearch_dashboards_table', type: 'render', inputTypes: ['opensearch_dashboards_datatable'], @@ -69,18 +47,15 @@ export const createTableVisFn = (): ExpressionFunctionDefinition< }, fn(input, args) { const visConfig = args.visConfig && JSON.parse(args.visConfig); - const convertedData = tableVisResponseHandler(input, visConfig.dimensions); + const convertedData = tableVisResponseHandler(input, visConfig); return { type: 'render', - as: 'visualization', + as: 'table_vis', value: { visData: convertedData, visType: 'table', visConfig, - params: { - listenOnChange: true, - }, }, }; }, diff --git a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/table_vis_legacy_module.ts deleted file mode 100644 index 49eed3494f9..00000000000 --- a/src/plugins/vis_type_table/public/table_vis_legacy_module.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IModule } from 'angular'; - -// @ts-ignore -import { TableVisController } from './table_vis_controller.js'; -// @ts-ignore -import { OsdAggTable } from './agg_table/agg_table'; -// @ts-ignore -import { OsdAggTableGroup } from './agg_table/agg_table_group'; -// @ts-ignore -import { OsdRows } from './paginated_table/rows'; -// @ts-ignore -import { PaginatedTable } from './paginated_table/paginated_table'; - -/** @internal */ -export const initTableVisLegacyModule = (angularIns: IModule): void => { - angularIns - .controller('OsdTableVisController', TableVisController) - .directive('osdAggTable', OsdAggTable) - .directive('osdAggTableGroup', OsdAggTableGroup) - .directive('osdRows', OsdRows) - .directive('paginatedTable', PaginatedTable); -}; diff --git a/src/plugins/vis_type_table/public/table_vis_renderer.tsx b/src/plugins/vis_type_table/public/table_vis_renderer.tsx new file mode 100644 index 00000000000..8e467112528 --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_renderer.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { CoreStart } from 'opensearch-dashboards/public'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { TableVisRenderValue } from './table_vis_fn'; +import { TableVisApp } from './components/table_vis_app'; + +export const getTableVisRenderer: ( + core: CoreStart +) => ExpressionRenderDefinition<TableVisRenderValue> = (core) => ({ + name: 'table_vis', + displayName: 'table visualization', + reuseDomNode: true, + render: async (domNode, { visData, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const showNoResult = visData.table + ? visData.table.rows.length === 0 + : visData.tableGroups?.length === 0; + render( + <VisualizationContainer className="tableVis" showNoResult={showNoResult}> + <TableVisApp services={core} visData={visData} visConfig={visConfig} handlers={handlers} /> + </VisualizationContainer>, + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_table/public/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.ts index 78b2306e744..b1d41edfff8 100644 --- a/src/plugins/vis_type_table/public/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/table_vis_response_handler.ts @@ -28,19 +28,17 @@ * under the License. */ -import { Required } from '@osd/utility-types'; - import { getFormatService } from './services'; -import { Input } from './table_vis_fn'; +import { OpenSearchDashboardsDatatable } from '../../expressions/public'; +import { TableVisConfig } from './types'; -export interface TableContext { - tables: Array<TableGroup | Table>; - direction?: 'row' | 'column'; +export interface Table { + columns: OpenSearchDashboardsDatatable['columns']; + rows: OpenSearchDashboardsDatatable['rows']; } export interface TableGroup { - $parent: TableContext; - table: Input; + table: OpenSearchDashboardsDatatable; tables: Table[]; title: string; name: string; @@ -49,61 +47,66 @@ export interface TableGroup { row: number; } -export interface Table { - $parent?: TableGroup; - columns: Input['columns']; - rows: Input['rows']; +export interface TableContext { + table?: Table; + tableGroups: TableGroup[]; + direction?: 'row' | 'column'; } -export function tableVisResponseHandler(table: Input, dimensions: any): TableContext { - const converted: TableContext = { - tables: [], - }; +export function tableVisResponseHandler( + input: OpenSearchDashboardsDatatable, + config: TableVisConfig +): TableContext { + let table: Table | undefined; + const tableGroups: TableGroup[] = []; + let direction: TableContext['direction']; - const split = dimensions.splitColumn || dimensions.splitRow; + const split = config.splitColumn || config.splitRow; if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; + direction = config.splitRow ? 'row' : 'column'; const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); - const splitColumn = table.columns[splitColumnIndex]; - const splitMap = {}; + const splitColumn = input.columns[splitColumnIndex]; + const splitMap: { [key: string]: number } = {}; let splitIndex = 0; - table.rows.forEach((row, rowIndex) => { + input.rows.forEach((row, rowIndex) => { const splitValue: any = row[splitColumn.id]; if (!splitMap.hasOwnProperty(splitValue as any)) { (splitMap as any)[splitValue] = splitIndex++; - const tableGroup: Required<TableGroup, 'tables'> = { - $parent: converted, + const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, name: splitColumn.name, key: splitValue, column: splitColumnIndex, row: rowIndex, - table, + table: input, tables: [], }; tableGroup.tables.push({ - $parent: tableGroup, - columns: table.columns, + columns: input.columns, rows: [], }); - converted.tables.push(tableGroup); + tableGroups.push(tableGroup); } const tableIndex = (splitMap as any)[splitValue]; - (converted.tables[tableIndex] as any).tables[0].rows.push(row); + (tableGroups[tableIndex] as any).tables[0].rows.push(row); }); } else { - converted.tables.push({ - columns: table.columns, - rows: table.rows, - }); + table = { + columns: input.columns, + rows: input.rows, + }; } - return converted; + return { + table, + tableGroups, + direction, + }; } diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index df1495a3d06..0c27e7a8af0 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -28,91 +28,78 @@ * under the License. */ -import { CoreSetup, PluginInitializerContext } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; +import { toExpressionAst } from './to_ast'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; import { TableOptions } from './components/table_vis_options_lazy'; -import { getTableVisualizationControllerClass } from './vis_controller'; -import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; -export function getTableVisTypeDefinition( - core: CoreSetup, - context: PluginInitializerContext -): BaseVisTypeOptions { - return { - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: getTableVisualizationControllerClass(core, context), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; +export const getTableVisTypeDefinition = (): BaseVisTypeOptions => ({ + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + toExpressionAst, + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', }, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', - }, - template: tableVisTemplate, - }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, - }; -} + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + responseHandler: tableVisResponseHandler, + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, + hierarchicalData: (vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, +}); diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts new file mode 100644 index 00000000000..be2741e8de4 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { toExpressionAst } from './to_ast'; +import { Vis } from '../../visualizations/public'; + +describe('table vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + isHierarchical: () => false, + type: {}, + params: {}, + data: { + indexPattern: { id: '123' } as any, + aggs: { + getResponseAggs: () => [], + aggs: [], + } as any, + }, + } as any; + }); + + it('without params', () => { + vis.params = { table: {} }; + const actual = toExpressionAst(vis, {}); + expect(actual).toMatchSnapshot(); + }); + + it('with default params', () => { + vis.params = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }; + const actual = toExpressionAst(vis, {}); + expect(actual).toMatchSnapshot(); + }); + + it('with customized params', () => { + vis.params = { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: true, + totalFunc: 'min', + percentageCol: 'Count', + }; + const actual = toExpressionAst(vis, {}); + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts new file mode 100644 index 00000000000..3753c35cfc2 --- /dev/null +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getVisSchemas, Vis } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { TableVisExpressionFunctionDefinition } from './table_vis_fn'; +import { OpenSearchaggsExpressionFunctionDefinition } from '../../data/common/search/expressions'; + +export const toExpressionAst = (vis: Vis, params: any) => { + const opensearchaggs = buildExpressionFunction<OpenSearchaggsExpressionFunctionDefinition>( + 'opensearchaggs', + { + index: vis.data.indexPattern!.id!, + metricsAtAllLevels: vis.isHierarchical(), + partialRows: vis.params.showPartialRows || false, + aggConfigs: JSON.stringify(vis.data.aggs!.aggs), + includeFormatHints: false, + } + ); + + const schemas = getVisSchemas(vis, params); + // if customer selects showPartialRows without showMetricsAtAllLevels, + // we need to remove the duplicated metrics. + // First, we need to calculate the required number of metrics + // Then, we return one copy of the required metrics from metric array + const metrics = + schemas.bucket && vis.params.showPartialRows && !vis.params.showMetricsAtAllLevels + ? schemas.metric.slice(-1 * (schemas.metric.length / schemas.bucket.length)) + : schemas.metric; + + const tableData = { + title: vis.title, + metrics, + buckets: schemas.bucket || [], + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const tableConfig = { + perPage: vis.params.perPage, + percentageCol: vis.params.percentageCol, + showPartialRows: vis.params.showPartialRows, + showMetricsAtAllLevels: vis.params.showMetricsAtAllLevels, + showTotal: vis.params.showTotal, + totalFunc: vis.params.totalFunc, + }; + + const visConfig = { + ...tableConfig, + ...tableData, + }; + + const tableVis = buildExpressionFunction<TableVisExpressionFunctionDefinition>( + 'opensearch_dashboards_table', + { + visConfig: JSON.stringify(visConfig), + } + ); + + const ast = buildExpression([opensearchaggs, tableVis]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index c780ef3b5db..814a86f5ac6 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -28,7 +28,8 @@ * under the License. */ -import { SchemaConfig } from '../../visualizations/public'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { IFieldFormat } from 'src/plugins/data/public'; export enum AggTypes { SUM = 'sum', @@ -38,22 +39,46 @@ export enum AggTypes { COUNT = 'count', } -export interface Dimensions { - buckets: SchemaConfig[]; +export interface TableVisConfig extends TableVisParams { + title: string; metrics: SchemaConfig[]; + buckets: SchemaConfig[]; + splitRow?: SchemaConfig[]; + splitColumn?: SchemaConfig[]; } export interface TableVisParams { - type: 'table'; perPage: number | ''; showPartialRows: boolean; showMetricsAtAllLevels: boolean; - sort: { - columnIndex: number | null; - direction: string | null; - }; showTotal: boolean; totalFunc: AggTypes; percentageCol: string; - dimensions: Dimensions; +} + +export interface FormattedColumn { + id: string; + title: string; + formatter: IFieldFormat; + filterable: boolean; + formattedTotal?: string | number; + sumTotal?: number; + total?: number; +} + +export interface ColumnWidth { + colIndex: number; + width: number; +} + +export interface ColumnSort { + colIndex?: number; + direction?: 'asc' | 'desc'; +} + +export interface TableUiState { + sort: ColumnSort; + setSort: (sort: ColumnSort) => void; + width: ColumnWidth[]; + setWidth: (columnWidths: ColumnWidth[]) => void; } diff --git a/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts new file mode 100644 index 00000000000..2c37df1aa3d --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts @@ -0,0 +1,85 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isObject } from 'lodash'; +// @ts-ignore +import { saveAs } from '@elastic/filesaver'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../share/public'; +import { OpenSearchDashboardsDatatable } from '../../../expressions/public'; +import { FormattedColumn } from '../types'; + +const nonAlphaNumRE = /[^a-zA-Z0-9]/; +const allDoubleQuoteRE = /"/g; + +interface CSVDataProps { + filename?: string; + rows: OpenSearchDashboardsDatatable['rows']; + columns: FormattedColumn[]; + uiSettings: CoreStart['uiSettings']; +} + +const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { + const separator = uiSettings.get(CSV_SEPARATOR_SETTING); + const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING); + + function escape(val: any) { + if (!formatted && isObject(val)) val = val.valueOf(); + val = String(val); + if (quoteValues && nonAlphaNumRE.test(val)) { + val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; + } + return val; + } + + let csvRows: string[][] = []; + for (const row of rows) { + const rowArray = []; + for (const col of columns) { + const value = row[col.id]; + const formattedValue = + formatted && col.formatter ? escape(col.formatter.convert(value)) : escape(value); + rowArray.push(formattedValue); + } + csvRows = [...csvRows, rowArray]; + } + + // add the columns to the rows + csvRows.unshift(columns.map((col) => escape(col.title))); + + return csvRows.map((row) => row.join(separator) + '\r\n').join(''); +}; + +export const exportAsCsv = function (formatted: boolean, csvData: CSVDataProps) { + const csv = new Blob([toCsv(formatted, csvData)], { type: 'text/csv;charset=utf-8' }); + const type = formatted ? 'formatted' : 'raw'; + if (csvData.filename) saveAs(csv, `${csvData.filename}-${type}.csv`); + else saveAs(csv, `unsaved-${type}.csv`); +}; diff --git a/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts new file mode 100644 index 00000000000..2ab67e3b0a6 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts @@ -0,0 +1,179 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { chain } from 'lodash'; +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { Table } from '../table_vis_response_handler'; +import { AggTypes, TableVisConfig } from '../types'; +import { getFormatService } from '../services'; +import { FormattedColumn } from '../types'; + +function insert(arr: FormattedColumn[], index: number, col: FormattedColumn) { + const newArray = [...arr]; + newArray.splice(index + 1, 0, col); + return newArray; +} + +/** + * @param columns - the formatted columns that will be displayed + * @param title - the title of the column to add to + * @param rows - the row data for the columns + * @param insertAtIndex - the index to insert the percentage column at + * @returns cols and rows for the table to render now included percentage column(s) + */ +function addPercentageCol( + columns: FormattedColumn[], + title: string, + rows: Table['rows'], + insertAtIndex: number +) { + const { id, sumTotal } = columns[insertAtIndex]; + const newId = `${id}-percents`; + const formatter = getFormatService().deserialize({ id: 'percent' }); + const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { + defaultMessage: '{title} percentages', + values: { title }, + }); + const newCols = insert(columns, insertAtIndex, { + title: i18nTitle, + id: newId, + formatter, + filterable: false, + }); + const newRows = rows.map((row) => ({ + [newId]: (row[id] as number) / (sumTotal as number), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} + +export interface FormattedDataProps { + formattedRows: OpenSearchDashboardsDatatableRow[]; + formattedColumns: FormattedColumn[]; +} + +export const convertToFormattedData = ( + table: Table, + visConfig: TableVisConfig +): FormattedDataProps => { + const { buckets, metrics } = visConfig; + let formattedRows: OpenSearchDashboardsDatatableRow[] = table.rows; + let formattedColumns: FormattedColumn[] = table.columns + .map(function (col, i) { + const isBucket = buckets.find((bucket) => bucket.accessor === i); + const dimension = isBucket || metrics.find((metric) => metric.accessor === i); + + if (!dimension) return undefined; + + const formatter = getFormatService().deserialize(dimension.format); + + const formattedColumn: FormattedColumn = { + id: col.id, + title: col.name, + formatter, + filterable: !!isBucket, + }; + + const isDate = dimension?.format?.id === 'date' || dimension?.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + if (allowsNumericalAggregations || isDate || visConfig.totalFunc === AggTypes.COUNT) { + const sum = table.rows.reduce((prev, curr) => { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + (curr[col.id] as number); + }, 0); + + formattedColumn.sumTotal = sum; + switch (visConfig.totalFunc) { + case AggTypes.SUM: { + if (!isDate) { + formattedColumn.formattedTotal = formatter?.convert(sum); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case AggTypes.AVG: { + if (!isDate) { + const total = sum / table.rows.length; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + } + break; + } + case AggTypes.MIN: { + const total = chain(table.rows).map(col.id).min().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + break; + } + case AggTypes.MAX: { + const total = chain(table.rows).map(col.id).max().value() as number; + formattedColumn.formattedTotal = formatter?.convert(total); + formattedColumn.total = total; + break; + } + case 'count': { + const total = table.rows.length; + formattedColumn.formattedTotal = total; + formattedColumn.total = total; + break; + } + default: + break; + } + } + + return formattedColumn; + }) + .filter((column): column is FormattedColumn => !!column); + + if (visConfig.percentageCol) { + const insertAtIndex = formattedColumns.findIndex( + (col) => col.title === visConfig.percentageCol + ); + + // column to show percentage was removed + if (insertAtIndex < 0) return { formattedRows, formattedColumns }; + + const { cols, rows } = addPercentageCol( + formattedColumns, + visConfig.percentageCol, + table.rows, + insertAtIndex + ); + formattedRows = rows; + formattedColumns = cols; + } + return { formattedRows, formattedColumns }; +}; diff --git a/src/plugins/vis_type_table/public/utils/index.ts b/src/plugins/vis_type_table/public/utils/index.ts new file mode 100644 index 00000000000..1fd0e3f1e0f --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './convert_to_csv_data'; +export * from './convert_to_formatted_data'; +export * from './use_pagination'; diff --git a/src/plugins/vis_type_table/public/utils/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use_pagination.ts new file mode 100644 index 00000000000..45dbed2c0da --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use_pagination.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { TableVisConfig } from '../types'; + +export const usePagination = (visConfig: TableVisConfig, nRow: number) => { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: visConfig.perPage || 10, + }); + const onChangeItemsPerPage = useCallback( + (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), + [setPagination] + ); + const onChangePage = useCallback((pageIndex) => setPagination((p) => ({ ...p, pageIndex })), [ + setPagination, + ]); + + useEffect(() => { + const perPage = visConfig.perPage || 10; + const maxiPageIndex = Math.ceil(nRow / perPage) - 1; + setPagination((p) => ({ + pageIndex: p.pageIndex > maxiPageIndex ? maxiPageIndex : p.pageIndex, + pageSize: perPage, + })); + }, [nRow, visConfig.perPage]); + + return useMemo( + () => ({ + ...pagination, + onChangeItemsPerPage, + onChangePage, + }), + [pagination, onChangeItemsPerPage, onChangePage] + ); +}; diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts deleted file mode 100644 index aa7ffb05110..00000000000 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { CoreSetup, PluginInitializerContext } from 'opensearch-dashboards/public'; -import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; - -import { VisParams, ExprVis } from '../../visualizations/public'; -import { getAngularModule } from './get_inner_angular'; -import { getOpenSearchDashboardsLegacy } from './services'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; - -const innerAngularName = 'opensearch-dashboards/table_vis'; - -export function getTableVisualizationControllerClass( - core: CoreSetup, - context: PluginInitializerContext -) { - return class TableVisualizationController { - private tableVisModule: IModule | undefined; - private injector: auto.IInjectorService | undefined; - el: JQuery<Element>; - vis: ExprVis; - $rootScope: IRootScopeService | null = null; - $scope: (IScope & { [key: string]: any }) | undefined; - $compile: ICompileService | undefined; - - constructor(domeElement: Element, vis: ExprVis) { - this.el = $(domeElement); - this.vis = vis; - } - - getInjector() { - if (!this.injector) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); - this.injector = angular.bootstrap(mountpoint, [innerAngularName]); - this.el.append(mountpoint); - } - - return this.injector; - } - - async initLocalAngular() { - if (!this.tableVisModule) { - const [coreStart] = await core.getStartServices(); - this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); - initTableVisLegacyModule(this.tableVisModule); - } - } - - async render(opensearchResponse: object, visParams: VisParams): Promise<void> { - getOpenSearchDashboardsLegacy().loadFontAwesome(); - await this.initLocalAngular(); - - return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; - } - - // How things get into this $scope? - // To inject variables into this $scope there's the following pipeline of stuff to check: - // - visualize_embeddable => that's what the editor creates to wrap this Angular component - // - build_pipeline => it serialize all the params into an Angular template compiled on the fly - // - table_vis_fn => unserialize the params and prepare them for the final React/Angular bridge - // - visualization_renderer => creates the wrapper component for this controller and passes the params - // - // In case some prop is missing check into the top of the chain if they are available and check - // the list above that it is passing through - this.$scope.vis = this.vis; - this.$scope.visState = { params: visParams, title: visParams.title }; - this.$scope.opensearchResponse = opensearchResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); - updateScope(); - this.el - .find('div') - .append(this.$compile(this.vis.type.visConfig?.template ?? '')(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); - } - }); - } - - destroy() { - if (this.$rootScope) { - this.$rootScope.$destroy(); - this.$rootScope = null; - } - } - }; -} diff --git a/src/plugins/vis_type_timeseries/README.md b/src/plugins/vis_type_timeseries/README.md index 4b4184b6ead..719f772cdea 100644 --- a/src/plugins/vis_type_timeseries/README.md +++ b/src/plugins/vis_type_timeseries/README.md @@ -1 +1 @@ -Contains everything around TSVB (the editor, visualizatin implementations and backends). \ No newline at end of file +Contains everything around TSVB (the editor, visualization implementations and backends). \ No newline at end of file diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index d0941e8c236..5ecb003e56a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -57,7 +57,8 @@ export const SerialDiffAgg = (props) => { const handleNumberChange = createNumberHandler(handleChange); const htmlId = htmlIdGenerator(); - const docLinks = useOpenSearchDashboards().services.docLinks; + const serialDiffDoc = useOpenSearchDashboards().services.docLinks?.links.opensearch.aggregations + .pipeline.serial_diff; return ( <AggRow @@ -111,7 +112,7 @@ export const SerialDiffAgg = (props) => { id="visTypeTimeseries.serialDiff.lagLabel" defaultMessage="Lag" description={`'Lag' refers to the parameter name of the serial diff translation - ${docLinks.services.links.opensearch.aggregations.pipeline.serial_diff}. + ${serialDiffDoc}. This should only be translated if there is a reasaonable word explaining what that parameter does.`} /> } diff --git a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap index 8b813ee06b1..7ba343a02f2 100644 --- a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap +++ b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap @@ -2,8 +2,8 @@ exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"<div class=\\"vgaVis__view leaflet-container leaflet-grab leaflet-touch-drag\\" style=\\"height: 100%; position: relative;\\" tabindex=\\"0\\"><div class=\\"leaflet-pane leaflet-map-pane\\" style=\\"left: 0px; top: 0px;\\"><div class=\\"leaflet-pane leaflet-tile-pane\\"></div><div class=\\"leaflet-pane leaflet-shadow-pane\\"></div><div class=\\"leaflet-pane leaflet-overlay-pane\\"><div class=\\"leaflet-vega-container\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\" style=\\"left: 0px; top: 0px; cursor: default;\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"0\\" height=\\"0\\" viewBox=\\"0 0 0 0\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-rect role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"rect mark container\\"><path d=\\"M0,0h0v0h0Z\\" fill=\\"#0f0\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div></div><div class=\\"leaflet-pane leaflet-marker-pane\\"></div><div class=\\"leaflet-pane leaflet-tooltip-pane\\"></div><div class=\\"leaflet-pane leaflet-popup-pane\\"></div></div><div class=\\"leaflet-control-container\\"><div class=\\"leaflet-top leaflet-left\\"><div class=\\"leaflet-control-zoom leaflet-bar leaflet-control\\"><a class=\\"leaflet-control-zoom-in\\" href=\\"#\\" title=\\"Zoom in\\" role=\\"button\\" aria-label=\\"Zoom in\\">+</a><a class=\\"leaflet-control-zoom-out\\" href=\\"#\\" title=\\"Zoom out\\" role=\\"button\\" aria-label=\\"Zoom out\\">βˆ’</a></div></div><div class=\\"leaflet-top leaflet-right\\"></div><div class=\\"leaflet-bottom leaflet-left\\"></div><div class=\\"leaflet-bottom leaflet-right\\"><div class=\\"leaflet-control-attribution leaflet-control\\"></div></div></div></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; -exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"<div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"512\\" height=\\"512\\" viewBox=\\"0 0 512 512\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h512v512h-512Z\\"></path><g><g class=\\"mark-group role-scope\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,512C18.962962962962962,512,37.925925925925924,512,56.888888888888886,512C75.85185185185185,512,94.81481481481481,512,113.77777777777777,512C132.74074074074073,512,151.7037037037037,512,170.66666666666666,512C189.62962962962962,512,208.59259259259258,512,227.55555555555554,512C246.5185185185185,512,265.48148148148147,512,284.44444444444446,512C303.4074074074074,512,322.3703703703704,512,341.3333333333333,512C360.29629629629625,512,379.25925925925924,512,398.2222222222222,512C417.18518518518516,512,436.1481481481481,512,455.1111111111111,512C474.0740740740741,512,493.037037037037,512,512,512L512,355.2C493.037037037037,324.79999999999995,474.0740740740741,294.4,455.1111111111111,294.4C436.1481481481481,294.4,417.18518518518516,457.6,398.2222222222222,457.6C379.25925925925924,457.6,360.29629629629625,233.60000000000002,341.3333333333333,233.60000000000002C322.3703703703704,233.60000000000002,303.4074074074074,435.2,284.44444444444446,435.2C265.48148148148147,435.2,246.5185185185185,345.6,227.55555555555554,345.6C208.59259259259258,345.6,189.62962962962962,451.2,170.66666666666666,451.2C151.7037037037037,451.2,132.74074074074073,252.8,113.77777777777777,252.8C94.81481481481481,252.8,75.85185185185185,346.1333333333333,56.888888888888886,374.4C37.925925925925924,402.66666666666663,18.962962962962962,412.5333333333333,0,422.4Z\\" fill=\\"#54B399\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,422.4C18.962962962962962,412.5333333333333,37.925925925925924,402.66666666666663,56.888888888888886,374.4C75.85185185185185,346.1333333333333,94.81481481481481,252.8,113.77777777777777,252.8C132.74074074074073,252.8,151.7037037037037,451.2,170.66666666666666,451.2C189.62962962962962,451.2,208.59259259259258,345.6,227.55555555555554,345.6C246.5185185185185,345.6,265.48148148148147,435.2,284.44444444444446,435.2C303.4074074074074,435.2,322.3703703703704,233.60000000000002,341.3333333333333,233.60000000000002C360.29629629629625,233.60000000000002,379.25925925925924,457.6,398.2222222222222,457.6C417.18518518518516,457.6,436.1481481481481,294.4,455.1111111111111,294.4C474.0740740740741,294.4,493.037037037037,324.79999999999995,512,355.2L512,307.2C493.037037037037,275.2,474.0740740740741,243.2,455.1111111111111,243.2C436.1481481481481,243.2,417.18518518518516,371.2,398.2222222222222,371.2C379.25925925925924,371.2,360.29629629629625,22.399999999999977,341.3333333333333,22.399999999999977C322.3703703703704,22.399999999999977,303.4074074074074,278.4,284.44444444444446,278.4C265.48148148148147,278.4,246.5185185185185,204.8,227.55555555555554,192C208.59259259259258,179.20000000000002,189.62962962962962,185.6,170.66666666666666,172.8C151.7037037037037,160.00000000000003,132.74074074074073,83.19999999999999,113.77777777777777,83.19999999999999C94.81481481481481,83.19999999999999,75.85185185185185,83.19999999999999,56.888888888888886,83.19999999999999C37.925925925925924,83.19999999999999,18.962962962962962,164.79999999999998,0,246.39999999999998Z\\" fill=\\"#6092C0\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; +exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"<div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"512\\" height=\\"512\\" viewBox=\\"0 0 512 512\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h512v512h-512Z\\"></path><g><g class=\\"mark-group role-scope\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,512C18.963,512,37.926,512,56.889,512C75.852,512,94.815,512,113.778,512C132.741,512,151.704,512,170.667,512C189.63,512,208.593,512,227.556,512C246.519,512,265.481,512,284.444,512C303.407,512,322.37,512,341.333,512C360.296,512,379.259,512,398.222,512C417.185,512,436.148,512,455.111,512C474.074,512,493.037,512,512,512L512,355.2C493.037,324.8,474.074,294.4,455.111,294.4C436.148,294.4,417.185,457.6,398.222,457.6C379.259,457.6,360.296,233.6,341.333,233.6C322.37,233.6,303.407,435.2,284.444,435.2C265.481,435.2,246.519,345.6,227.556,345.6C208.593,345.6,189.63,451.2,170.667,451.2C151.704,451.2,132.741,252.8,113.778,252.8C94.815,252.8,75.852,346.133,56.889,374.4C37.926,402.667,18.963,412.533,0,422.4Z\\" fill=\\"#54B399\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,422.4C18.963,412.533,37.926,402.667,56.889,374.4C75.852,346.133,94.815,252.8,113.778,252.8C132.741,252.8,151.704,451.2,170.667,451.2C189.63,451.2,208.593,345.6,227.556,345.6C246.519,345.6,265.481,435.2,284.444,435.2C303.407,435.2,322.37,233.6,341.333,233.6C360.296,233.6,379.259,457.6,398.222,457.6C417.185,457.6,436.148,294.4,455.111,294.4C474.074,294.4,493.037,324.8,512,355.2L512,307.2C493.037,275.2,474.074,243.2,455.111,243.2C436.148,243.2,417.185,371.2,398.222,371.2C379.259,371.2,360.296,22.4,341.333,22.4C322.37,22.4,303.407,278.4,284.444,278.4C265.481,278.4,246.519,204.8,227.556,192C208.593,179.2,189.63,185.6,170.667,172.8C151.704,160,132.741,83.2,113.778,83.2C94.815,83.2,75.852,83.2,56.889,83.2C37.926,83.2,18.963,164.8,0,246.4Z\\" fill=\\"#6092C0\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"<ul class=\\"vgaVis__messages\\"><li class=\\"vgaVis__message vgaVis__message--warn\\"><pre class=\\"vgaVis__messageCode\\">\\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable</pre></li></ul><div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"0\\" height=\\"0\\" viewBox=\\"0 0 0 0\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(7,7)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0.5,0.5h0v0h0Z\\" fill=\\"transparent\\" stroke=\\"#ddd\\"></path><g><g class=\\"mark-line role-mark marks\\" role=\\"graphics-object\\" aria-roledescription=\\"line mark container\\"><path aria-label=\\"key: Dec 11, 2017; doc_count: 0\\" role=\\"graphics-symbol\\" aria-roledescription=\\"line mark\\" d=\\"M0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0\\" stroke=\\"#54B399\\" stroke-width=\\"2\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; -exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"<ul class=\\"vgaVis__messages\\"><li class=\\"vgaVis__message vgaVis__message--warn\\"><pre class=\\"vgaVis__messageCode\\">\\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable</pre></li></ul><div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"256\\" height=\\"250\\" viewBox=\\"0 0 256 250\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(7,5)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0.5,0.5h242v238h-242Z\\" fill=\\"transparent\\" stroke=\\"#ddd\\"></path><g><g class=\\"mark-line role-mark marks\\" role=\\"graphics-object\\" aria-roledescription=\\"line mark container\\"><path aria-label=\\"key: Dec 11, 2017; doc_count: 0\\" role=\\"graphics-symbol\\" aria-roledescription=\\"line mark\\" d=\\"M0,238L26.888888888888886,238L53.77777777777777,238L80.66666666666666,21.657999999999994L107.55555555555554,15.850799999999998L134.44444444444446,16.183999999999987L161.33333333333331,231.66920000000002L188.22222222222223,238L215.1111111111111,238L242,238\\" stroke=\\"#54B399\\" stroke-width=\\"2\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"<ul class=\\"vgaVis__messages\\"><li class=\\"vgaVis__message vgaVis__message--warn\\"><pre class=\\"vgaVis__messageCode\\">\\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable</pre></li></ul><div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"256\\" height=\\"250\\" viewBox=\\"0 0 256 250\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(7,5)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0.5,0.5h242v238h-242Z\\" fill=\\"transparent\\" stroke=\\"#ddd\\"></path><g><g class=\\"mark-line role-mark marks\\" role=\\"graphics-object\\" aria-roledescription=\\"line mark container\\"><path aria-label=\\"key: Dec 11, 2017; doc_count: 0\\" role=\\"graphics-symbol\\" aria-roledescription=\\"line mark\\" d=\\"M0,238L26.889,238L53.778,238L80.667,21.658L107.556,15.851L134.444,16.184L161.333,231.669L188.222,238L215.111,238L242,238\\" stroke=\\"#54B399\\" stroke-width=\\"2\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 72921f8213e..5dd326c0c58 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -37,7 +37,7 @@ jest.mock('../services'); jest.mock('../lib/vega', () => ({ vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), + vegaLite: jest.requireActual('vega-lite/src'), })); describe(`VegaParser.parseAsync`, () => { @@ -245,11 +245,15 @@ describe('VegaParser.parseSchema', () => { test( 'should not warn on current vega-lite version', + check('https://vega.github.io/schema/vega-lite/v5.json', true, 0) + ); + test( + 'should not warn on older vega-lite version', check('https://vega.github.io/schema/vega-lite/v4.json', true, 0) ); test( 'should warn on vega-lite version too new to be supported', - check('https://vega.github.io/schema/vega-lite/v5.json', true, 1) + check('https://vega.github.io/schema/vega-lite/v6.json', true, 1) ); }); diff --git a/src/plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson index 98a0787c46c..b4e126bfa3d 100644 --- a/src/plugins/vis_type_vega/public/default.spec.hjson +++ b/src/plugins/vis_type_vega/public/default.spec.hjson @@ -1,31 +1,31 @@ { -/* + /* -Welcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments. + Welcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments. -This example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner. -*/ + This example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner. + */ - $schema: https://vega.github.io/schema/vega-lite/v4.json + $schema: https://vega.github.io/schema/vega-lite/v5.json title: Event counts from all indexes // Define the data source data: { url: { -/* -An object instead of a string for the "url" param is treated as an OpenSearch query. Anything inside this object is not part of the Vega language, but only understood by OpenSearch Dashboards and OpenSearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data. + /* + An object instead of a string for the "url" param is treated as an OpenSearch query. Anything inside this object is not part of the Vega language, but only understood by OpenSearch Dashboards and OpenSearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data. -OpenSearch Dashboards has a special handling for the fields surrounded by "%". They are processed before the the query is sent to OpenSearch. This way the query becomes context aware, and can use the time range and the dashboard filters. -*/ + OpenSearch Dashboards has a special handling for the fields surrounded by "%". They are processed before the the query is sent to OpenSearch. This way the query becomes context aware, and can use the time range and the dashboard filters. + */ // Apply dashboard context filters when set %context%: true // Filter the time picker (upper right corner) with this field %timefield%: @timestamp -/* -See .search() documentation for : https://opensearch.org/docs/latest/clients/javascript/ -*/ + /* + See .search() documentation for : https://opensearch.org/docs/latest/clients/javascript/ + */ // Which index to search index: _all @@ -53,29 +53,29 @@ See .search() documentation for : https://opensearch.org/docs/latest/clients/ja size: 0 } } -/* -OpenSearch will return results in this format: + /* + OpenSearch will return results in this format: -aggregations: { - time_buckets: { - buckets: [ - { - key_as_string: 2015-11-30T22:00:00.000Z - key: 1448920800000 - doc_count: 0 - }, - { - key_as_string: 2015-11-30T23:00:00.000Z - key: 1448924400000 - doc_count: 0 + aggregations: { + time_buckets: { + buckets: [ + { + key_as_string: 2015-11-30T22:00:00.000Z + key: 1448920800000 + doc_count: 0 + }, + { + key_as_string: 2015-11-30T23:00:00.000Z + key: 1448924400000 + doc_count: 0 + } + ... + ] } - ... - ] - } -} + } -For our graph, we only need the list of bucket values. Use the format.property to discard everything else. -*/ + For our graph, we only need the list of bucket values. Use the format.property to discard everything else. + */ format: {property: "aggregations.time_buckets.buckets"} } diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js index 1c3068bae9e..9d4f7983591 100644 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ b/src/plugins/vis_type_vega/public/lib/vega.js @@ -28,8 +28,11 @@ * under the License. */ -import * as vegaLite from 'vega-lite/build-es5/vega-lite'; +// Build vega-lite from source for es5 compatibility +import { compile, version } from 'vega-lite/src'; import * as vega from 'vega/build-es5/vega'; import { expressionInterpreter as vegaExpressionInterpreter } from 'vega-interpreter/build/vega-interpreter.module'; +const vegaLite = { compile, version }; + export { vega, vegaLite, vegaExpressionInterpreter }; diff --git a/src/plugins/vis_type_vega/public/test_utils/default.spec.json b/src/plugins/vis_type_vega/public/test_utils/default.spec.json index 8cf76364711..266f2bd6eea 100644 --- a/src/plugins/vis_type_vega/public/test_utils/default.spec.json +++ b/src/plugins/vis_type_vega/public/test_utils/default.spec.json @@ -1,5 +1,5 @@ { - "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "title": "Event counts from all indexes", "data": { "url": { diff --git a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json index 5394f009b07..5a5e72f5902 100644 --- a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json +++ b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json @@ -1,5 +1,5 @@ { - "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "data": { "format": {"property": "aggregations.time_buckets.buckets"}, "values": { diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx index bf0eb584fc5..e5cd924e6e4 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx @@ -110,26 +110,26 @@ export const InspectorDataGrid = ({ columns, data, dataGridAriaLabel }: Inspecto }, [gridData, pagination]); // Resize - const [columnsWidth, setColumnsWidth] = useState<Record<string, number>>({}); + const [columnWidths, setColumnWidths] = useState<Record<string, number>>({}); const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( ({ columnId, width }) => { - setColumnsWidth({ - ...columnsWidth, + setColumnWidths({ + ...columnWidths, [columnId]: width, }); }, - [columnsWidth] + [columnWidths] ); return ( <EuiDataGrid aria-label={dataGridAriaLabel} columns={columns.map((column) => { - if (columnsWidth[column.id]) { + if (columnWidths[column.id]) { return { ...column, - initialWidth: columnsWidth[column.id], + initialWidth: columnWidths[column.id], }; } return column; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 6f0a961f7a6..16fa0466bd5 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -144,7 +144,8 @@ export class VegaMapView extends VegaBaseView { bindingsContainer: this._$controls.get(0), delayRepaint: mapConfig.delayRepaint, viewConfig: this._vegaViewConfig, - viewOptions: this._vegaViewOptions, + parseConfig: null, + parseOptions: this._vegaViewOptions, onWarning: this.onWarn.bind(this), onError: this.onError.bind(this), }, diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index f809a28bc0b..2d4d648828b 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -53,7 +53,7 @@ jest.mock('./default_spec', () => ({ jest.mock('./lib/vega', () => ({ vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), + vegaLite: jest.requireActual('vega-lite/src'), })); // FLAKY: https://github.com/elastic/kibana/issues/71713 diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index 3fcc50de941..c0c21266f0e 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -44,7 +44,7 @@ const mockedSavedObjects = [ visState: JSON.stringify({ type: 'vega', params: { - spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v4.json" }', + spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v5.json" }', }, }), }, diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index b3b4dc5e09b..5712f358f7d 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -12,16 +12,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"opensearch_dashboards_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"opensearch_dashboards_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":2,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":4,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"opensearch_dashboards_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"opensearch_dashboards_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"opensearch_dashboards_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[]}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 90721f66c4a..5f240f82602 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -128,84 +128,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - describe('handles table function', () => { - it('without splits or buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - split_row: [1, 2], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with splits and buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 0 }, - { ...schemaConfig, accessor: 1 }, - ], - split_row: [2, 4], - bucket: [3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=true', () => { - const params = { - showMetricsAtAllLevels: true, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=false', () => { - const params = { - showMetricsAtAllLevels: false, - showPartialRows: true, - }; - const schemas = { - ...schemasDef, - metric: [ - { ...schemaConfig, accessor: 1 }, - { ...schemaConfig, accessor: 2 }, - { ...schemaConfig, accessor: 4 }, - { ...schemaConfig, accessor: 5 }, - ], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 7a28ae7ac39..1cbb3bc3887 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -278,13 +278,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param)); return `tsvb ${paramsArray.join(' ')}`; }, - table: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.table(schemas, params), - }; - return `opensearch_dashboards_table ${prepareJson('visConfig', visConfig)}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, @@ -309,26 +302,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }; const buildVisConfig: BuildVisConfigFunction = { - table: (schemas, visParams = {}) => { - const visConfig = {} as any; - const metrics = schemas.metric; - const buckets = schemas.bucket || []; - visConfig.dimensions = { - metrics, - buckets, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - - if (visParams.showMetricsAtAllLevels === false && visParams.showPartialRows === true) { - // Handle case where user wants to see partial rows but not metrics at all levels. - // This requires calculating how many metrics will come back in the tabified response, - // and removing all metrics from the dimensions except the last set. - const metricsPerBucket = metrics.length / buckets.length; - visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket); - } - return visConfig; - }, region_map: (schemas) => { const visConfig = {} as any; visConfig.metric = schemas.metric[0]; diff --git a/src/setup_node_env/__snapshots__/node_version_validator.test.js.snap b/src/setup_node_env/__snapshots__/node_version_validator.test.js.snap new file mode 100644 index 00000000000..e4937710779 --- /dev/null +++ b/src/setup_node_env/__snapshots__/node_version_validator.test.js.snap @@ -0,0 +1,755 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Node.js version validation fails if package.json has a 0.x.x engines.node 1`] = `"OpenSearch Dashboards did not report its required version of the Node.js runtime in a valid format. Please revert any changes that might have been made to the package.json file and try again."`; + +exports[`Node.js version validation fails if package.json has a blank engines.node 1`] = `"OpenSearch Dashboards did not report its required version of the Node.js runtime. Please revert any changes that might have been made to the package.json file and try again."`; + +exports[`Node.js version validation fails if package.json has an invalid engines.node 1`] = `"OpenSearch Dashboards did not report its required version of the Node.js runtime in a valid format. Please revert any changes that might have been made to the package.json file and try again."`; + +exports[`Node.js version validation fails if package.json is missing engines 1`] = `"OpenSearch Dashboards did not report its required version of the Node.js runtime. Please revert any changes that might have been made to the package.json file and try again."`; + +exports[`Node.js version validation fails if package.json is missing engines.node 1`] = `"OpenSearch Dashboards did not report its required version of the Node.js runtime. Please revert any changes that might have been made to the package.json file and try again."`; + +exports[`Node.js version validation fails if process.version is not reported 1`] = `"OpenSearch Dashboards cannot start up because the JavaScript runtime did not report its version. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation fails if process.version reports an invalid value 1`] = `"OpenSearch Dashboards cannot start up because the JavaScript runtime did not report its version in a discernible format. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v3.0.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v3.0.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v3.1.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v3.1.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v5.0.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v5.0.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v5.1.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and no operator 4, should not accept v5.1.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v3.0.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v3.0.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v3.1.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v3.1.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v5.0.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v5.0.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v5.1.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the caret operator ^4, should not accept v5.1.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v3.0.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v3.0.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v3.1.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v3.1.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v5.0.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v5.0.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v5.1.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the equals operator =4, should not accept v5.1.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v3.0.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.0. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v3.0.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.1. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v3.1.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.0. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v3.1.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.1. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v4.0.0, the exact version 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.0.0. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v4.0.1, a patch upgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.0.1. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v4.1.0, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.1.0. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than operator >4, should not accept v4.1.1, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.1.1. Please use a Node.js runtime version that is v5.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than-or-equals operator >=4, should not accept v3.0.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.0. Please use a Node.js runtime version that is v4.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than-or-equals operator >=4, should not accept v3.0.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.1. Please use a Node.js runtime version that is v4.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than-or-equals operator >=4, should not accept v3.1.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.0. Please use a Node.js runtime version that is v4.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the greater-than-or-equals operator >=4, should not accept v3.1.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.1. Please use a Node.js runtime version that is v4.0.0 or greater."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v3.0.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v3.0.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v3.1.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v3.1.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v5.0.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v5.0.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.0.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v5.1.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.0. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new major Node.js version using a comparator with a version missing its patch and minor, and the tilde operator ~4, should not accept v5.1.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.1.1. Please use a Node.js runtime version that is v4.0.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.54.0, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.54.1, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.55.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.55.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.56.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.56.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.54.0, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.54.1, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.56.0, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.56.1, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.54.0, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.54.1, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.55.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.55.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.56.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.56.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.54.0, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.54.1, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.55.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.55.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.56.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.56.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v4.54.0, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v4.54.1, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.54.0, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.54.1, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.55.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.55.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.56.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.56.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.54.0, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.54.1, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.55.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.55.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.56.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.56.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.54.0, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.54.1, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.56.0, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.56.1, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.54.0, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.54.1, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.55.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.55.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.56.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.56.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.54.0, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.0. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.54.1, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.1. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.55.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.0. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.55.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.1. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.56.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.0. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.56.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.1. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.54.0, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.0. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.54.1, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.1. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.55.0, the exact version 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.0. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.55.1, a patch upgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.1. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.54.0, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.0. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.54.1, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.1. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.55.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.0. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.55.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.1. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.56.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.0. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.56.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.1. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v4.54.0, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.0. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v4.54.1, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.1. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.54.0, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.54.1, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.55.0, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.55.1, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.56.0, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.56.1, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.54.0, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.54.1, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.56.0, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.56.1, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.54.0, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.54.1, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.55.0, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.55.1, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.56.0, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.0. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation new minor Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.56.1, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.1. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.55.667, a patch upgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.56.665, a minor upgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.56.666, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v4.56.667, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and no operator 4.55.666, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the caret operator ^4.55.666, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.55.667, a patch upgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.56.665, a minor upgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.56.666, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v4.56.667, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the equals operator =4.55.666, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use Node.js v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than operator >4.55.666, should not accept v4.55.666, the exact version 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.666. Please use a Node.js runtime version that is greater than v4.55.666."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the greater-than-or-equals operator >=4.55.666, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use a Node.js runtime version that is v4.55.666 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.56.665, a minor upgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.56.666, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v4.56.667, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a complete version, and the tilde operator ~4.55.666, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use a Node.js runtime version that is v4.55.666 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.56.665, a minor upgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.56.666, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v4.56.667, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and no operator 4.55, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the caret operator ^4.55, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v5.0.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.56.665, a minor upgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.56.666, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v4.56.667, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the equals operator =4.55, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.55.665, a patch downgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.665. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.55.666, the exact version 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.666. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than operator >4.55, should not accept v4.55.667, a patch upgrade 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.55.667. Please use a Node.js runtime version that is v4.56.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the greater-than-or-equals operator >=4.55, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.0 or greater."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.54.665, a major downgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.54.666, a major downgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.54.667, a major downgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.55.665, a major downgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.55.666, a major downgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.55.667, a major downgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.56.665, a major downgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.56.666, a major downgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v3.56.667, a major downgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v3.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.54.665, a minor downgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.54.666, a minor downgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.54.667, a minor downgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.56.665, a minor upgrade with lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.56.666, a minor upgrade with same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v4.56.667, a minor upgrade with higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v4.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.54.665, a major upgrade with lower minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.54.666, a major upgrade with lower minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.54.667, a major upgrade with lower minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.54.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.55.665, a major upgrade with same minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.55.666, a major upgrade with same minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.55.667, a major upgrade with same minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.55.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.56.665, a major upgrade with higher minor and lower patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.665. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.56.666, a major upgrade with higher minor and same patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.666. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; + +exports[`Node.js version validation non-new Node.js version using a comparator with a version missing its patch, and the tilde operator ~4.55, should not accept v5.56.667, a major upgrade with higher minor and higher patch 1`] = `"OpenSearch Dashboards cannot start up using the Node.js runtime v5.56.667. Please use a Node.js runtime version that is v4.55.0 or greater and lower than v4.56.0."`; diff --git a/src/setup_node_env/node_version_validator.js b/src/setup_node_env/node_version_validator.js index 504d0970ecf..ba418244b08 100644 --- a/src/setup_node_env/node_version_validator.js +++ b/src/setup_node_env/node_version_validator.js @@ -28,27 +28,217 @@ * under the License. */ +/* Note: + * This file uses ES5 in order to provide meaningful output even if an old Node.js runtime is used. + * + * The exit codes facilitating the testing are: + * 21: JS runtime version doesn't satisfy the range specific in `engines.node` of `package.json`. + * 22: `package.json` is missing `engines.node` or it is not a string. + * 23: `package.json` has a `engines.node` that is not a valid semver range. + * 24: `package.json` has a `engines.node` that has a major of zero. + * 25: JS runtime did not report `process.version`. + * 26: JS runtime returned a `process.version` that didn't match semver. + */ + var pkg = require('../../package.json'); -// Note: This is written in ES5 so we can run this before anything else -// and gives support for older NodeJS versions -var currentVersion = (process && process.version) || null; -var rawRequiredVersion = (pkg && pkg.engines && pkg.engines.node) || null; -var requiredVersion = rawRequiredVersion ? 'v' + rawRequiredVersion : rawRequiredVersion; -var currentVersionMajorMinorPatch = currentVersion.match(/^v(\d+)\.(\d+)\.(\d+)/); -var requiredVersionMajorMinorPatch = requiredVersion.match(/^v(\d+)\.(\d+)\.(\d+)/); -var isVersionValid = - currentVersionMajorMinorPatch[1] === requiredVersionMajorMinorPatch[1] && - currentVersionMajorMinorPatch[2] === requiredVersionMajorMinorPatch[2] && - parseInt(currentVersionMajorMinorPatch[3], 10) >= parseInt(requiredVersionMajorMinorPatch[3], 10); - -// Validates current the NodeJS version compatibility when OpenSearch Dashboards starts. -if (!isVersionValid) { - var errorMessage = - `OpenSearch Dashboards was built with ${requiredVersion} and does not support the current Node.js version ${currentVersion}. ` + - `Please use Node.js ${requiredVersion} or a higher patch version.`; - - // Actions to apply when validation fails: error report + exit. - console.error(errorMessage); - process.exit(1); +var versionMatcher = /^\s*v?\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\D.*)?$/; +var rangeMatcher = /^\s*(>=?|\^|~|=)?\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[^\d.].*)?$/; + +var ERR_MISSING_REQUIREMENTS = + 'OpenSearch Dashboards did not report its required version of the Node.js runtime'; +var ERR_BAD_REQUIREMENTS = ERR_MISSING_REQUIREMENTS + ' in a valid format'; +var _BAD_REQUIREMENTS_SUFFIX = + '. Please revert any changes that might have been made to the package.json file and try again.'; +ERR_MISSING_REQUIREMENTS += _BAD_REQUIREMENTS_SUFFIX; +ERR_BAD_REQUIREMENTS += _BAD_REQUIREMENTS_SUFFIX; + +var pkgEngineNodeVersion = pkg && pkg.engines && pkg.engines.node; +if (!pkgEngineNodeVersion || typeof pkgEngineNodeVersion !== 'string') { + console.error(ERR_MISSING_REQUIREMENTS); + process.exit(22); +} + +/* Basic semver parsing: This is a very limited subset of what semver supports where only a single comparator, composed + * of an operator and a version, is supported. + * [https://github.com/npm/node-semver/blob/cb1ca1d5480a6c07c12ac31ba5f2071ed530c4ed/README.md#ranges] + * + * The supported operators are: + * > Greater than + * >= Greater than or equal to + * = Equal + * ~ Tilde ranges: Allows patch changes if a minor version is specified but if only a major version is specified, + * it allows minor changes. + * ^ Caret ranges: Allows patch and minor updates when major is non-zero (and we will never have that). + * + * Note: If no operator is specified, equality is assumed. + */ +var requiredParts = pkgEngineNodeVersion.match(rangeMatcher); +if (requiredParts === null) { + console.error(ERR_BAD_REQUIREMENTS); + process.exit(23); +} + +var comparatorVersion = { + major: requiredParts[2], + minor: requiredParts[3], + patch: requiredParts[4], +}; +var comparatorOperator = requiredParts[1] || '='; +var rangeBottom = { + major: parseInt(comparatorVersion.major, 10) || 0, + minor: parseInt(comparatorVersion.minor, 10) || 0, + patch: parseInt(comparatorVersion.patch, 10) || 0, + inclusive: comparatorOperator.indexOf('>') === -1 || comparatorOperator.indexOf('=') > -1, +}; +var rangeTop = undefined; + +if (!rangeBottom.major) { + console.error(ERR_BAD_REQUIREMENTS); + process.exit(24); +} + +if (comparatorOperator === '>') { + if (!comparatorVersion.minor) { + // >3 is >=4.0.0 + rangeBottom.major += 1; + rangeBottom.inclusive = true; + } else if (!comparatorVersion.patch) { + // >3.1 is >=3.2.0 + rangeBottom.minor += 1; + rangeBottom.inclusive = true; + } +} + +// =3 is ~3.0.0 and =3.1 is ~3.1.0 +if (comparatorOperator === '=' && (!comparatorVersion.minor || !comparatorVersion.patch)) { + comparatorOperator = '~'; +} + +// =3.1.4 +if (comparatorOperator === '=') { + rangeTop = { + major: rangeBottom.major, + minor: rangeBottom.minor, + patch: rangeBottom.patch, + inclusive: true, + }; +} else if (comparatorOperator === '~') { + if (comparatorVersion.minor) { + // ~3.1.4 and ~3.1 are <3.2.0 + rangeTop = { + major: rangeBottom.major, + minor: rangeBottom.minor + 1, + patch: 0, + }; + } else { + // ~3 is <4.0.0 + rangeTop = { + major: rangeBottom.major + 1, + minor: 0, + patch: 0, + }; + } +} else if (comparatorOperator === '^') { + // ^3.1.4 is <4.0.0 + rangeTop = { + major: rangeBottom.major + 1, + minor: 0, + patch: 0, + }; +} + +function getVersionCompatibilityMessage() { + var versionBottom = 'v' + rangeBottom.major + '.' + rangeBottom.minor + '.' + rangeBottom.patch; + if (comparatorOperator === '=') { + return 'Please use Node.js ' + versionBottom + '.'; + } + + var message = 'Please use a Node.js runtime version that is greater than ' + versionBottom; + if (rangeBottom.inclusive) { + message = 'Please use a Node.js runtime version that is ' + versionBottom + ' or greater'; + } + + if (!rangeTop) return message + '.'; + + var versionTop = 'v' + rangeTop.major + '.' + rangeTop.minor + '.' + rangeTop.patch; + // The only operator with a truthy `rangeTop.inclusive` is the `=` which was handled above + return message + ' and lower than ' + versionTop + '.'; +} + +/* Compares the 2 versions and returns + * 1: A > B + * 0: A === B + * -1: A < B + */ +function versionCompare(versionA, versionB) { + // 4.x.x > 3.1.4 + if (versionA.major > versionB.major) return 1; + else if (versionA.major === versionB.major) { + // 3.2.x > 3.1.4 + if (versionA.minor > versionB.minor) return 1; + else if (versionA.minor === versionB.minor) { + // 3.1.5 >= 3.1.4 + if (versionA.patch > versionB.patch) return 1; + // 3.1.4 = 3.1.4 + else if (versionA.patch === versionB.patch) return 0; + } + } + + return -1; +} + +var currentVersion = process && process.version; +if (!currentVersion) { + console.error( + 'OpenSearch Dashboards cannot start up because the JavaScript runtime did not report its version. ' + + getVersionCompatibilityMessage() + ); + process.exit(25); +} + +var currentParts = currentVersion.match(versionMatcher); +if (currentParts === null) { + console.error( + 'OpenSearch Dashboards cannot start up because the JavaScript runtime did not report its version in a ' + + 'discernible format. ' + + getVersionCompatibilityMessage() + ); + process.exit(26); +} + +var version = { + major: parseInt(currentParts[1], 10) || 0, + minor: parseInt(currentParts[2], 10) || 0, + patch: parseInt(currentParts[3], 10) || 0, +}; + +var satisfiesBottom = false; + +// Check if version is greater than rangeBottom or if equal, that `rangeBottom` is inclusive +var versionComparedToRangeBottom = versionCompare(version, rangeBottom); +if (versionComparedToRangeBottom === 1) satisfiesBottom = true; +else if (rangeBottom.inclusive && versionComparedToRangeBottom === 0) satisfiesBottom = true; + +var satisfiesTop = false; + +if (satisfiesBottom && rangeTop) { + var versionComparedToRangeTop = versionCompare(version, rangeTop); + if (versionComparedToRangeTop === -1) satisfiesTop = true; + else if (rangeTop.inclusive && versionComparedToRangeTop === 0) satisfiesTop = true; +} + +// Fail if the Node.js version doesn't satisfy the requirements of OpenSearch Dashboards +if (!satisfiesBottom || (rangeTop && !satisfiesTop)) { + console.error( + 'OpenSearch Dashboards cannot start up using the Node.js runtime v' + + version.major + + '.' + + version.minor + + '.' + + version.patch + + '. ' + + getVersionCompatibilityMessage() + ); + process.exit(21); } diff --git a/src/setup_node_env/node_version_validator.test.js b/src/setup_node_env/node_version_validator.test.js index cb3639154c6..5d3a70cd7b9 100644 --- a/src/setup_node_env/node_version_validator.test.js +++ b/src/setup_node_env/node_version_validator.test.js @@ -28,83 +28,279 @@ * under the License. */ -var exec = require('child_process').exec; -var pkg = require('../../package.json'); +const semver = require('semver'); +const util = require('util'); +const exec = util.promisify(require('child_process').exec); -var REQUIRED_NODE_JS_VERSION = 'v' + pkg.engines.node; +const allPossibleCombinations = [ + 'a patch downgrade', + 'the exact version', + 'a patch upgrade', -describe('NodeVersionValidator', function () { - it('should run the script WITHOUT error when the version is the same', function (done) { - testValidateNodeVersion(done, REQUIRED_NODE_JS_VERSION); - }); + 'a minor upgrade with lower patch', + 'a minor upgrade with same patch', + 'a minor upgrade with higher patch', - it('should run the script WITHOUT error when only the patch version is higher', function (done) { - testValidateNodeVersion(done, requiredNodeVersionWithDiff(0, 0, +1)); - }); + 'a minor downgrade with lower patch', + 'a minor downgrade with same patch', + 'a minor downgrade with higher patch', + + 'a major upgrade with same minor and lower patch', + 'a major upgrade with same minor and same patch', + 'a major upgrade with same minor and higher patch', + + 'a major downgrade with same minor and lower patch', + 'a major downgrade with same minor and same patch', + 'a major downgrade with same minor and higher patch', + + 'a major upgrade with lower minor and lower patch', + 'a major upgrade with lower minor and same patch', + 'a major upgrade with lower minor and higher patch', + + 'a major downgrade with lower minor and lower patch', + 'a major downgrade with lower minor and same patch', + 'a major downgrade with lower minor and higher patch', + + 'a major upgrade with higher minor and lower patch', + 'a major upgrade with higher minor and same patch', + 'a major upgrade with higher minor and higher patch', + + 'a major downgrade with higher minor and lower patch', + 'a major downgrade with higher minor and same patch', + 'a major downgrade with higher minor and higher patch', +]; + +const allPossibleOperators = [ + { title: 'no operator', operator: '' }, + { title: 'the equals operator', operator: '=' }, + { title: 'the greater-than operator', operator: '>' }, + { title: 'the greater-than-or-equals operator', operator: '>=' }, + { title: 'the caret operator', operator: '^' }, + { title: 'the tilde operator', operator: '~' }, +]; + +// Regex pattern to parse test titles which gets used to create new versions +const testTitleMatcher = /(?:(major|minor|patch)\s+(upgrade|downgrade)|(higher|lower)\s+(major|minor|patch))/g; +const testTitleChanges = { + upgrade: 1, + downgrade: -1, + higher: 1, + lower: -1, +}; + +const titleChangesCache = new Map(); + +// Parse the test title and generate a new version +const getUpdatedVersion = (testedVersion, title) => { + let matches; + let majorChange = 0; + let minorChange = 0; + let patchChange = 0; + + if (titleChangesCache.has(title)) { + const cache = titleChangesCache.get(title); + majorChange = cache.majorChange; + minorChange = cache.minorChange; + patchChange = cache.patchChange; + } else { + while ((matches = testTitleMatcher.exec(title)) !== null) { + const change = testTitleChanges[matches[2]] || testTitleChanges[matches[3]]; + if (matches[1] === 'major' || matches[4] === 'major') majorChange = change; + else if (matches[1] === 'minor' || matches[4] === 'minor') minorChange = change; + else if (matches[1] === 'patch' || matches[4] === 'patch') patchChange = change; + } + + titleChangesCache.set(title, { majorChange, minorChange, patchChange }); + } + + return testedVersion.change(majorChange, minorChange, patchChange); +}; + +const getMockedPackageJson = (requiredRange) => { + switch (requiredRange) { + case 'BLANK': + return `{ engines: { node: '' }}`; + case 'NO_NODE': + return `{ engines: { }}`; + case 'NO_ENGINES': + return `{}`; + default: + return `{ engines: { node: '${requiredRange}' }}`; + } +}; + +const checkNodeVersionValidation = async (nodeVersion, requiredRange) => { + const mockedProcessVersion = `Object.defineProperty(process, 'version', { value: '${ + nodeVersion ? 'v' + nodeVersion : '' + }', writable: true });`; + // Node + Windows doesn't like line-feeds + const mockedRequire = + `const Module = require('module');` + + `const req = Module.prototype.require;` + + `Module.prototype.require = name =>` + + `name === '../../package.json' ?` + + getMockedPackageJson(requiredRange) + + `: req(name);`; + try { + /* ToDo: Implement changes so these tests can contribute to the code coverage report + * `jest --coverage` is not capable of handling child processes: https://github.com/facebook/jest/issues/5274 + * + * The workaround is to do + * 1. `nyc node scripts/jest` + * This cleans the cached `.nyc_output` and correctly instruments subprocesses + * 2. `nyc report --reporter=lcov --reporter=text-summary --report-dir target/opensearch-dashboards-coverage/jest` + * This generates the lcov report and shows a summary + * 3. Clean up `.nyc_output` + * + * Note: src/dev/jest/config.js should be checked for any other config that we would like to pass to nyc + */ + await exec( + `node -e "${mockedProcessVersion}${mockedRequire}require('./node_version_validator.js')"`, + { cwd: __dirname } + ); + } catch (ex) { + if (ex.stderr?.indexOf('OpenSearch Dashboards') > -1) + return { + error: ex.code, + stderr: ex.stderr, + }; + + throw ex; + } + + return { + error: 0, + }; +}; - it('should run the script WITH error if the patch version is lower', function (done) { - var lowerPatchversion = requiredNodeVersionWithDiff(0, 0, -1); - testValidateNodeVersion( - done, - lowerPatchversion, - REQUIRED_NODE_JS_VERSION !== lowerPatchversion +// Create an appropriate test based on how semver feels about the version and range +const defineTest = (title, testedVersion, requiredRange) => { + const version = getUpdatedVersion(testedVersion, title); + + return semver.satisfies(version, requiredRange) + ? it(`${requiredRange}, should accept v${version}, ${title}`, async () => { + const { error, stderr } = await checkNodeVersionValidation(version, requiredRange); + expect(error).toEqual(0); // The exit code indicating an acceptable version + expect(stderr).toStrictEqual(undefined); + }) + : it(`${requiredRange}, should not accept v${version}, ${title}`, async () => { + const { error, stderr } = await checkNodeVersionValidation(version, requiredRange); + expect(error).toEqual(21); // The exit code indicating an unacceptable version + expect(stderr.trim()).toMatchSnapshot(); + }); +}; + +const describeSuites = (testsToRun, testedVersion, comparatorVersion) => { + describe.each(allPossibleOperators)('$title', ({ operator }) => { + testsToRun.forEach((title) => + defineTest(title, testedVersion, `${operator}${comparatorVersion}`) ); }); +}; + +const parseVersion = (version) => { + const parts = version?.match(/^\s*v?\s*(\d+)\.(\d+)\.(\d+)(\D.*)?$/) || null; + if (parts === null) throw `<${version}> is not a parsable version.`; + const numericParts = { + major: parseInt(parts[1], 10) || 0, + minor: parseInt(parts[2], 10) || 0, + patch: parseInt(parts[3], 10) || 0, + }; + + // prettier-ignore + const change = (majorChange = 0, minorChange = 0, patchChange = 0) => + (numericParts.major + majorChange) + + '.' + + (numericParts.minor + minorChange) + + '.' + + (numericParts.patch + patchChange); + + return { + get exact() { + return change(); + }, + change, + }; +}; + +describe('Node.js version validation', () => { + describe('non-new Node.js version using a comparator with a complete version, and', () => { + const comparatorVersion = '4.55.666'; + const testedVersion = parseVersion('4.55.666'); + const testsToRun = allPossibleCombinations; - it('should run the script WITH error if the major version is higher', function (done) { - testValidateNodeVersion(done, requiredNodeVersionWithDiff(+1, 0, 0), true); + describeSuites(testsToRun, testedVersion, comparatorVersion); }); - it('should run the script WITH error if the major version is lower', function (done) { - var lowerMajorVersion = requiredNodeVersionWithDiff(-1, 0, 0); - testValidateNodeVersion( - done, - lowerMajorVersion, - REQUIRED_NODE_JS_VERSION !== lowerMajorVersion - ); + describe('non-new Node.js version using a comparator with a version missing its patch, and', () => { + const comparatorVersion = '4.55'; + const testedVersion = parseVersion('4.55.666'); + const testsToRun = allPossibleCombinations; + + describeSuites(testsToRun, testedVersion, comparatorVersion); }); - it('should run the script WITH error if the minor version is higher', function (done) { - testValidateNodeVersion(done, requiredNodeVersionWithDiff(0, +1, 0), true); + describe('new minor Node.js version using a comparator with a version missing its patch, and', () => { + const comparatorVersion = '4.55'; + const testedVersion = parseVersion('4.55.0'); + // The tested version has zero for its patch value so exclude tests that lower it + const testsToRun = allPossibleCombinations.filter( + (title) => !/(patch downgrade|lower patch)/.test(title) + ); + + describeSuites(testsToRun, testedVersion, comparatorVersion); }); - it('should run the script WITH error if the minor version is lower', function (done) { - var lowerMinorVersion = requiredNodeVersionWithDiff(0, -1, 0); - testValidateNodeVersion( - done, - lowerMinorVersion, - REQUIRED_NODE_JS_VERSION !== lowerMinorVersion + describe('new major Node.js version using a comparator with a version missing its patch and minor, and', () => { + const comparatorVersion = '4'; + const testedVersion = parseVersion('4.0.0'); + // The tested version has zeros for patch and minor values so exclude tests that lower them + const testsToRun = allPossibleCombinations.filter( + (title) => !/((patch|minor) downgrade|lower (patch|minor))/.test(title) ); + + describeSuites(testsToRun, testedVersion, comparatorVersion); }); -}); -function requiredNodeVersionWithDiff(majorDiff, minorDiff, patchDiff) { - var matches = REQUIRED_NODE_JS_VERSION.match(/^v(\d+)\.(\d+)\.(\d+)/); - var major = Math.max(parseInt(matches[1], 10) + majorDiff, 0); - var minor = Math.max(parseInt(matches[2], 10) + minorDiff, 0); - var patch = Math.max(parseInt(matches[3], 10) + patchDiff, 0); + it('fails if package.json is missing engines', async () => { + const { error, stderr } = await checkNodeVersionValidation('4.55.666', 'NO_ENGINES'); + expect(error).toEqual(22); // The exit code indicating missing engines.node range + expect(stderr.trim()).toMatchSnapshot(); + }); - return `v${major}.${minor}.${patch}`; -} + it('fails if package.json is missing engines.node', async () => { + const { error, stderr } = await checkNodeVersionValidation('4.55.666', 'NO_NODE'); + expect(error).toEqual(22); // The exit code indicating missing engines.node range + expect(stderr.trim()).toMatchSnapshot(); + }); + + it('fails if package.json has a blank engines.node', async () => { + const { error, stderr } = await checkNodeVersionValidation('4.55.666', 'BLANK'); + expect(error).toEqual(22); // The exit code indicating missing engines.node range + expect(stderr.trim()).toMatchSnapshot(); + }); -function testValidateNodeVersion(done, versionToTest, expectError = false) { - var processVersionOverwrite = `Object.defineProperty(process, 'version', { value: '${versionToTest}', writable: true });`; - var command = `node -e "${processVersionOverwrite}require('./node_version_validator.js')"`; + it('fails if package.json has an invalid engines.node', async () => { + const { error, stderr } = await checkNodeVersionValidation('4.55.666', 'INVALID'); + expect(error).toEqual(23); // The exit code indicating an unacceptable engines.node range + expect(stderr.trim()).toMatchSnapshot(); + }); - exec(command, { cwd: __dirname }, function (error, _stdout, stderr) { - expect(stderr).toBeDefined(); - if (expectError) { - expect(error.code).toBe(1); + it('fails if package.json has a 0.x.x engines.node', async () => { + const { error, stderr } = await checkNodeVersionValidation('4.55.666', '0.11.222'); + expect(error).toEqual(24); // The exit code indicating an unacceptable engines.node range + expect(stderr.trim()).toMatchSnapshot(); + }); - var speficicErrorMessage = - `OpenSearch Dashboards was built with ${REQUIRED_NODE_JS_VERSION} and does not support the current Node.js version ${versionToTest}. ` + - `Please use Node.js ${REQUIRED_NODE_JS_VERSION} or a higher patch version.\n`; + it('fails if process.version is not reported', async () => { + const { error, stderr } = await checkNodeVersionValidation('', '4.55.666'); + expect(error).toEqual(25); // The exit code indicating an unacceptable process.version + expect(stderr.trim()).toMatchSnapshot(); + }); - expect(stderr).toStrictEqual(speficicErrorMessage); - } else { - expect(error).toBeNull(); - expect(stderr).toHaveLength(0); - } - done(); + it('fails if process.version reports an invalid value', async () => { + const { error, stderr } = await checkNodeVersionValidation('INVALID', '4.55.666'); + expect(error).toEqual(26); // The exit code indicating an unacceptable process.version + expect(stderr.trim()).toMatchSnapshot(); }); -} +}); diff --git a/src/setup_node_env/root/force.test.js b/src/setup_node_env/root/force.test.js index f48f3255e4e..eca20f9547b 100644 --- a/src/setup_node_env/root/force.test.js +++ b/src/setup_node_env/root/force.test.js @@ -28,7 +28,7 @@ * under the License. */ -var forceRoot = require('./force'); +const forceRoot = require('./force'); describe('forceRoot', function () { it('with flag', function () { @@ -40,7 +40,7 @@ describe('forceRoot', function () { }); test('remove argument', function () { - var args = ['--allow-root', 'foo']; + const args = ['--allow-root', 'foo']; forceRoot(args); expect(args.includes('--allow-root')).toBeFalsy(); }); diff --git a/src/setup_node_env/root/is_root.test.js b/src/setup_node_env/root/is_root.test.js index 81dfbe3616c..e7f8e0670b7 100644 --- a/src/setup_node_env/root/is_root.test.js +++ b/src/setup_node_env/root/is_root.test.js @@ -28,7 +28,7 @@ * under the License. */ -var isRoot = require('./is_root'); +const isRoot = require('./is_root'); describe('isRoot', function () { test('0 is root', function () { diff --git a/tasks/config/run.js b/tasks/config/run.js index 7e3afceef55..474c6c535f1 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -38,107 +38,82 @@ const OPENSEARCH_DASHBOARDS_INSTALL_DIR = module.exports = function () { const NODE = 'node'; const YARN = 'yarn'; - const scriptWithGithubChecks = ({ title, options, cmd, args }) => - process.env.CHECKS_REPORTER_ACTIVE === 'true' - ? { - options, - cmd: YARN, - args: ['run', 'github-checks-reporter', title, cmd, ...args], - } - : { options, cmd, args }; - const gruntTaskWithGithubChecks = (title, task) => - scriptWithGithubChecks({ - title, - cmd: YARN, - args: ['run', 'grunt', task], - }); return { // used by the test and jenkins:unit tasks // runs the eslint script to check for linting errors - eslint: scriptWithGithubChecks({ - title: 'eslint', + eslint: { cmd: NODE, args: ['scripts/eslint', '--no-cache'], - }), + }, - stylelint: scriptWithGithubChecks({ - title: 'stylelint', + stylelint: { cmd: NODE, args: ['scripts/stylelint'], - }), + }, // used by the test tasks // runs the check_file_casing script to ensure filenames use correct casing - checkFileCasing: scriptWithGithubChecks({ - title: 'Check file casing', + checkFileCasing: { cmd: NODE, args: [ 'scripts/check_file_casing', '--quiet', // only log errors, not warnings ], - }), + }, // used by the test tasks // runs the check_lockfile_symlinks script to ensure manifests with non-dev dependencies have adjacent lockfile symlinks - checkLockfileSymlinks: scriptWithGithubChecks({ - title: 'Check lockfile symlinks', + checkLockfileSymlinks: { cmd: NODE, args: [ 'scripts/check_lockfile_symlinks', '--quiet', // only log errors, not warnings ], - }), + }, // used by the test tasks // runs the check_published_api_changes script to ensure API changes are explictily accepted - checkDocApiChanges: scriptWithGithubChecks({ - title: 'Check core API changes', + checkDocApiChanges: { cmd: NODE, args: ['scripts/check_published_api_changes'], - }), + }, // used by the test and jenkins:unit tasks // runs the typecheck script to check for Typescript type errors - typeCheck: scriptWithGithubChecks({ - title: 'Type check', + typeCheck: { cmd: NODE, args: ['scripts/type_check'], - }), + }, // used by the test and jenkins:unit tasks // ensures that all typescript files belong to a typescript project - checkTsProjects: scriptWithGithubChecks({ - title: 'TypeScript - all files belong to a TypeScript project', + checkTsProjects: { cmd: NODE, args: ['scripts/check_ts_projects'], - }), + }, // used by the test and jenkins:unit tasks // runs the i18n_check script to check i18n engine usage - i18nCheck: scriptWithGithubChecks({ - title: 'Internationalization check', + i18nCheck: { cmd: NODE, args: ['scripts/i18n_check', '--ignore-missing'], - }), + }, - telemetryCheck: scriptWithGithubChecks({ - title: 'Telemetry Schema check', + telemetryCheck: { cmd: NODE, args: ['scripts/telemetry_check'], - }), + }, // used by the test:quick task // runs all node.js/server mocha tests - mocha: scriptWithGithubChecks({ - title: 'Mocha tests', + mocha: { cmd: NODE, args: ['scripts/mocha'], - }), + }, // used by the test:mochaCoverage task - mochaCoverage: scriptWithGithubChecks({ - title: 'Mocha tests coverage', + mochaCoverage: { cmd: YARN, args: [ 'nyc', @@ -147,25 +122,22 @@ module.exports = function () { NODE, 'scripts/mocha', ], - }), + }, - verifyNotice: scriptWithGithubChecks({ - title: 'Verify NOTICE.txt', + verifyNotice: { options: { wait: true, }, cmd: NODE, args: ['scripts/notice', '--validate'], - }), + }, - test_hardening: scriptWithGithubChecks({ - title: 'Node.js hardening tests', + test_hardening: { cmd: NODE, args: ['scripts/test_hardening.js'], - }), + }, - apiIntegrationTests: scriptWithGithubChecks({ - title: 'API integration tests', + apiIntegrationTests: { cmd: NODE, args: [ 'scripts/functional_tests', @@ -174,10 +146,9 @@ module.exports = function () { '--bail', '--debug', ], - }), + }, - serverIntegrationTests: scriptWithGithubChecks({ - title: 'Server integration tests', + serverIntegrationTests: { cmd: NODE, args: [ 'scripts/functional_tests', @@ -196,10 +167,9 @@ module.exports = function () { '--opensearch-dashboards-install-dir', OPENSEARCH_DASHBOARDS_INSTALL_DIR, ], - }), + }, - interpreterFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Interpreter functional tests', + interpreterFunctionalTestsRelease: { cmd: NODE, args: [ 'scripts/functional_tests', @@ -210,10 +180,9 @@ module.exports = function () { '--opensearch-dashboards-install-dir', OPENSEARCH_DASHBOARDS_INSTALL_DIR, ], - }), + }, - pluginFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Plugin functional tests', + pluginFunctionalTestsRelease: { cmd: NODE, args: [ 'scripts/functional_tests', @@ -222,10 +191,9 @@ module.exports = function () { '--bail', '--debug', ], - }), + }, - exampleFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Example functional tests', + exampleFunctionalTestsRelease: { cmd: NODE, args: [ 'scripts/functional_tests', @@ -234,10 +202,9 @@ module.exports = function () { '--bail', '--debug', ], - }), + }, - functionalTests: scriptWithGithubChecks({ - title: 'Functional tests', + functionalTests: { cmd: NODE, args: [ 'scripts/functional_tests', @@ -246,20 +213,25 @@ module.exports = function () { '--bail', '--debug', ], - }), + }, - licenses: scriptWithGithubChecks({ - title: 'Check licenses', + licenses: { cmd: NODE, args: ['scripts/check_licenses', '--dev'], - }), + }, - test_jest: gruntTaskWithGithubChecks('Jest tests', 'test:jest'), - test_jest_integration: gruntTaskWithGithubChecks( - 'Jest integration tests', - 'test:jest_integration' - ), - test_projects: gruntTaskWithGithubChecks('Project tests', 'test:projects'), + test_jest: { + cmd: YARN, + args: ['run', 'grunt', 'test:jest'], + }, + test_jest_integration: { + cmd: YARN, + args: ['run', 'grunt', 'test:jest_integration'], + }, + test_projects: { + cmd: YARN, + args: ['run', 'grunt', 'test:projects'], + }, ...getFunctionalTestGroupRunConfigs({ opensearchDashboardsInstallDir: OPENSEARCH_DASHBOARDS_INSTALL_DIR, diff --git a/test/common/config.js b/test/common/config.js index 0ad9dc042c5..5db5748087a 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -78,7 +78,6 @@ export default function () { `--opensearchDashboards.branding.mark.defaultUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg`, `--opensearchDashboards.branding.mark.darkModeUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg`, `--opensearchDashboards.branding.applicationTitle=OpenSearch`, - `--vis_builder.enabled=true`, ], }, services, diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index f5cd93c3528..302197bb455 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('default request response should include `"timed_out" : false`', async () => { - const expectedResponseContains = '"timed_out" : false,'; + const expectedResponseContains = '"timed_out": false,'; await PageObjects.console.clickPlay(); await retry.try(async () => { const actualResponse = await PageObjects.console.getResponse(); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index fd05f5b134e..deb2ae39924 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -98,10 +98,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.seriesElementCount(0); }); - it('data tables are filtered', async () => { - await dashboardExpect.dataTableRowCount(0); - }); - it('goal and guages are filtered', async () => { await dashboardExpect.goalAndGuageLabelsExist(['0', '0%']); }); @@ -159,10 +155,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.seriesElementCount(0); }); - it('data tables are filtered', async () => { - await dashboardExpect.dataTableRowCount(0); - }); - it('goal and guages are filtered', async () => { await dashboardExpect.goalAndGuageLabelsExist(['0', '0%']); }); @@ -212,10 +204,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.seriesElementCount(3); }); - it('data tables', async () => { - await dashboardExpect.dataTableRowCount(10); - }); - it('goal and guages', async () => { await dashboardExpect.goalAndGuageLabelsExist(['39.958%', '7,544']); }); diff --git a/test/functional/apps/dashboard/dashboard_grid.js b/test/functional/apps/dashboard/dashboard_grid.js index 3e68f33f204..0da0c58573a 100644 --- a/test/functional/apps/dashboard/dashboard_grid.js +++ b/test/functional/apps/dashboard/dashboard_grid.js @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }) { describe('move panel', () => { // Specific test after https://github.com/elastic/kibana/issues/14764 fix it('Can move panel from bottom to top row', async () => { - const lastVisTitle = 'Rendering Test: datatable'; + const lastVisTitle = 'Rendering Test: pie'; const panelTitleBeforeMove = await dashboardPanelActions.getPanelHeading(lastVisTitle); const position1 = await panelTitleBeforeMove.getPosition(); await browser.dragAndDrop( diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index b11955a1e24..5cacc85cb0e 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -67,7 +67,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.markdownWithValuesExists(["I'm a markdown!"]); await dashboardExpect.vegaTextsExist(['5,000']); await dashboardExpect.goalAndGuageLabelsExist(['62.925%', '55.625%', '11.915 GB']); - await dashboardExpect.dataTableRowCount(5); await dashboardExpect.tagCloudWithValuesFound(['CN', 'IN', 'US', 'BR', 'ID']); // TODO add test for 'region map viz' // TODO add test for 'tsvb gauge' viz @@ -90,7 +89,6 @@ export default function ({ getService, getPageObjects }) { const expectNoDataRenders = async () => { await pieChart.expectPieSliceCount(0); await dashboardExpect.seriesElementCount(0); - await dashboardExpect.dataTableRowCount(0); await dashboardExpect.savedSearchRowCount(0); await dashboardExpect.inputControlItemCount(5); await dashboardExpect.metricValuesExist(['0']); @@ -146,7 +144,7 @@ export default function ({ getService, getPageObjects }) { visNames.push(await dashboardAddPanel.addVisualization('Filter Bytes Test: vega')); await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardExpect.visualizationsArePresent(visNames); - expect(visNames.length).to.be.equal(27); + expect(visNames.length).to.be.equal(26); await PageObjects.dashboard.waitForRenderComplete(); }); @@ -157,7 +155,7 @@ export default function ({ getService, getPageObjects }) { await dashboardAddPanel.closeAddPanel(); await PageObjects.header.waitUntilLoadingHasFinished(); await dashboardExpect.visualizationsArePresent(visAndSearchNames); - expect(visAndSearchNames.length).to.be.equal(28); + expect(visAndSearchNames.length).to.be.equal(27); await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.dashboard.saveDashboard('embeddable rendering test', { diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts index 0f7d8172063..798adeb99bf 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -74,14 +74,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await settings.controlChangeSave(); }); - it('applied on dashboard', async () => { - await common.navigateToApp('dashboard'); - await dashboard.loadSavedDashboard('dashboard with everything'); - await dashboard.waitForRenderComplete(); - const fieldLink = await visChart.getFieldLinkInVisTable(`${fieldName}: Descending`, 1); - await clickFieldAndCheckUrl(fieldLink); - }); - it('applied on discover', async () => { await common.navigateToApp('discover'); await timePicker.setAbsoluteRange( diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index cbd1169e7a3..9ce2a57436e 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -509,10 +509,10 @@ export default function ({ getService, getPageObjects }) { it('should filter by scripted field value in Discover', async function () { await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); - await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list'); + await log.debug('filter by "Sep 18, 2015 @ 7:52" in the expanded scripted field list'); await PageObjects.discover.clickFieldListPlusFilter( scriptedPainlessFieldName2, - '1442531297065' + '1442562775953' ); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js deleted file mode 100644 index fde30412bdc..00000000000 --- a/test/functional/apps/visualize/_data_table.js +++ /dev/null @@ -1,485 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const log = getService('log'); - const inspector = getService('inspector'); - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['visualize', 'timePicker', 'visEditor', 'visChart']); - - describe('data table', function indexPatternCreation() { - const vizName1 = 'Visualization DataTable'; - - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = Split rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should allow applying changed params', async () => { - await PageObjects.visEditor.setInterval('1', { type: 'numeric', append: true }); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('20001'); - const isApplyButtonEnabled = await PageObjects.visEditor.isApplyEnabled(); - expect(isApplyButtonEnabled).to.be(true); - }); - - it('should allow reseting changed params', async () => { - await PageObjects.visEditor.clickReset(); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('2000'); - }); - - it('should be able to save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); - }); - - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); - }); - - it('should show correct data', function () { - const expectedChartData = [ - ['0B', '2,088'], - ['1.953KB', '2,748'], - ['3.906KB', '2,707'], - ['5.859KB', '2,876'], - ['7.813KB', '2,863'], - ['9.766KB', '147'], - ['11.719KB', '148'], - ['13.672KB', '129'], - ['15.625KB', '161'], - ['17.578KB', '137'], - ]; - - return retry.try(async function () { - await inspector.open(); - await inspector.expectTableData(expectedChartData); - await inspector.close(); - }); - }); - - it('should show percentage columns', async () => { - async function expectValidTableData() { - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - 'β‰₯ 0B and < 1,000B', - '1,351 64.703%', - 'β‰₯ 1,000B and < 1.953KB', - '737 35.297%', - ]); - } - - // load a plain table - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Range'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.clickGo(); - await PageObjects.visEditor.clickOptionsTab(); - await PageObjects.visEditor.setSelectByOptionText( - 'datatableVisualizationPercentageCol', - 'Count' - ); - await PageObjects.visEditor.clickGo(); - - await expectValidTableData(); - - // check that it works after a save and reload - const SAVE_NAME = 'viz w/ percents'; - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(SAVE_NAME); - - await PageObjects.visualize.loadSavedVisualization(SAVE_NAME); - await PageObjects.visChart.waitForVisualization(); - - await expectValidTableData(); - - // check that it works after selecting a column that's deleted - await PageObjects.visEditor.clickDataTab(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average', 'metrics'); - await PageObjects.visEditor.selectField('bytes', 'metrics'); - await PageObjects.visEditor.removeDimension(1); - await PageObjects.visEditor.clickGo(); - await PageObjects.visEditor.clickOptionsTab(); - - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - 'β‰₯ 0B and < 1,000B', - '344.094B', - 'β‰₯ 1,000B and < 1.953KB', - '1.697KB', - ]); - }); - - it('should show correct data when using average pipeline aggregation', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets'); - await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); - }); - - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); - - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); - - it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - - it('should correctly filter for pinned filters', async () => { - await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - - it('should show correct data for a data table with top hits', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickMetricEditor(); - await PageObjects.visEditor.selectAggregation('Top Hit', 'metrics'); - await PageObjects.visEditor.selectField('agent.raw', 'metrics'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data); - expect(data.length).to.be.greaterThan(0); - }); - - it('should show correct data for a data table with range agg', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Range'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - 'β‰₯ 0B and < 1,000B', - '1,351', - 'β‰₯ 1,000B and < 1.953KB', - '737', - ]); - }); - - describe('otherBucket', () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.clickGo(); - - await PageObjects.visEditor.toggleOtherBucket(); - await PageObjects.visEditor.toggleMissingBucket(); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data', async () => { - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109'], - ['css', '2,159'], - ['Other', '2,736'], - ]); - }); - - it('should apply correct filter', async () => { - await PageObjects.visChart.filterOnTableCell(1, 3); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['png', '1,373'], - ['gif', '918'], - ['Other', '445'], - ]); - }); - }); - - describe('metricsOnAllLevels', () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.dest'); - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data without showMetricsAtAllLevels', async () => { - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', 'CN', '1,718'], - ['jpg', 'IN', '1,511'], - ['jpg', 'US', '770'], - ['jpg', 'ID', '314'], - ['jpg', 'PK', '244'], - ['css', 'CN', '422'], - ['css', 'IN', '346'], - ['css', 'US', '189'], - ['css', 'ID', '68'], - ['css', 'BR', '58'], - ]); - }); - - it('should show correct data without showMetricsAtAllLevels even if showPartialRows is selected', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showPartialRows', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', 'CN', '1,718'], - ['jpg', 'IN', '1,511'], - ['jpg', 'US', '770'], - ['jpg', 'ID', '314'], - ['jpg', 'PK', '244'], - ['css', 'CN', '422'], - ['css', 'IN', '346'], - ['css', 'US', '189'], - ['css', 'ID', '68'], - ['css', 'BR', '58'], - ]); - }); - - it('should show metrics on each level', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109', 'CN', '1,718'], - ['jpg', '9,109', 'IN', '1,511'], - ['jpg', '9,109', 'US', '770'], - ['jpg', '9,109', 'ID', '314'], - ['jpg', '9,109', 'PK', '244'], - ['css', '2,159', 'CN', '422'], - ['css', '2,159', 'IN', '346'], - ['css', '2,159', 'US', '189'], - ['css', '2,159', 'ID', '68'], - ['css', '2,159', 'BR', '58'], - ]); - }); - - it('should show metrics other than count on each level', async () => { - await PageObjects.visEditor.clickDataTab(); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average', 'metrics'); - await PageObjects.visEditor.selectField('bytes', 'metrics'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['jpg', '9,109', '5.469KB', 'CN', '1,718', '5.477KB'], - ['jpg', '9,109', '5.469KB', 'IN', '1,511', '5.456KB'], - ['jpg', '9,109', '5.469KB', 'US', '770', '5.371KB'], - ['jpg', '9,109', '5.469KB', 'ID', '314', '5.424KB'], - ['jpg', '9,109', '5.469KB', 'PK', '244', '5.41KB'], - ['css', '2,159', '5.566KB', 'CN', '422', '5.712KB'], - ['css', '2,159', '5.566KB', 'IN', '346', '5.754KB'], - ['css', '2,159', '5.566KB', 'US', '189', '5.333KB'], - ['css', '2,159', '5.566KB', 'ID', '68', '4.82KB'], - ['css', '2,159', '5.566KB', 'BR', '58', '5.915KB'], - ]); - }); - }); - - describe('split tables', () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split table'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.setSize(2); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.dest'); - await PageObjects.visEditor.setSize(3, 3); - await PageObjects.visEditor.toggleOpenEditor(3, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Terms'); - await PageObjects.visEditor.selectField('geo.src'); - await PageObjects.visEditor.setSize(3, 4); - await PageObjects.visEditor.toggleOpenEditor(4, 'false'); - await PageObjects.visEditor.clickGo(); - }); - - it('should have a splitted table', async () => { - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - [ - ['CN', 'CN', '330'], - ['CN', 'IN', '274'], - ['CN', 'US', '140'], - ['IN', 'CN', '286'], - ['IN', 'IN', '281'], - ['IN', 'US', '133'], - ['US', 'CN', '135'], - ['US', 'IN', '134'], - ['US', 'US', '52'], - ], - [ - ['CN', 'CN', '90'], - ['CN', 'IN', '84'], - ['CN', 'US', '27'], - ['IN', 'CN', '69'], - ['IN', 'IN', '58'], - ['IN', 'US', '34'], - ['US', 'IN', '36'], - ['US', 'CN', '29'], - ['US', 'US', '13'], - ], - ]); - }); - - it('should show metrics for split bucket when using showMetricsAtAllLevels', async () => { - await PageObjects.visEditor.clickOptionsTab(); - await testSubjects.setCheckbox('showMetricsAtAllLevels', 'check'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - [ - ['CN', '1,718', 'CN', '330'], - ['CN', '1,718', 'IN', '274'], - ['CN', '1,718', 'US', '140'], - ['IN', '1,511', 'CN', '286'], - ['IN', '1,511', 'IN', '281'], - ['IN', '1,511', 'US', '133'], - ['US', '770', 'CN', '135'], - ['US', '770', 'IN', '134'], - ['US', '770', 'US', '52'], - ], - [ - ['CN', '422', 'CN', '90'], - ['CN', '422', 'IN', '84'], - ['CN', '422', 'US', '27'], - ['IN', '346', 'CN', '69'], - ['IN', '346', 'IN', '58'], - ['IN', '346', 'US', '34'], - ['US', '189', 'IN', '36'], - ['US', '189', 'CN', '29'], - ['US', '189', 'US', '13'], - ], - ]); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js deleted file mode 100644 index 37ad4c2a9eb..00000000000 --- a/test/functional/apps/visualize/_data_table_nontimeindex.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const log = getService('log'); - const inspector = getService('inspector'); - const retry = getService('retry'); - const filterBar = getService('filterBar'); - const renderable = getService('renderable'); - const PageObjects = getPageObjects(['visualize', 'visEditor', 'header', 'visChart']); - - describe('data table with index without time filter', function indexPatternCreation() { - const vizName1 = 'Visualization DataTable without time filter'; - - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - log.debug('Bucket = Split Rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should allow applying changed params', async () => { - await PageObjects.visEditor.setInterval('1', { type: 'numeric', append: true }); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('20001'); - const isApplyButtonEnabled = await PageObjects.visEditor.isApplyEnabled(); - expect(isApplyButtonEnabled).to.be(true); - }); - - it('should allow reseting changed params', async () => { - await PageObjects.visEditor.clickReset(); - const interval = await PageObjects.visEditor.getNumericInterval(); - expect(interval).to.be('2000'); - }); - - it('should be able to save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); - }); - - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); - }); - - it('should show correct data', function () { - const expectedChartData = [ - ['0B', '2,088'], - ['1.953KB', '2,748'], - ['3.906KB', '2,707'], - ['5.859KB', '2,876'], - ['7.813KB', '2,863'], - ['9.766KB', '147'], - ['11.719KB', '148'], - ['13.672KB', '129'], - ['15.625KB', '161'], - ['17.578KB', '137'], - ]; - - return retry.try(async function () { - await inspector.open(); - await inspector.expectTableData(expectedChartData); - await inspector.close(); - }); - }); - - it('should show correct data when using average pipeline aggregation', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Metric', 'metrics'); - await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visEditor.selectAggregation('Terms', 'metrics', 'buckets'); - await PageObjects.visEditor.selectField('geo.src', 'metrics', 'buckets'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); - }); - - describe('data table with date histogram', async () => { - before(async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Day'); - await PageObjects.visEditor.clickGo(); - }); - - it('should show correct data', async () => { - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); - - it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - - it('should correctly filter for pinned filters', async () => { - await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts deleted file mode 100644 index 3661a4847bd..00000000000 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@osd/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const filterBar = getService('filterBar'); - const renderable = getService('renderable'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'header', - 'dashboard', - 'timePicker', - 'visEditor', - 'visChart', - ]); - - describe('data table with index without time filter filters', function indexPatternCreation() { - const vizName1 = 'Visualization DataTable w/o time filter'; - - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - log.debug('clickNewSearch'); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - log.debug('Bucket = Split Rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = bytes'); - await PageObjects.visEditor.selectField('bytes'); - log.debug('Interval = 2000'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - }); - - it('should be able to save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); - - await PageObjects.visualize.loadSavedVisualization(vizName1); - await PageObjects.visChart.waitForVisualization(); - }); - - it('timefilter should be disabled', async () => { - const isOff = await PageObjects.timePicker.isOff(); - expect(isOff).to.be(true); - }); - - // test to cover bug #54548 - add this visualization to a dashboard and filter - it('should add to dashboard and allow filtering', async function () { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await dashboardAddPanel.addVisualization(vizName1); - - // hover and click on cell to filter - await PageObjects.visChart.filterOnTableCell('1', '2'); - - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.be(1); - - await filterBar.removeAllFilters(); - }); - }); -} diff --git a/test/functional/apps/visualize/_embedding_chart.js b/test/functional/apps/visualize/_embedding_chart.js deleted file mode 100644 index c47319303dc..00000000000 --- a/test/functional/apps/visualize/_embedding_chart.js +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const filterBar = getService('filterBar'); - const log = getService('log'); - const renderable = getService('renderable'); - const embedding = getService('embedding'); - const PageObjects = getPageObjects([ - 'visualize', - 'visEditor', - 'visChart', - 'header', - 'timePicker', - ]); - - describe('embedding', () => { - describe('a data table', () => { - before(async function () { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.toggleOpenEditor(2, 'false'); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Histogram'); - await PageObjects.visEditor.selectField('bytes'); - await PageObjects.visEditor.setInterval('2000', { type: 'numeric', aggNth: 3 }); - await PageObjects.visEditor.clickGo(); - }); - - it('should allow opening table vis in embedded mode', async () => { - await embedding.openInEmbeddedMode(); - await renderable.waitForRender(); - - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20 00:00', - '0B', - '5', - '2015-09-20 00:00', - '1.953KB', - '5', - '2015-09-20 00:00', - '3.906KB', - '9', - '2015-09-20 00:00', - '5.859KB', - '4', - '2015-09-20 00:00', - '7.813KB', - '14', - '2015-09-20 03:00', - '0B', - '32', - '2015-09-20 03:00', - '1.953KB', - '33', - '2015-09-20 03:00', - '3.906KB', - '45', - '2015-09-20 03:00', - '5.859KB', - '31', - '2015-09-20 03:00', - '7.813KB', - '48', - ]); - }); - - it('should allow to filter in embedded mode', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-21', '2015-09-23'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-21 00:00', - '0B', - '7', - '2015-09-21 00:00', - '1.953KB', - '9', - '2015-09-21 00:00', - '3.906KB', - '9', - '2015-09-21 00:00', - '5.859KB', - '6', - '2015-09-21 00:00', - '7.813KB', - '10', - '2015-09-21 00:00', - '11.719KB', - '1', - '2015-09-21 03:00', - '0B', - '28', - '2015-09-21 03:00', - '1.953KB', - '39', - '2015-09-21 03:00', - '3.906KB', - '36', - '2015-09-21 03:00', - '5.859KB', - '43', - ]); - }); - - it('should allow to change timerange from the visualization in embedded mode', async () => { - await PageObjects.visChart.filterOnTableCell(1, 7); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '03:00', - '0B', - '1', - '03:00', - '1.953KB', - '1', - '03:00', - '3.906KB', - '1', - '03:00', - '5.859KB', - '2', - '03:10', - '0B', - '1', - '03:10', - '5.859KB', - '1', - '03:10', - '7.813KB', - '1', - '03:15', - '0B', - '1', - '03:15', - '1.953KB', - '1', - '03:20', - '1.953KB', - '1', - ]); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_histogram_request_start.js b/test/functional/apps/visualize/_histogram_request_start.js deleted file mode 100644 index f797b83af73..00000000000 --- a/test/functional/apps/visualize/_histogram_request_start.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@osd/expect'; - -export default function ({ getService, getPageObjects }) { - const log = getService('log'); - const retry = getService('retry'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'visEditor', - 'visChart', - 'timePicker', - ]); - - describe('histogram agg onSearchRequestStart', function () { - before(async function () { - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickDataTable'); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = Split Rows'); - await PageObjects.visEditor.clickBucket('Split rows'); - log.debug('Aggregation = Histogram'); - await PageObjects.visEditor.selectAggregation('Histogram'); - log.debug('Field = machine.ram'); - await PageObjects.visEditor.selectField('machine.ram'); - }); - - describe('interval parameter uses autoBounds', function () { - it('should use provided value when number of generated buckets is less than histogram:maxBars', async function () { - const providedInterval = 2400000000; - log.debug(`Interval = ${providedInterval}`); - await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - await retry.try(async () => { - const data = await PageObjects.visChart.getTableVisData(); - const dataArray = data.replace(/,/g, '').split('\n'); - expect(dataArray.length).to.eql(20); - const bucketStart = parseInt(dataArray[0], 10); - const bucketEnd = parseInt(dataArray[2], 10); - const actualInterval = bucketEnd - bucketStart; - expect(actualInterval).to.eql(providedInterval); - }); - }); - - it('should scale value to round number when number of generated buckets is greater than histogram:maxBars', async function () { - const providedInterval = 100; - log.debug(`Interval = ${providedInterval}`); - await PageObjects.visEditor.setInterval(providedInterval, { type: 'numeric' }); - await PageObjects.visEditor.clickGo(); - await PageObjects.common.sleep(1000); //fix this - await retry.try(async () => { - const data = await PageObjects.visChart.getTableVisData(); - const dataArray = data.replace(/,/g, '').split('\n'); - expect(dataArray.length).to.eql(20); - const bucketStart = parseInt(dataArray[0], 10); - const bucketEnd = parseInt(dataArray[2], 10); - const actualInterval = bucketEnd - bucketStart; - expect(actualInterval).to.eql(1200000000); - }); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts deleted file mode 100644 index 72c2a4322a2..00000000000 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@osd/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const browser = getService('browser'); - const filterBar = getService('filterBar'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects([ - 'common', - 'discover', - 'visualize', - 'header', - 'timePicker', - 'visChart', - ]); - - describe('saved search visualizations from visualize app', function describeIndexTests() { - describe('linked saved searched', () => { - const savedSearchName = 'vis_saved_search'; - let discoverSavedSearchUrlPath: string; - - before(async () => { - await PageObjects.common.navigateToApp('discover'); - await filterBar.addFilter('extension.raw', 'is', 'jpg'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.discover.saveSearch(savedSearchName); - discoverSavedSearchUrlPath = (await browser.getCurrentUrl()).split('?')[0]; - }); - - it('should create a visualization from a saved search', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickSavedSearch(savedSearchName); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await retry.waitFor('wait for count to equal 9,109', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '9,109'; - }); - }); - - it('should have a valid link to the saved search from the visualization', async () => { - await testSubjects.click('showUnlinkSavedSearchPopover'); - await testSubjects.click('viewSavedSearch'); - await PageObjects.header.waitUntilLoadingHasFinished(); - - await retry.waitFor('wait discover load its breadcrumbs', async () => { - const discoverBreadcrumb = await PageObjects.discover.getCurrentQueryName(); - return discoverBreadcrumb === savedSearchName; - }); - - const discoverURLPath = (await browser.getCurrentUrl()).split('?')[0]; - expect(discoverURLPath).to.equal(discoverSavedSearchUrlPath); - - // go back to visualize - await browser.goBack(); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - it('should respect the time filter when linked to a saved search', async () => { - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 19, 2015 @ 06:31:44.000', - 'Sep 21, 2015 @ 10:00:00.000' - ); - await retry.waitFor('wait for count to equal 3,950', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '3,950'; - }); - }); - - it('should allow adding filters while having a linked saved search', async () => { - await filterBar.addFilter('bytes', 'is between', '100', '3000'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('wait for count to equal 707', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '707'; - }); - }); - - it('should allow unlinking from a linked search', async () => { - await PageObjects.visualize.clickUnlinkSavedSearch(); - await retry.waitFor('wait for count to equal 707', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '707'; - }); - // The filter on the saved search should now be in the editor - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); - - // Disabling this filter should now result in different values, since - // the visualization should not be linked anymore with the saved search. - await filterBar.toggleFilterEnabled('extension.raw'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('wait for count to equal 1,293', async () => { - const unfilteredData = await PageObjects.visChart.getTableVisData(); - return unfilteredData.trim() === '1,293'; - }); - }); - - it('should not break when saving after unlinking', async () => { - await PageObjects.visualize.saveVisualizationExpectSuccess('Unlinked before saved'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('wait for count to equal 1,293', async () => { - const data = await PageObjects.visChart.getTableVisData(); - return data.trim() === '1,293'; - }); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index b6563a1c28c..fb7e721db7d 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -57,12 +57,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags('ciGroup9'); loadTestFile(require.resolve('./_custom_branding')); - loadTestFile(require.resolve('./_embedding_chart')); loadTestFile(require.resolve('./_chart_types')); loadTestFile(require.resolve('./_area_chart')); - loadTestFile(require.resolve('./_data_table')); - loadTestFile(require.resolve('./_data_table_nontimeindex')); - loadTestFile(require.resolve('./_data_table_notimeindex_filters')); }); describe('', function () { @@ -73,7 +69,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_gauge_chart')); loadTestFile(require.resolve('./_heatmap_chart')); loadTestFile(require.resolve('./input_control_vis')); - loadTestFile(require.resolve('./_histogram_request_start')); loadTestFile(require.resolve('./_metric_chart')); }); @@ -86,7 +81,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_markdown_vis')); loadTestFile(require.resolve('./_shared_item')); loadTestFile(require.resolve('./_lab_mode')); - loadTestFile(require.resolve('./_linked_saved_searches')); loadTestFile(require.resolve('./_visualize_listing')); if (isOss) { loadTestFile(require.resolve('./_tile_map')); diff --git a/test/functional/config.js b/test/functional/config.js index d6025096872..9485225dc8c 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -93,7 +93,7 @@ export default async function ({ readConfigFile }) { hash: '/', }, visBuilder: { - pathname: '/app/visBuilder', + pathname: '/app/vis-builder', hash: '/', }, dashboard: { diff --git a/test/functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards/data.json.gz b/test/functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards/data.json.gz index 9944453bff1..1ca01589588 100644 Binary files a/test/functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards/data.json.gz and b/test/functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards/data.json.gz differ diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 40e17779069..58f75555b15 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -425,7 +425,6 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide { name: PIE_CHART_VIS_NAME, description: 'PieChart' }, { name: 'Visualization☺ VerticalBarChart', description: 'VerticalBarChart' }, { name: AREA_CHART_VIS_NAME, description: 'AreaChart' }, - { name: 'Visualizationβ˜ΊζΌ’ε­— DataTable', description: 'DataTable' }, { name: LINE_CHART_VIS_NAME, description: 'LineChart' }, { name: 'Visualization TileMap', description: 'TileMap' }, { name: 'Visualization MetricChart', description: 'MetricChart' }, diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 02d9e60ca06..e13d8eed608 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -36,7 +36,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr const find = getService('find'); const log = getService('log'); const retry = getService('retry'); - const table = getService('table'); const defaultFindTimeout = config.get('timeouts.find'); const { common } = getPageObjects(['common']); @@ -294,18 +293,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr }); } - public async filterOnTableCell(column: string, row: string) { - await retry.try(async () => { - const tableVis = await testSubjects.find('tableVis'); - const cell = await tableVis.findByCssSelector( - `tbody tr:nth-child(${row}) td:nth-child(${column})` - ); - await cell.moveMouseTo(); - const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell); - await filterBtn.click(); - }); - } - public async getMarkdownText() { const markdownContainer = await testSubjects.find('markdownBody'); return markdownContainer.getVisibleText(); @@ -317,65 +304,6 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr return element.getVisibleText(); } - public async getFieldLinkInVisTable(fieldName: string, rowIndex: number = 1) { - const tableVis = await testSubjects.find('tableVis'); - const $ = await tableVis.parseDomContent(); - const headers = $('span[ng-bind="::col.title"]') - .toArray() - .map((header: any) => $(header).text()); - const fieldColumnIndex = headers.indexOf(fieldName); - return await find.byCssSelector( - `[data-test-subj="paginated-table-body"] tr:nth-of-type(${rowIndex}) td:nth-of-type(${ - fieldColumnIndex + 1 - }) a` - ); - } - - /** - * If you are writing new tests, you should rather look into getTableVisContent method instead. - * @deprecated Use getTableVisContent instead. - */ - public async getTableVisData() { - return await testSubjects.getVisibleText('paginated-table-body'); - } - - /** - * This function is the newer function to retrieve data from within a table visualization. - * It uses a better return format, than the old getTableVisData, by properly splitting - * cell values into arrays. Please use this function for newer tests. - */ - public async getTableVisContent({ stripEmptyRows = true } = {}) { - return await retry.try(async () => { - const container = await testSubjects.find('tableVis'); - const allTables = await testSubjects.findAllDescendant('paginated-table-body', container); - - if (allTables.length === 0) { - return []; - } - - const allData = await Promise.all( - allTables.map(async (t) => { - let data = await table.getDataFromElement(t); - if (stripEmptyRows) { - data = data.filter( - (row) => row.length > 0 && row.some((cell) => cell.trim().length > 0) - ); - } - return data; - }) - ); - - if (allTables.length === 1) { - // If there was only one table we return only the data for that table - // This prevents an unnecessary array around that single table, which - // is the case we have in most tests. - return allData[0]; - } - - return allData; - }); - } - public async getMetric() { const elements = await find.allByCssSelector( '[data-test-subj="visualizationLoader"] .mtrVis__container' diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 4f019c1a4a0..b60d50b449c 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -99,10 +99,6 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await this.clickVisType('area'); } - public async clickDataTable() { - await this.clickVisType('table'); - } - public async clickLineChart() { await this.clickVisType('line'); } diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index a676730f4c1..ab6eaed8a9b 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -241,17 +241,6 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi }); } - async dataTableRowCount(expectedCount: number) { - log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`); - await retry.try(async () => { - const dataTableRows = await find.allByCssSelector( - '[data-test-subj="paginated-table-body"] [data-cell-content]', - findTimeout - ); - expect(dataTableRows.length).to.be(expectedCount); - }); - } - async seriesElementCount(expectedCount: number) { log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`); await retry.try(async () => { diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 391967de7bc..c09d6399516 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -57,7 +57,6 @@ import { ManagementMenuProvider } from './management'; import { QueryBarProvider } from './query_bar'; import { RemoteProvider } from './remote'; import { RenderableProvider } from './renderable'; -import { TableProvider } from './table'; import { ToastsProvider } from './toasts'; import { DataGridProvider } from './data_grid'; import { @@ -93,7 +92,6 @@ export const services = { dataGrid: DataGridProvider, embedding: EmbeddingProvider, renderable: RenderableProvider, - table: TableProvider, browser: BrowserProvider, pieChart: PieChartProvider, inspector: InspectorProvider, diff --git a/test/functional/services/table.ts b/test/functional/services/table.ts deleted file mode 100644 index 34578df40e8..00000000000 --- a/test/functional/services/table.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from './lib/web_element_wrapper'; - -export function TableProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - - class Table { - /** - * Finds table and returns data in the nested array format - * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] - * @param dataTestSubj data-test-subj selector - */ - - public async getDataFromTestSubj(dataTestSubj: string): Promise<string[][]> { - const table = await testSubjects.find(dataTestSubj); - return await this.getDataFromElement(table); - } - - /** - * Converts the table data into nested array - * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] - * @param element table - */ - public async getDataFromElement(element: WebElementWrapper): Promise<string[][]> { - const $ = await element.parseDomContent(); - return $('tr') - .toArray() - .map((row) => - $(row) - .find('td') - .toArray() - .map((cell) => - $(cell) - .text() - .replace(/ /g, '') - .trim() - ) - ); - } - } - - return new Table(); -} diff --git a/yarn.lock b/yarn.lock index 7c7726328e1..98bb598f924 100644 --- a/yarn.lock +++ b/yarn.lock @@ -345,6 +345,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== +"@babel/helper-plugin-utils@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" @@ -495,6 +500,14 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-json-strings" "^7.8.3" +"@babel/plugin-proposal-logical-assignment-operators@^7.16.5": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" + integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" @@ -1309,18 +1322,6 @@ resolved "https://registry.yarnpkg.com/@elastic/filesaver/-/filesaver-1.1.2.tgz#1998ffb3cd89c9da4ec12a7793bfcae10e30c77a" integrity sha512-YZbSufYFBhAj+S2cJgiKALoxIJevqXN2MSr6Yqr42rJdaPuM31cj6pUDwflkql1oDjupqD9la+MfxPFjXI1JFQ== -"@elastic/github-checks-reporter@0.0.20b3": - version "0.0.20-b3" - resolved "https://registry.yarnpkg.com/@elastic/github-checks-reporter/-/github-checks-reporter-0.0.20-b3.tgz#025ac0e152cda03d947faec190c244fbbe59bdfc" - integrity sha512-OmhbddqNkFZMYVQxMqpqLj7NJhqphN+wQb68IeiewxvWXq8NEPaBpaZ9f+nUbixmMY2Q/XA0JgtuE4EhMGIljg== - dependencies: - "@octokit/app" "^2.2.2" - "@octokit/plugin-retry" "^2.2.0" - "@octokit/request" "^2.4.2" - "@octokit/rest" "^16.23.2" - async-retry "^1.2.3" - strip-ansi "^5.2.0" - "@elastic/good@^9.0.1-kibana3": version "9.0.1-kibana3" resolved "https://registry.yarnpkg.com/@elastic/good/-/good-9.0.1-kibana3.tgz#a70c2b30cbb4f44d1cf4a464562e0680322eac9b" @@ -1331,9 +1332,9 @@ "@hapi/validate" "1.x.x" "@elastic/makelogs@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@elastic/makelogs/-/makelogs-6.1.0.tgz#1ea61a01b4680c0e904c5b8be3d325f427030e16" - integrity sha512-iFhSpgOcmv7q65AiTzny2fkhddVdG/6yoX7pUsvwJo9Cc0B6ySVOlwweAWntkwQC3Y1IQpm9gpQUe2GwsyRUkQ== + version "6.1.1" + resolved "https://registry.yarnpkg.com/@elastic/makelogs/-/makelogs-6.1.1.tgz#5bc173b16acdfd7844fd85c97824ddcffd67cfb3" + integrity sha512-cmfXFQITwyT4SV+Ryerg/vVbGQ9E2BhYKQ9flG85Ba3blGVmOjkgv7TYQam6xAIvGXFGBBrcyqEwmuw7xZ5ZNQ== dependencies: async "^1.4.2" commander "^5.0.0" @@ -1342,7 +1343,6 @@ moment "^2.10.6" progress "^1.1.8" through2 "^2.0.0" - update-notifier "^0.5.0" "@elastic/node-crypto@1.1.1": version "1.1.1" @@ -1645,9 +1645,9 @@ "@hapi/hoek" "9.x.x" "@hapi/statehood@^7.0.3": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.3.tgz#655166f3768344ed3c3b50375a303cdeca8040d9" - integrity sha512-pYB+pyCHkf2Amh67QAXz7e/DN9jcMplIL7Z6N8h0K+ZTy0b404JKPEYkbWHSnDtxLjJB/OtgElxocr2fMH4G7w== + version "7.0.4" + resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.4.tgz#6acb9d0817b5c657089356f7d9fd60af0bce4f41" + integrity sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw== dependencies: "@hapi/boom" "9.x.x" "@hapi/bounce" "2.x.x" @@ -2338,178 +2338,12 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@octokit/app@^2.2.2": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@octokit/app/-/app-2.2.5.tgz#2cdd1eed763a822ed5cd0ffcf38fbdd0d40acbf9" - integrity sha512-WIvIVzZItDWSvnkleA6e5wmNBqH4dfzFZsB5GV0QWiMNAOT7TjecVcB6Uz6GhQvfuV4rmjY3/al3akWNNuPLmg== - dependencies: - "@octokit/request" "^3.0.0" - "@types/lru-cache" "^5.1.0" - jsonwebtoken "^8.3.0" - lru-cache "^5.1.1" - -"@octokit/auth-token@^2.4.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" - integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== - dependencies: - "@octokit/types" "^6.0.3" - -"@octokit/endpoint@^3.2.0": - version "3.2.3" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-3.2.3.tgz#bd9aea60cd94ce336656b57a5c9cb7f10be8f4f3" - integrity sha512-yUPCt4vMIOclox13CUxzuKiPJIFo46b/6GhUnUTw5QySczN1L0DtSxgmIZrZV4SAb9EyAqrceoyrWoYVnfF2AA== - dependencies: - deepmerge "3.2.0" - is-plain-object "^2.0.4" - universal-user-agent "^2.0.1" - url-template "^2.0.8" - -"@octokit/endpoint@^5.1.0": - version "5.5.3" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978" - integrity sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ== - dependencies: - "@octokit/types" "^2.0.0" - is-plain-object "^3.0.0" - universal-user-agent "^5.0.0" - -"@octokit/endpoint@^6.0.1": - version "6.0.12" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" - integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== - dependencies: - "@octokit/types" "^6.0.3" - is-plain-object "^5.0.0" - universal-user-agent "^6.0.0" - -"@octokit/openapi-types@^11.2.0": - version "11.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" - integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== - -"@octokit/plugin-paginate-rest@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" - integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== - dependencies: - "@octokit/types" "^2.0.1" - -"@octokit/plugin-request-log@^1.0.0": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" - integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== - -"@octokit/plugin-rest-endpoint-methods@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" - integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== - dependencies: - "@octokit/types" "^2.0.1" - deprecation "^2.3.1" - -"@octokit/plugin-retry@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-2.2.0.tgz#11f3957a46ccdb7b7f33caabf8c17e57b25b80b2" - integrity sha512-x5Kd8Lke+a4hTDCe5akZxpGmVwu1eeVt2FJX0jeo3CxHGbfHbXb4zhN5quKfGL9oBLV/EdHQIJ6zwIMjuzxOlw== - dependencies: - bottleneck "^2.15.3" - -"@octokit/request-error@^1.0.2": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" - integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== - dependencies: - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request-error@^2.1.0": +"@opensearch-project/opensearch@^2.1.0": version "2.1.0" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" - integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== - dependencies: - "@octokit/types" "^6.0.3" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request@^2.4.2": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-2.4.2.tgz#87c36e820dd1e43b1629f4f35c95b00cd456320b" - integrity sha512-lxVlYYvwGbKSHXfbPk5vxEA8w4zHOH1wobado4a9EfsyD3Cbhuhus1w0Ye9Ro0eMubGO8kNy5d+xNFisM3Tvaw== - dependencies: - "@octokit/endpoint" "^3.2.0" - deprecation "^1.0.1" - is-plain-object "^2.0.4" - node-fetch "^2.3.0" - once "^1.4.0" - universal-user-agent "^2.0.1" - -"@octokit/request@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-3.0.3.tgz#ace63b5ea196cc00ad27f3fbe5c13a9698681ec8" - integrity sha512-M7pUfsiaiiUMEP4/SMysTeWxyGrkoQg6FBPEtCBIFgeDnzHaPboTpUZGTh6u1GQXdrlzMfPVn/vQs98js1QtwQ== - dependencies: - "@octokit/endpoint" "^5.1.0" - deprecation "^1.0.1" - is-plain-object "^3.0.0" - node-fetch "^2.3.0" - once "^1.4.0" - universal-user-agent "^2.0.1" - -"@octokit/request@^5.2.0": - version "5.6.3" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" - integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== - dependencies: - "@octokit/endpoint" "^6.0.1" - "@octokit/request-error" "^2.1.0" - "@octokit/types" "^6.16.1" - is-plain-object "^5.0.0" - node-fetch "^2.6.7" - universal-user-agent "^6.0.0" - -"@octokit/rest@^16.23.2": - version "16.43.2" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.2.tgz#c53426f1e1d1044dee967023e3279c50993dd91b" - integrity sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/plugin-paginate-rest" "^1.1.1" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "2.4.0" - "@octokit/request" "^5.2.0" - "@octokit/request-error" "^1.0.2" - atob-lite "^2.0.0" - before-after-hook "^2.0.0" - btoa-lite "^1.0.0" - deprecation "^2.0.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniq "^4.5.0" - octokit-pagination-methods "^1.1.0" - once "^1.4.0" - universal-user-agent "^4.0.0" - -"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": - version "2.16.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2" - integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q== - dependencies: - "@types/node" ">= 8" - -"@octokit/types@^6.0.3", "@octokit/types@^6.16.1": - version "6.34.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" - integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== - dependencies: - "@octokit/openapi-types" "^11.2.0" - -"@opensearch-project/opensearch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-1.1.0.tgz#8b3c8b4cbcea01755ba092d2997bf0b4ca7f22f7" - integrity sha512-1TDw92JL8rD1b2QGluqBsIBLIiD5SGciIpz4qkrGAe9tcdfQ1ptub5e677rhWl35UULSjr6hP8M6HmISZ/M5HQ== + resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.1.0.tgz#d79ab4ae643493512099673e117faffe40b4fe56" + integrity sha512-iM2u63j2IlUOuMSbcw1TZFpRqjK6qMwVhb3jLLa/x4aATxdKOiO1i17mgzfkeepqj85efNzXBZzN+jkq1/EXhQ== dependencies: + aws4 "^1.11.0" debug "^4.3.1" hpagent "^0.1.1" ms "^2.1.3" @@ -2707,9 +2541,9 @@ "@hapi/hoek" "^9.0.0" "@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0" @@ -2764,10 +2598,10 @@ dependencies: defer-to-connect "^2.0.0" -"@testim/chrome-version@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.2.tgz#092005c5b77bd3bb6576a4677110a11485e11864" - integrity sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw== +"@testim/chrome-version@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.3.tgz#fbb68696899d7b8c1b9b891eded9c04fe2cd5529" + integrity sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A== "@testing-library/dom@^8.0.0", "@testing-library/dom@^8.11.3": version "8.12.0" @@ -2922,14 +2756,7 @@ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.1.3.tgz#0b03d737ff28fad10eb884e0c6cedd5ffdc4ba0a" integrity sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g== -"@types/chromedriver@^81.0.0": - version "81.0.1" - resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-81.0.1.tgz#bff3e4cdc7830dc0f115a9c0404f6979771064d4" - integrity sha512-I7ma6bBzfWc5YiMV/OZ6lYMZIANAwGbDH+QRYKnbXRptdAvUhSoFP5iHzQHas6QZCRDtefMvbxCjySUyXhxafQ== - dependencies: - "@types/node" "*" - -"@types/clone@~2.1.0": +"@types/clone@~2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.1.tgz#9b880d0ce9b1f209b5e0bd6d9caa38209db34024" integrity sha512-BZIU34bSYye0j/BFcPraiDZ5ka6MJADjcDVELGf7glr9K+iE8NYVjFslJFVWzskSxkLLyCrSPScE82/UUoBSvg== @@ -3005,6 +2832,13 @@ resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964" integrity sha512-sq+kwx8zA9BSugT9N+Jr8/uWjbHMZ+N/meJEzRyT3gmLq/WMtx/iSIpvdpmBUi/cvXl6Kzpvve8G2ESkabFwmg== +"@types/dompurify@^2.3.3": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" + integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== + dependencies: + "@types/trusted-types" "*" + "@types/duplexify@^3.6.0": version "3.6.1" resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.1.tgz#5685721cf7dc4a21b6f0e8a8efbec6b4d2fbafad" @@ -3017,7 +2851,7 @@ resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.0.tgz#ab8109208106b5e764e5a6c92b2ba1c625b73020" integrity sha512-DCg+Ka+uDQ31lJ/UtEXVlaeV3d6t81gifaVWKJy4MYVVgvJttyX/viREy+If7fz+tK/gVxTGMtyrFPnm4gjrVA== -"@types/elasticsearch@^5.0.33": +"@types/elasticsearch@*", "@types/elasticsearch@^5.0.33": version "5.0.40" resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.40.tgz#811f6954088c264173e0a9876b97933250a4da10" integrity sha512-lhnbkC0XorAD7Dt7X+94cXUSHEdDNnEVk/DgFLHgIZQNhixV631Lj4+KpXunTT5rCHyj9RqK3TfO7QrOiwEeUQ== @@ -3051,15 +2885,10 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== - -"@types/estree@^0.0.50": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== "@types/expect@^1.20.4": version "1.20.4" @@ -3071,13 +2900,6 @@ resolved "https://registry.yarnpkg.com/@types/extract-zip/-/extract-zip-1.6.2.tgz#5c7eb441c41136167a42b88b64051e6260c29e86" integrity sha1-XH60QcQRNhZ6QriLZAUeYmDCnoY= -"@types/fast-json-stable-stringify@^2.0.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#c4d9c003d546b7ca9496ea924e9e9faca72873b4" - integrity sha512-IyNhGHu71jH1jCXTHmafuoAAdsbBON3kDh7u/UUhLmjYgN5TYB54e1R8ckTCiIevl2UuZaCsi9XRxineY5yUjw== - dependencies: - fast-json-stable-stringify "*" - "@types/fetch-mock@^7.3.1": version "7.3.5" resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.3.5.tgz#7aee678c4e7c7e1a168bae8fdab5b8d712e377f6" @@ -3090,6 +2912,11 @@ dependencies: "@types/jquery" "*" +"@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/getopts@^2.0.1": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/getopts/-/getopts-2.1.0.tgz#f2c7423b25c8e8d12326cd8c389ff9ec6fc51e79" @@ -3228,6 +3055,15 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/http-aws-es@6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/http-aws-es/-/http-aws-es-6.0.2.tgz#3c608f7da83382bb5a1a35c4f9704296b979ca26" + integrity sha512-VfQ/h+xxdeWP2Sf3BDf2feyzC8duBH5rFPJw2RW5m800fJLkZof/oojn1Atw1jCh4XerjiXRTIyqd5gUQ2iWNw== + dependencies: + "@types/elasticsearch" "*" + "@types/node" "*" + aws-sdk "^2.814.0" + "@types/http-cache-semantics@*": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" @@ -3449,7 +3285,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@>= 8", "@types/node@^14.17.32": +"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^14.17.32": version "14.18.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24" integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A== @@ -3736,9 +3572,9 @@ csstype "^3.0.2" "@types/superagent@*": - version "4.1.15" - resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" - integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + version "4.1.16" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881" + integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ== dependencies: "@types/cookiejar" "*" "@types/node" "*" @@ -3752,7 +3588,7 @@ "@types/superagent" "*" "@types/supertest" "*" -"@types/supertest@*", "@types/supertest@^2.0.11": +"@types/supertest@*", "@types/supertest@^2.0.12": version "2.0.12" resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== @@ -3801,6 +3637,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40" integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg== +"@types/trusted-types@*": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + "@types/type-detect@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6" @@ -4279,9 +4120,9 @@ acorn@^7.0.0, acorn@^7.1.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.2.4: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== adm-zip@0.5.9: version "0.5.9" @@ -4300,7 +4141,7 @@ agent-base@4: dependencies: es6-promisify "^5.0.0" -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -4314,7 +4155,7 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" -agentkeepalive@^4.2.1: +agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== @@ -4356,7 +4197,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -4418,10 +4259,10 @@ angular@>=1.0.6, angular@^1.8.2: resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.2.tgz#5983bbb5a9fa63e213cb7749199e0d352de3a2f1" integrity sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw== -ansi-colors@3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-colors@^1.0.1: version "1.1.0" @@ -4506,7 +4347,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.0, anymatch@^3.0.3, anymatch@~3.1.1, anymatch@~3.1.2: +anymatch@^3.0.0, anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -4528,7 +4369,12 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -aproba@^1.0.3, aproba@^1.1.1: +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== @@ -4567,13 +4413,13 @@ archy@^1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -are-we-there-yet@~1.1.2: - version "1.1.7" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== dependencies: delegates "^1.0.0" - readable-stream "^2.0.6" + readable-stream "^3.6.0" argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" @@ -4637,11 +4483,6 @@ array-find@^1.0.0: resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" integrity sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg= -array-flat-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz#1e3a4255be619dfbffbfd1d635c1cf357cd034e7" - integrity sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw== - array-from@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" @@ -4758,18 +4599,6 @@ asn1.js@^5.2.0, asn1.js@^5.3.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" -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 sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - assert@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" @@ -4815,13 +4644,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-retry@^1.2.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" - integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== - dependencies: - retry "0.13.1" - async-value-promise@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/async-value-promise/-/async-value-promise-1.1.1.tgz#68957819e3eace804f3b4b69477e2bd276c15378" @@ -4844,11 +4666,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -4902,12 +4719,23 @@ aws-sdk@^2.650.0: uuid "8.0.0" xml2js "0.4.19" -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= +aws-sdk@^2.814.0: + version "2.1271.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1271.0.tgz#a060fe65ff33afddb7385913200df4a26717f691" + integrity sha512-hQF+mjwe2FXFKOMNQGlfqn9InIL1bRp650ftctRqDo+VpnrYnKqF9eZa5Hk2kugs3/WUa4J2aqQa+foGWeH+Fg== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.16.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + util "^0.12.4" + uuid "8.0.0" + xml2js "0.4.19" -aws4@^1.8.0: +aws4@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== @@ -4917,7 +4745,7 @@ axe-core@^4.0.2, axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^0.24.0, axios@^0.27.2: +axios@^0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== @@ -4925,6 +4753,15 @@ axios@^0.24.0, axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.0.tgz#1cb65bd75162c70e9f8d118a905126c4a201d383" + integrity sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5137,18 +4974,6 @@ batch-processor@1.0.0: resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -before-after-hook@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" - integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -5215,11 +5040,6 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bottleneck@^2.15.3: - version "2.19.5" - resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" - integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5228,6 +5048,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + brace@0.11.1, brace@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" @@ -5357,21 +5184,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -5467,7 +5284,7 @@ cacache@^13.0.1: ssri "^7.0.0" unique-filename "^1.1.1" -cacache@^15.0.5: +cacache@^15.0.5, cacache@^15.2.0: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== @@ -5566,7 +5383,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: +camelcase@^6.0.0, camelcase@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -5577,14 +5394,9 @@ camelize@^1.0.0: integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= caniuse-lite@^1.0.30001317: - version "1.0.30001397" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz" - integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + version "1.0.30001460" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz" + integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== ccount@^1.0.0: version "1.1.0" @@ -5728,22 +5540,7 @@ cheerio@^1.0.0-rc.3: parse5-htmlparser2-tree-adapter "^6.0.1" tslib "^2.2.0" -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.2.0" - optionalDependencies: - fsevents "~2.1.1" - -"chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.2: +chokidar@3.5.3, "chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.2: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -5797,16 +5594,16 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -chromedriver@^100.0.0: - version "100.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-100.0.0.tgz#1b4bf5c89cea12c79f53bc94d8f5bb5aa79ed7be" - integrity sha512-oLfB0IgFEGY9qYpFQO/BNSXbPw7bgfJUN5VX8Okps9W2qNT4IqKh5hDwKWtpUIQNI6K3ToWe2/J5NdpurTY02g== +chromedriver@^107.0.3: + version "107.0.3" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-107.0.3.tgz#330c0808bb14a53f13ab7e2b0c78adf3cdb4c14b" + integrity sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg== dependencies: - "@testim/chrome-version" "^1.1.2" - axios "^0.24.0" - del "^6.0.0" + "@testim/chrome-version" "^1.1.3" + axios "^1.1.3" + compare-versions "^5.0.1" extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" proxy-from-env "^1.1.0" tcp-port-used "^1.0.1" @@ -5888,15 +5685,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -5906,7 +5694,7 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.0, cliui@^7.0.2: +cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== @@ -5915,6 +5703,15 @@ cliui@^7.0.0, cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -6045,6 +5842,11 @@ color-string@^1.4.0: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + color@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" @@ -6073,7 +5875,7 @@ colors@~1.2.1: resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc" integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg== -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -6120,6 +5922,11 @@ compare-versions@3.5.1: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== +compare-versions@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.1.tgz#14c6008436d994c3787aba38d4087fabe858555e" + integrity sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ== + component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -6161,29 +5968,15 @@ concat-stream@^1.4.7, concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" -configstore@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" - integrity sha1-w1eB0FAdJowlxUuLF/YkDopPsCE= - dependencies: - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - object-assign "^4.0.1" - os-tmpdir "^1.0.0" - osenv "^0.1.0" - uuid "^2.0.1" - write-file-atomic "^1.1.2" - xdg-basedir "^2.0.0" - console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== console-log-level@^1.4.1: version "1.4.1" @@ -6219,10 +6012,10 @@ cookie@^0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -cookiejar@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" - integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== copy-anything@^2.0.1: version "2.0.6" @@ -6295,11 +6088,6 @@ core-js@^3.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== -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 sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - core-util-is@^1.0.2, 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" @@ -6395,7 +6183,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -6609,10 +6397,10 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3.1.1, d3-array@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c" - integrity sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ== +"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3.2.2, d3-array@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.2.tgz#f8ac4705c5b06914a7e0025bbf8d5f1513f6a86e" + integrity sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ== dependencies: internmap "1 - 2" @@ -6633,7 +6421,7 @@ d3-collection@1, d3-collection@^1.0.7: resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== -d3-color@1, "d3-color@1 - 3", d3-color@^1.4.0, d3-color@^3.0.1, d3-color@^3.1.0: +d3-color@1, "d3-color@1 - 3", d3-color@^1.4.0, d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== @@ -6692,17 +6480,17 @@ d3-geo-projection@^4.0.0: d3-array "1 - 3" d3-geo "1.12.0 - 3" -"d3-geo@1.12.0 - 3", d3-geo@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e" - integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA== +"d3-geo@1.12.0 - 3", d3-geo@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" + integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== dependencies: d3-array "2.5.0 - 3" -d3-hierarchy@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz#9cbb0ffd2375137a351e6cfeed344a06d4ff4597" - integrity sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA== +d3-hierarchy@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== d3-interpolate@1, d3-interpolate@^1.4.0: version "1.4.0" @@ -6723,10 +6511,10 @@ d3-path@1: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== -"d3-path@1 - 3", d3-path@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" - integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== "d3-quadtree@1 - 3": version "3.0.1" @@ -6764,12 +6552,12 @@ d3-shape@^1.3.4: dependencies: d3-path "1" -d3-shape@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" - integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ== +d3-shape@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== dependencies: - d3-path "1 - 3" + d3-path "^3.1.0" d3-time-format@2: version "2.3.0" @@ -6790,10 +6578,10 @@ d3-time@1: resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975" - integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ== +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== dependencies: d3-array "2 - 3" @@ -6820,13 +6608,6 @@ damerau-levenshtein@^1.0.7: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - dashify@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" @@ -6867,13 +6648,6 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - debug@3.X, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -6881,7 +6655,7 @@ debug@3.X, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@4, debug@^4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -6920,15 +6694,20 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decompress-response@^6.0.0: version "6.0.0" @@ -6970,11 +6749,6 @@ deep-equal@^2.0.5: which-collection "^1.0.1" which-typed-array "^1.1.2" -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-freeze-strict@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0" @@ -6985,11 +6759,6 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e" - integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow== - deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -7014,7 +6783,7 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -define-properties@^1.1.2, define-properties@^1.1.3: +define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -7091,10 +6860,10 @@ del@^5.1.0: rimraf "^3.0.0" slash "^3.0.0" -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== +del@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" + integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== dependencies: globby "^11.0.1" graceful-fs "^4.2.4" @@ -7151,16 +6920,6 @@ dependency-check@^4.1.0: read-package-json "^2.0.10" resolve "^1.1.7" -deprecation@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-1.0.1.tgz#2df79b79005752180816b7b6e079cbd80490d711" - integrity sha512-ccVHpE72+tcIKaGMql33x5MAjKQIZrk+3x2GbJ7TeraUCZWHoT+KSZpoC+JQFsUBlSTXUrBaGiF0j6zVTepPLg== - -deprecation@^2.0.0, deprecation@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" - integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== - des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -7208,7 +6967,7 @@ detective@^5.0.2: defined "^1.0.0" minimist "^1.1.1" -dezalgo@1.0.3, dezalgo@^1.0.0: +dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= @@ -7216,6 +6975,14 @@ dezalgo@1.0.3, dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-match-patch@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" @@ -7226,7 +6993,12 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== -diff@3.5.0, diff@^3.5.0: +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== @@ -7365,6 +7137,11 @@ domhandler@^4.0, domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2, domhan dependencies: domelementtype "^2.2.0" +dompurify@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631" + integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA== + domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -7402,7 +7179,7 @@ duplexer@^0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== -duplexify@^3.2.0, duplexify@^3.4.2, duplexify@^3.6.0: +duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== @@ -7429,21 +7206,6 @@ eachr@^4.5.0: dependencies: typechecker "^6.2.0" -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ejs@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" @@ -7575,6 +7337,13 @@ emoticon@^3.2.0: resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -7703,6 +7472,11 @@ enzyme@^3.11.0: rst-selector-parser "^2.2.3" string.prototype.trim "^1.2.1" +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -7872,12 +7646,17 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -escalade@^3.0.2, escalade@^3.1.1: +escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, 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 sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -7887,11 +7666,6 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -8305,19 +8079,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -8427,7 +8188,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.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== @@ -8474,16 +8235,6 @@ extract-zip@^2.0.1: 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 sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -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== - fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -8517,7 +8268,7 @@ fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.4, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@*, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@~2.1.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -8732,12 +8483,13 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@3.0.0, find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - locate-path "^3.0.0" + locate-path "^6.0.0" + path-exists "^4.0.0" find-up@^2.1.0: version "2.1.0" @@ -8746,6 +8498,13 @@ find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -8804,12 +8563,10 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flat@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" - integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== - dependencies: - is-buffer "~2.0.3" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatstr@^1.0.12: version "1.0.12" @@ -8841,10 +8598,10 @@ focus-lock@^0.10.2: dependencies: tslib "^2.0.3" -follow-redirects@^1.14.9: - version "1.15.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" - integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== +follow-redirects@^1.14.9, follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== font-awesome@4.7.0: version "4.7.0" @@ -8883,11 +8640,6 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -8906,24 +8658,15 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -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" - -formidable@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" - integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== dependencies: - dezalgo "1.0.3" - hexoid "1.0.0" - once "1.4.0" - qs "6.9.3" + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" forwarded-parse@^2.1.0: version "2.1.2" @@ -9027,11 +8770,6 @@ fsevents@^2.3.2, fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fsevents@~2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -9057,29 +8795,29 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" -geckodriver@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.0.2.tgz#6bd69166a24859c5edbc6ece9868339378b6c97b" - integrity sha512-GHOQzQnTeZOJdcdEXLuzmcRwkbHuei1VivXkn2BLyleKiT6lTvl0T7vm+d0wvr/EZC7jr0m1u1pBHSfqtuFuNQ== +geckodriver@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.2.0.tgz#6b0a85e2aafbce209bca30e2d53af857707b1034" + integrity sha512-p+qR2RKlI/TQoCEYrSuTaYCLqsJNni96WmEukTyXmOmLn+3FLdgPAEwMZ0sG2Cwi9hozUzGAWyT6zLuhF6cpiQ== dependencies: adm-zip "0.5.9" bluebird "3.7.2" got "11.8.5" - https-proxy-agent "5.0.0" + https-proxy-agent "5.0.1" tar "6.1.11" gensync@^1.0.0-beta.2: @@ -9130,13 +8868,6 @@ get-stdin@^8.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - get-stream@^5.0.0, get-stream@^5.1.0, get-stream@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -9186,13 +8917,6 @@ getos@^3.2.1: 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 sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - gifwrap@^0.9.2: version "0.9.4" resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.9.4.tgz#f4eb6169ba027d61df64aafbdcb1f8ae58ccc0c5" @@ -9209,7 +8933,7 @@ glob-all@^3.2.1: glob "^7.1.2" yargs "^15.3.1" -glob-parent@^3.1.0, glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@^6.0.0, glob-parent@~5.1.0, glob-parent@~5.1.2: +glob-parent@^3.1.0, glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@^6.0.0, glob-parent@~5.1.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -9242,18 +8966,6 @@ glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.1.7, glob@~7.1.6: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" @@ -9266,7 +8978,7 @@ glob@7.1.7, glob@~7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: +glob@7.2.0, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -9414,7 +9126,7 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= -got@11.8.5: +got@11.8.5, got@^11.8.2: version "11.8.5" resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== @@ -9431,54 +9143,11 @@ got@11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -got@^11.8.2: - version "11.8.3" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" - integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== - dependencies: - "@sindresorhus/is" "^4.0.0" - "@szmarczak/http-timer" "^4.0.5" - "@types/cacheable-request" "^6.0.1" - "@types/responselike" "^1.0.0" - cacheable-lookup "^5.0.3" - cacheable-request "^7.0.2" - decompress-response "^6.0.0" - http2-wrapper "^1.0.0-beta.5.2" - lowercase-keys "^2.0.0" - p-cancelable "^2.0.0" - responselike "^2.0.0" - -got@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" - integrity sha1-5dDtSvVfw+701WAHdp2YGSvLLso= - dependencies: - duplexify "^3.2.0" - infinity-agent "^2.0.0" - is-redirect "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - nested-error-stacks "^1.0.0" - object-assign "^3.0.0" - prepend-http "^1.0.0" - read-all-stream "^3.0.0" - timed-out "^2.0.0" - -graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -graceful-fs@^4.2.0: +graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - grunt-available-tasks@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/grunt-available-tasks/-/grunt-available-tasks-0.6.3.tgz#5be7f6fdda776b80a7b272a21f68bd3050f82260" @@ -9640,19 +9309,6 @@ handlebars@4.7.7: optionalDependencies: uglify-js "^3.1.4" -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -9721,7 +9377,7 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -9733,10 +9389,10 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.0: +has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== has-value@^1.0.0: version "1.0.0" @@ -9902,7 +9558,7 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hexoid@1.0.0: +hexoid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== @@ -10051,10 +9707,15 @@ htmlparser2@^7.0: domutils "^2.8.0" entities "^3.0.1" -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== +http-aws-es@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/http-aws-es/-/http-aws-es-6.0.0.tgz#1528978d2bee718b8732dcdced0856efa747aeff" + integrity sha512-g+qp7J110/m4aHrR3iit4akAlnW0UljZ6oTq/rCcbsI8KP9x+95vqUtx49M2XQ2JMpwJio3B6gDYx+E8WDxqiA== + +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-headers@^3.0.2: version "3.0.2" @@ -10080,15 +9741,6 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -10102,10 +9754,10 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== +https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" @@ -10139,7 +9791,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6: +iconv-lite@0.6, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -10261,11 +9913,6 @@ infer-owner@^1.0.3, infer-owner@^1.0.4: resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== -infinity-agent@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216" - integrity sha1-ReDi/3qesDCyfWK3SzdEt6esQhY= - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -10289,7 +9936,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, 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== @@ -10332,10 +9979,10 @@ inquirer@^7.0.0, inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -install-artifact-from-github@^1.0.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.0.tgz#cab6ff821976b8a35b0c079da19a727c90381a40" - integrity sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA== +install-artifact-from-github@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz#eefaad9af35d632e5d912ad1569c1de38c3c2462" + integrity sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg== internal-slot@^1.0.3: version "1.0.3" @@ -10428,6 +10075,11 @@ ip-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + irregular-plurals@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-3.3.0.tgz#67d0715d4361a60d9fd9ee80af3881c631a31ee2" @@ -10520,7 +10172,7 @@ is-buffer@^1.1.4, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.0, is-buffer@~2.0.3: +is-buffer@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -10668,6 +10320,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" @@ -10696,11 +10353,6 @@ is-nil@^1.0.0: resolved "https://registry.yarnpkg.com/is-nil/-/is-nil-1.0.1.tgz#2daba29e0b585063875e7b539d071f5b15937969" integrity sha1-LauingtYUGOHXntTnQcfWxWTeWk= -is-npm@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" - integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= - is-number-object@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" @@ -10756,7 +10408,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-obj@^2.0.0: +is-plain-obj@^2.0.0, is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== @@ -10768,11 +10420,6 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-plain-object@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" - integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -10788,11 +10435,6 @@ is-promise@^2.1.0, is-promise@^2.2.2: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= - is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -10830,7 +10472,7 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" -is-stream@^1.0.0, is-stream@^1.1.0: +is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -10881,7 +10523,7 @@ is-typed-array@^1.1.7: foreach "^2.0.5" has-tostringtag "^1.0.0" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -11011,11 +10653,6 @@ isobject@^4.0.0: resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -11158,10 +10795,10 @@ jest-cli@^27.5.1: prompts "^2.0.1" yargs "^16.2.0" -jest-config@^27.5.1: +jest-config@^27.5.1, "jest-config@npm:@amoo-miki/jest-config@27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" - integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== + resolved "https://registry.yarnpkg.com/@amoo-miki/jest-config/-/jest-config-27.5.1.tgz#3afdb485fdccad8fd8b19be505747cafbf8e6707" + integrity sha512-6pSD/Lo5axflM2NO0eIHuJ40nxfXn2NQGqzOCrZO1EE7yS3k90p10RalInf8RXl2g9/f1Ax4U4aU2e3BWSaSaQ== dependencies: "@babel/core" "^7.8.0" "@jest/test-sequencer" "^27.5.1" @@ -11266,10 +10903,10 @@ jest-haste-map@^27.5.1: optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.5.1: +jest-jasmine2@^27.5.1, "jest-jasmine2@npm:@amoo-miki/jest-jasmine2@27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" - integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== + resolved "https://registry.yarnpkg.com/@amoo-miki/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#0c99502f9879e1504b124da8355880924de1f310" + integrity sha512-oOihnJGLdFZO780Ts6XR+Sbs8d4J0+sqcNvBNcJch/LSKpQt7rARi0V+TgF80LRAvJI4gyq97az/2fdXXgxDNg== dependencies: "@jest/environment" "^27.5.1" "@jest/source-map" "^27.5.1" @@ -11597,14 +11234,6 @@ js-yaml-js-types@1.0.0: dependencies: esprima "^4.0.1" -js-yaml@3.13.1, js-yaml@~3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -11620,16 +11249,19 @@ js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@~3.14.0: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@~3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsbn@1.1.0, jsbn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA= -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -11698,7 +11330,7 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.4.0, json-schema@^0.4.0: +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== @@ -11720,27 +11352,27 @@ json-stringify-pretty-compact@1.2.0: resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-1.2.0.tgz#0bc316b5e6831c07041fc35612487fb4e9ab98b8" integrity sha512-/11Pj1OyX814QMKO7K8l85SHPTr/KsFxHp8GE2zVa0BtJgGimDjXHfM3FhC7keQdWDea7+nXf+f1de7ATZcZkQ== -json-stringify-pretty-compact@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885" - integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ== +json-stringify-pretty-compact@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz#f71ef9d82ef16483a407869556588e91b681d9ab" + integrity sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA== -json-stringify-safe@5.0.1, json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: +json-stringify-safe@5.0.1, 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 sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" json5@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^4.0.0: version "4.0.0" @@ -11759,32 +11391,6 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsonwebtoken@^8.3.0: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" @@ -11813,23 +11419,6 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - keyv@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.1.1.tgz#02c538bfdbd2a9308cc932d4096f05ae42bfa06a" @@ -11869,13 +11458,6 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -latest-version@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" - integrity sha1-cs/Ebj6NG+ZR4eu1Tqn26pbzdLs= - dependencies: - package-json "^1.0.0" - lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -11900,10 +11482,10 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -"leaflet-vega@npm:@amoo-miki/leaflet-vega@0.8.8": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@amoo-miki/leaflet-vega/-/leaflet-vega-0.8.8.tgz#675abf37d72fbea859755e982f4fd19dea776557" - integrity sha512-W2gGgFDxzy/XUx+fQJfz0NYVXsKl7V+G6QywiMcOV5NEodDId9c60up7NNf+cfM7ggpo+5BuLqrKmosuGO1CsA== +leaflet-vega@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/leaflet-vega/-/leaflet-vega-0.9.0.tgz#ea3d87221bae452dbeea7f435f4d6165d3c71eb7" + integrity sha512-mECYEAf4/k9JPWyWv0/OhA0vTjkRilsQas2PdDjALR1q9/SHkA056SDFNidIgQzg4/d/5NiQLpwBC7Hk4k43wA== dependencies: vega-spec-injector "^0.0.2" @@ -12103,10 +11685,10 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@^1.2.3, loader-utils@^2.0.0, loader-utils@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1" - integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A== +loader-utils@^1.2.3, loader-utils@^2.0.0, loader-utils@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" @@ -12135,6 +11717,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash-es@^4.17.15: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -12205,41 +11794,16 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - lodash.isequal@^4.0.0, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.map@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -12255,11 +11819,6 @@ lodash.merge@4.6.2, lodash.merge@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - lodash.padstart@4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" @@ -12285,11 +11844,6 @@ lodash.repeat@4.1.0: resolved "https://registry.yarnpkg.com/lodash.repeat/-/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44" integrity sha1-/H3oEx2MisB+S0n3T/6CnR8r7EQ= -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.some@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -12310,11 +11864,6 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - lodash@4.17.21, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -12328,12 +11877,13 @@ log-ok@^0.1.1: ansi-green "^0.1.1" success-symbol "^0.1.0" -log-symbols@3.0.0, log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@4.1.0, 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 "^2.4.2" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" log-symbols@^1.0.2: version "1.0.2" @@ -12349,13 +11899,12 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -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== +log-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" + chalk "^2.4.2" log-update@^2.3.0: version "2.3.0" @@ -12379,9 +11928,9 @@ lolex@^5.0.1: "@sinonjs/commons" "^1.7.0" long@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.0.tgz#2696dadf4b4da2ce3f6f6b89186085d94d52fd61" - integrity sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w== + version "5.2.1" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f" + integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" @@ -12390,11 +11939,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0 || ^4.0.0" -lowercase-keys@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -12434,11 +11978,6 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= -macos-release@^2.2.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2" - integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g== - make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -12454,6 +11993,28 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -12711,23 +12272,23 @@ mime-db@1.52.0, mime-db@1.x.x: 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.34, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@^2.1.34: 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" +mime@2.6.0, mime@^2.4.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mime@^1.3.4, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.4, mime@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -12787,13 +12348,27 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2, minimatch@~3.0.4: +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.2: 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" +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@~3.0.4: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -12815,6 +12390,17 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -12822,13 +12408,20 @@ minipass-flush@^1.0.5: dependencies: minipass "^3.0.0" -minipass-pipeline@^1.2.2: +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: minipass "^3.0.0" +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + minipass@^3.0.0, minipass@^3.1.1: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -12836,7 +12429,21 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" -minizlib@^2.1.1: +minipass@^3.1.0, minipass@^3.1.3: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.0.tgz#7cebb0f9fa7d56f0c5b17853cbe28838a8dbbd3b" + integrity sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw== + dependencies: + yallist "^4.0.0" + +minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -12873,19 +12480,12 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - mkdirp@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.0: +mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.0: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -12897,35 +12497,32 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mocha@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.2.0.tgz#01cc227b00d875ab1eed03a75106689cfed5a604" - integrity sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ== +mocha@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" + integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== dependencies: - ansi-colors "3.2.3" + ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.3.0" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" he "1.2.0" - js-yaml "3.13.1" - log-symbols "3.0.0" - minimatch "3.0.4" - mkdirp "0.5.5" - ms "2.1.1" - node-environment-flags "1.0.6" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" mock-fs@^4.12.0: version "4.14.0" @@ -13003,17 +12600,12 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - 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.0.0, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -13064,11 +12656,16 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.12.1, nan@^2.14.1, nan@^2.14.2: +nan@^2.12.1, nan@^2.14.2: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nan@^2.15.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + nano-css@^5.2.1: version "5.3.4" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.4.tgz#40af6a83a76f84204f346e8ccaa9169cdae9167b" @@ -13083,6 +12680,11 @@ nano-css@^5.2.1: stacktrace-js "^2.0.2" stylis "^4.0.6" +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + nanoid@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557" @@ -13134,18 +12736,16 @@ needle@^2.5.2: iconv-lite "^0.4.4" sax "^1.2.4" +negotiator@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -nested-error-stacks@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" - integrity sha1-GfYZWRUZ8JZ2mlupqG5u7sgjw88= - dependencies: - inherits "~2.0.1" - nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" @@ -13207,15 +12807,7 @@ node-emoji@^1.10.0: dependencies: lodash "^4.17.21" -node-environment-flags@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - -node-fetch@^2.3.0, node-fetch@^2.6.7: +node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -13232,20 +12824,20 @@ node-gyp-build@^4.2.3: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-gyp@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" - integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== +node-gyp@^8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== dependencies: env-paths "^2.2.0" glob "^7.1.4" - graceful-fs "^4.2.3" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" nopt "^5.0.0" - npmlog "^4.1.2" - request "^2.88.2" + npmlog "^6.0.0" rimraf "^3.0.2" - semver "^7.3.2" - tar "^6.0.2" + semver "^7.3.5" + tar "^6.1.2" which "^2.0.2" node-int64@^0.4.0: @@ -13253,10 +12845,10 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-jose@2.0.0, node-jose@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/node-jose/-/node-jose-2.1.1.tgz#7d82e8d9cef8d0a722d7fa385524babfa9e30614" - integrity sha512-19nyuUGShNmFmVTeqDfP6ZJCiikbcjI0Pw2kykBCH7rl8AZgSiDZK2Ww8EDaMrOSbRg6IlfIMhI5ZvCklmOhzg== +node-jose@2.0.0, node-jose@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/node-jose/-/node-jose-2.2.0.tgz#b64f3225ad6bec328509a420800de597ba2bf3ed" + integrity sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw== dependencies: base64url "^3.0.1" buffer "^6.0.3" @@ -13266,7 +12858,7 @@ node-jose@2.0.0, node-jose@^2.1.0: node-forge "^1.2.1" pako "^2.0.4" process "^0.11.10" - uuid "^8.3.2" + uuid "^9.0.0" "node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.2.1: version "2.2.1" @@ -13402,13 +12994,6 @@ npm-normalize-package-bin@^1.0.0: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -13416,15 +13001,15 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" nth-check@^2.0.1, nth-check@~1.0.1: version "2.0.1" @@ -13481,21 +13066,11 @@ nyc@^15.1.0: test-exclude "^6.0.0" yargs "^15.0.2" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-assign@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" - integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= - object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -13535,7 +13110,7 @@ object-is@^1.0.2, object-is@^1.1.2, object-is@^1.1.4, object-is@^1.1.5: call-bind "^1.0.2" define-properties "^1.1.3" -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -13547,16 +13122,6 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" @@ -13605,15 +13170,6 @@ object.fromentries@^2.0.3, object.fromentries@^2.0.5: define-properties "^1.1.3" es-abstract "^1.19.1" -object.getownpropertydescriptors@^2.0.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" - integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - object.hasown@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" @@ -13646,17 +13202,12 @@ object.values@^1.1.1, object.values@^1.1.2, object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - omggif@^1.0.10, omggif@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== -once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -13742,14 +13293,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-name@^3.0.0, os-name@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" - integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== - dependencies: - macos-release "^2.2.0" - windows-release "^3.1.0" - os-shim@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" @@ -13760,7 +13303,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@^0.1.0, osenv@^0.1.4: +osenv@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -13841,6 +13384,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -13887,23 +13437,15 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" -package-json@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" - integrity sha1-yOysCUInzfdqMWh07QXifMk5oOA= - dependencies: - got "^3.2.0" - registry-url "^3.0.0" - pako@^1.0.5, pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== pako@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" - integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== parallel-transform@^1.1.0: version "1.2.0" @@ -14057,7 +13599,7 @@ path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^2.0.0, path-key@^2.0.1: +path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= @@ -14369,11 +13911,6 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-http@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -14442,6 +13979,14 @@ promise-polyfill@^8.1.3: resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6" integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg== +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -14500,7 +14045,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== @@ -14557,7 +14102,7 @@ punycode@^1.2.4: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= -qs@6.9.3, qs@^6.10.1, qs@^6.10.3, qs@~6.5.2: +qs@^6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -14679,29 +14224,19 @@ raw-loader@^4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" -rc@^1.0.1: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - re-reselect@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd" integrity sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg== -re2@^1.15.4: - version "1.15.4" - resolved "https://registry.yarnpkg.com/re2/-/re2-1.15.4.tgz#2ffc3e4894fb60430393459978197648be01a0a9" - integrity sha512-7w3K+Daq/JjbX/dz5voMt7B9wlprVBQnMiypyCojAZ99kcAL+3LiJ5uBoX/u47l8eFTVq3Wj+V0pmvU+CT8tOg== +re2@1.17.4: + version "1.17.4" + resolved "https://registry.yarnpkg.com/re2/-/re2-1.17.4.tgz#7bf29290bdde963014e77bd2c2e799a6d788386e" + integrity sha512-xyZ4h5PqE8I9tAxTh3G0UttcK5ufrcUxReFjGzfX61vtanNbS1XZHjnwRSyPcLgChI4KLxVgOT/ioZXnUAdoTA== dependencies: - install-artifact-from-github "^1.0.2" - nan "^2.14.1" - node-gyp "^7.0.0" + install-artifact-from-github "^1.3.0" + nan "^2.15.0" + node-gyp "^8.4.1" react-ace@^7.0.5: version "7.0.5" @@ -15031,14 +14566,6 @@ reactcss@1.2.3: dependencies: lodash "^4.0.1" -read-all-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" - integrity sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po= - dependencies: - pinkie-promise "^2.0.0" - readable-stream "^2.0.0" - read-installed@~4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" @@ -15082,7 +14609,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -15140,13 +14667,6 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== - dependencies: - picomatch "^2.0.4" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -15300,13 +14820,6 @@ regexpu-core@^5.0.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.0.0" -registry-url@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" - integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= - dependencies: - rc "^1.0.1" - regjsgen@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" @@ -15451,13 +14964,6 @@ repeat-string@^1.5.4, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" - integrity sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw= - dependencies: - is-finite "^1.0.0" - replace-ext@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" @@ -15468,32 +14974,6 @@ replace-ext@^1.0.0: resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== -request@^2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - 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" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - 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 "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -15679,10 +15159,10 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry@0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== reusify@^1.0.4: version "1.0.4" @@ -15803,7 +15283,7 @@ safefs@^6.12.0: dependencies: graceful-fs "^4.2.6" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.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== @@ -15897,14 +15377,7 @@ selenium-webdriver@^4.0.0-alpha.7: rimraf "^2.7.1" tmp "0.0.30" -semver-diff@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" - integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= - dependencies: - semver "^5.0.3" - -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -15924,13 +15397,20 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@~7.3.0: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@~7.3.0: + 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" +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -15945,7 +15425,7 @@ serialize-javascript@^5.0.1: dependencies: randombytes "^2.1.0" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -16047,19 +15527,19 @@ side-channel@^1.0.3, side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: 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== -simple-git@^3.4.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.5.0.tgz#3c3538f4d7a1b3c8f3904412b12740bdcad9c8b1" - integrity sha512-fZsaq5nzdxQRhMNs6ESGLpMUHoL5GRP+boWPhq9pMYMKwOGZV2jHOxi8AbFFA2Y/6u4kR99HoULizSbpzaODkA== +simple-git@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.16.0.tgz#421773e24680f5716999cc4a1d60127b4b6a9dec" + integrity sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw== dependencies: "@kwsites/file-exists" "^1.1.1" "@kwsites/promise-deferred" "^1.1.1" - debug "^4.3.3" + debug "^4.3.4" simple-swizzle@^0.2.2: version "0.2.2" @@ -16124,11 +15604,16 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -slide@^1.1.5, slide@~1.1.3: +slide@~1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -16159,6 +15644,23 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + sonic-boom@^1.0.2: version "1.4.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" @@ -16396,21 +15898,6 @@ sql-summary@^1.0.1: resolved "https://registry.yarnpkg.com/sql-summary/-/sql-summary-1.0.1.tgz#a2dddb5435bae294eb11424a7330dc5bafe09c2b" integrity sha512-IpCr2tpnNkP3Jera4ncexsZUp0enJBLr+pHCyTweMUBrbJsTgQeLWx1FXLhoBj/MvcnUQpkgOn2EY8FKOkUzww== -sshpk@^1.7.0: - 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" - ssri@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" @@ -16426,7 +15913,7 @@ ssri@^7.0.0: figgy-pudding "^3.5.1" minipass "^3.1.1" -ssri@^8.0.1: +ssri@^8.0.0, ssri@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== @@ -16541,13 +16028,6 @@ string-argv@~0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-length@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" - integrity sha1-VpcPscOFWOnnC3KL894mmsRa36w= - dependencies: - strip-ansi "^3.0.0" - string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -16578,14 +16058,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -16595,7 +16067,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^3.0.0, string-width@^3.1.0: +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== @@ -16717,7 +16197,7 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -16746,11 +16226,6 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - 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" @@ -16763,12 +16238,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@2.0.1, strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -strip-json-comments@^3.0.1, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.0.1, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -16915,22 +16385,21 @@ success-symbol@^0.1.0: resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= -superagent@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.2.tgz#71393141edd086ccf2544a29a4a609e46b7911f3" - integrity sha512-o9/fP6dww7a4xmEF5a484o2rG34UUGo8ztDlv7vbCWuqPhpndMi0f7eXxdlryk5U12Kzy46nh8eNpLAJ93Alsg== +superagent@^8.0.5: + version "8.0.8" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.8.tgz#345f06f552dc23f3581f2c30fda7d6ad247f146d" + integrity sha512-OpxPrqqWKOjmuomLq5pCm4LWCSFdgAQ11XVkMl7t4ie13WxWuLkdJ83ZgG2jOQeLXKwMR2p9k30hLrKGAzkPaA== dependencies: component-emitter "^1.3.0" - cookiejar "^2.1.3" - debug "^4.3.3" + cookiejar "^2.1.4" + debug "^4.3.4" fast-safe-stringify "^2.1.1" form-data "^4.0.0" - formidable "^2.0.1" + formidable "^2.1.2" methods "^1.1.2" - mime "^2.5.0" - qs "^6.10.1" - readable-stream "^3.6.0" - semver "^7.3.5" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" supertest-as-promised@^4.0.2: version "4.0.2" @@ -16940,20 +16409,20 @@ supertest-as-promised@^4.0.2: bluebird "^3.3.1" methods "^1.1.1" -supertest@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.2.tgz#04a5998fd3efaff187cb69f07a169755d655b001" - integrity sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg== +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== dependencies: methods "^1.1.2" - superagent "^7.1.0" + superagent "^8.0.5" -supports-color@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" - integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== +supports-color@8.1.1, supports-color@^8.0.0: + 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 "^3.0.0" + has-flag "^4.0.0" supports-color@^0.2.0: version "0.2.0" @@ -16979,13 +16448,6 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: - 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" - supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" @@ -17098,7 +16560,7 @@ tar-stream@^2.1.4, tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@6.1.11, tar@^6.0.2, tar@^6.1.11: +tar@6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -17110,6 +16572,18 @@ tar@6.1.11, tar@^6.0.2, tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.1.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" + integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^4.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tcp-port-used@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea" @@ -17245,11 +16719,6 @@ through2@^3.0.1: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -timed-out@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" - integrity sha1-84sK6B03R9YoAB9B2vxlKs5nHAo= - timers-browserify@^2.0.4: version "2.0.12" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" @@ -17421,14 +16890,6 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" -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" - tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -17532,10 +16993,10 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== +tslib@~2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== tsutils@^3.17.1, tsutils@^3.21.0: version "3.21.0" @@ -17549,18 +17010,6 @@ tty-browserify@0.0.0: resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - 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 sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -17899,32 +17348,6 @@ unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: unist-util-is "^4.0.0" unist-util-visit-parents "^3.0.0" -universal-user-agent@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.1.0.tgz#5abfbcc036a1ba490cb941f8fd68c46d3669e8e4" - integrity sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q== - dependencies: - os-name "^3.0.0" - -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== - dependencies: - os-name "^3.1.0" - -universal-user-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" - integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q== - dependencies: - os-name "^3.1.0" - -universal-user-agent@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" - integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== - universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -17950,19 +17373,6 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-notifier@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" - integrity sha1-B7XcIGazYnqztPUwEw9+3doHpMw= - dependencies: - chalk "^1.0.0" - configstore "^1.0.0" - is-npm "^1.0.0" - latest-version "^1.0.0" - repeating "^1.1.2" - semver-diff "^2.0.0" - string-length "^1.0.0" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -17992,11 +17402,6 @@ url-parse@^1.5.9: querystringify "^2.1.1" requires-port "^1.0.0" -url-template@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" - integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= - url@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" @@ -18094,21 +17499,21 @@ uuid@8.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== -uuid@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" - integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= - uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0, uuid@^8.3.2: +uuid@^8.3.0: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -18161,385 +17566,359 @@ value-or-function@^3.0.0: resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= -vega-canvas@^1.2.5, vega-canvas@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/vega-canvas/-/vega-canvas-1.2.6.tgz#55e032ce9a62acd17229f6bac66d99db3d6879cd" - integrity sha512-rgeYUpslYn/amIfnuv3Sw6n4BGns94OjjZNtUc9IDji6b+K8LGS/kW+Lvay8JX/oFqtulBp8RLcHN6QjqPLA9Q== +vega-canvas@^1.2.6, vega-canvas@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/vega-canvas/-/vega-canvas-1.2.7.tgz#cf62169518f5dcd91d24ad352998c2248f8974fb" + integrity sha512-OkJ9CACVcN9R5Pi9uF6MZBF06pO6qFpDYHWSKBJsdHP5o724KrsgR6UvbnXFH82FdsiTOff/HqjuaG8C7FL+9Q== -vega-crossfilter@~4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vega-crossfilter/-/vega-crossfilter-4.1.0.tgz#b6c5a728ce987f2514074adb22cf86b9bc63e0c8" - integrity sha512-aiOJcvVpiEDIu5uNc4Kf1hakkkPaVOO5fw5T4RSFAw6GEDbdqcB6eZ1xePcsLVic1hxYD5SGiUPdiiIs0SMh2g== +vega-crossfilter@~4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vega-crossfilter/-/vega-crossfilter-4.1.1.tgz#3ff3ca0574883706f7a399dc6d60f4a0f065ece4" + integrity sha512-yesvlMcwRwxrtAd9IYjuxWJJuAMI0sl7JvAFfYtuDkkGDtqfLXUcCzHIATqW6igVIE7tWwGxnbfvQLhLNgK44Q== dependencies: - d3-array "^3.1.1" - vega-dataflow "^5.7.3" - vega-util "^1.15.2" + d3-array "^3.2.2" + vega-dataflow "^5.7.5" + vega-util "^1.17.1" -vega-dataflow@^5.7.3, vega-dataflow@^5.7.4, vega-dataflow@~5.7.4: - version "5.7.4" - resolved "https://registry.yarnpkg.com/vega-dataflow/-/vega-dataflow-5.7.4.tgz#7cafc0a41b9d0b11dd2e34a513f8b7ca345dfd74" - integrity sha512-JGHTpUo8XGETH3b1V892we6hdjzCWB977ybycIu8DPqRoyrZuj6t1fCVImazfMgQD1LAfJlQybWP+alwKDpKig== +vega-dataflow@^5.7.3, vega-dataflow@^5.7.5, vega-dataflow@~5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/vega-dataflow/-/vega-dataflow-5.7.5.tgz#0d559f3c3a968831f2995e099a2e270993ddfed9" + integrity sha512-EdsIl6gouH67+8B0f22Owr2tKDiMPNNR8lEvJDcxmFw02nXd8juimclpLvjPQriqn6ta+3Dn5txqfD117H04YA== dependencies: - vega-format "^1.0.4" - vega-loader "^4.3.2" - vega-util "^1.16.1" + vega-format "^1.1.1" + vega-loader "^4.5.1" + vega-util "^1.17.1" -vega-encode@~4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/vega-encode/-/vega-encode-4.9.0.tgz#3dd1031056bb8029f262afc4b4d58372c8f073a6" - integrity sha512-etv2BHuCn9bzEc0cxyA2TnbtcAFQGVFmsaqmB4sgBCaqTSEfXMoX68LK3yxBrsdm5LU+y3otJVoewi3qWYCx2g== +vega-encode@~4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/vega-encode/-/vega-encode-4.9.1.tgz#bad0e99bebec86d42184bcb898576c8accd91e89" + integrity sha512-05JB47UZaqIBS9t6rtHI/aKjEuH4EsSIH+wJWItht4BFj33eIl4XRNtlXdE31uuQT2pXWz5ZWW3KboMuaFzKLw== dependencies: - d3-array "^3.1.1" + d3-array "^3.2.2" d3-interpolate "^3.0.1" - vega-dataflow "^5.7.3" - vega-scale "^7.0.3" - vega-util "^1.15.2" + vega-dataflow "^5.7.5" + vega-scale "^7.3.0" + vega-util "^1.17.1" -vega-event-selector@^3.0.0, vega-event-selector@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-3.0.0.tgz#7b855ac0c3ddb59bc5b5caa0d96dbbc9fbd33a4c" - integrity sha512-Gls93/+7tEJGE3kUuUnxrBIxtvaNeF01VIFB2Q2Of2hBIBvtHX74jcAdDtkh5UhhoYGD8Q1J30P5cqEBEwtPoQ== - -vega-event-selector@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" - integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== - -vega-expression@^5.0.0, vega-expression@~5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-5.0.0.tgz#938f26689693a1e0d26716030cdaed43ca7abdfb" - integrity sha512-y5+c2frq0tGwJ7vYXzZcfVcIRF/QGfhf2e+bV1Z0iQs+M2lI1II1GPDdmOcMKimpoCVp/D61KUJDIGE1DSmk2w== - dependencies: - "@types/estree" "^0.0.50" - vega-util "^1.16.0" - -vega-expression@~3.0.0: +vega-event-selector@^3.0.1, vega-event-selector@~3.0.0, vega-event-selector@~3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-3.0.1.tgz#bbccd8f59371a537eab16f3d9eff5cbeaa27532d" - integrity sha512-+UwOFEkBnAWo8Zud6i8O4Pd2W6QqmPUOaAhjNtj0OxRL+d+Duoy7M4edUDZ+YuoUcMnjjBFfDQu7oRAA1fIMEQ== + resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-3.0.1.tgz#b99e92147b338158f8079d81b28b2e7199c2e259" + integrity sha512-K5zd7s5tjr1LiOOkjGpcVls8GsH/f2CWCrWcpKy74gTCp+llCdwz0Enqo013ZlGaRNjfgD/o1caJRt3GSaec4A== + +vega-expression@^5.0.1, vega-expression@~5.0.0, vega-expression@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-5.0.1.tgz#e6a6eff564d2a93496a9bf34cbc78d8942f236a8" + integrity sha512-atfzrMekrcsuyUgZCMklI5ki8cV763aeo1Y6YrfYU7FBwcQEoFhIV/KAJ1vae51aPDGtfzvwbtVIo3WShFCP2Q== dependencies: - vega-util "^1.15.2" + "@types/estree" "^1.0.0" + vega-util "^1.17.1" -vega-force@~4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.1.0.tgz#cc8dea972baa52adc60840ff744ebb9e57d8f1f5" - integrity sha512-Sssf8iH48vYlz+E7/RpU+SUaJbuLoIL87U4tG2Av4gf/hRiImU49x2TI3EuhFWg1zpaCFxlz0CAaX++Oh/gjdw== +vega-force@~4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.1.1.tgz#27bffa96bda293f27d2a2492c2cbf99d49fec77e" + integrity sha512-T6fJAUz9zdXf2qj2Hz0VlmdtaY7eZfcKNazhUV8hza4R3F9ug6r/hSrdovfc9ExmbUjL5iyvDUsf63r8K3/wVQ== dependencies: d3-force "^3.0.0" - vega-dataflow "^5.7.3" - vega-util "^1.15.2" + vega-dataflow "^5.7.5" + vega-util "^1.17.1" -vega-format@^1.0.4, vega-format@^1.1.0, vega-format@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vega-format/-/vega-format-1.1.0.tgz#b9d81cf1bcf222ae5cbd94357ae70245d2c7b18d" - integrity sha512-6mgpeWw8yGdG0Zdi8aVkx5oUrpJGOpNxqazC2858RSDPvChM/jDFlgRMTYw52qk7cxU0L08ARp4BwmXaI75j0w== +vega-format@^1.1.1, vega-format@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vega-format/-/vega-format-1.1.1.tgz#92e4876e18064e7ad54f39045f7b24dede0030b8" + integrity sha512-Rll7YgpYbsgaAa54AmtEWrxaJqgOh5fXlvM2wewO4trb9vwM53KBv4Q/uBWCLK3LLGeBXIF6gjDt2LFuJAUtkQ== dependencies: - d3-array "^3.1.1" + d3-array "^3.2.2" d3-format "^3.1.0" d3-time-format "^4.1.0" - vega-time "^2.0.3" - vega-util "^1.15.2" - -vega-functions@^5.12.1, vega-functions@^5.13.0, vega-functions@~5.13.0: - version "5.13.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.13.0.tgz#c9ab8c6eedbf39f75b424cca6776b1d0b8c74b32" - integrity sha512-Mf53zNyx+c9fFqagEI0T8zc9nMlx0zozOngr8oOpG1tZDKOgwOnUgN99zQKbLHjyv+UzWrq3LYTnSLyVe0ZmhQ== - dependencies: - d3-array "^3.1.1" - d3-color "^3.0.1" - d3-geo "^3.0.1" - vega-dataflow "^5.7.3" - vega-expression "^5.0.0" - vega-scale "^7.2.0" - vega-scenegraph "^4.9.3" - vega-selections "^5.3.1" - vega-statistics "^1.7.9" - vega-time "^2.1.0" - vega-util "^1.16.0" - -vega-geo@~4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/vega-geo/-/vega-geo-4.4.0.tgz#ce792df57f8ca4c54a7a1a29467cfa24bc53f573" - integrity sha512-3YX41y+J5pu0PMjvBCASg0/lgvu9+QXWJZ+vl6FFKa8AlsIopQ67ZL7ObwqjZcoZMolJ4q0rc+ZO8aj1pXCYcw== - dependencies: - d3-array "^3.1.1" - d3-color "^3.0.1" - d3-geo "^3.0.1" - vega-canvas "^1.2.5" - vega-dataflow "^5.7.3" - vega-projection "^1.4.5" - vega-statistics "^1.7.9" - vega-util "^1.15.2" - -vega-hierarchy@~4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vega-hierarchy/-/vega-hierarchy-4.1.0.tgz#605edbe3a6232853f9e8b57e3b905471d33b1a48" - integrity sha512-DWBK39IEt4FiQru12twzKSFUvFFZ7KtlH9+lAaqrJnKuIZFCyQ1XOUfKScfbKIlk4KS+DuCTNLI/pxC/f7Sk9Q== + vega-time "^2.1.1" + vega-util "^1.17.1" + +vega-functions@^5.13.1, vega-functions@~5.13.1: + version "5.13.1" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.13.1.tgz#504d672924495fe3ea844e6940c7f6e151cde151" + integrity sha512-0LhntimnvBl4VzRO/nkCwCTbtaP8bE552galKQbCg88GDxdmcmlsoTCwUzG0vZ/qmNM3IbqnP5k5/um3zwFqLw== + dependencies: + d3-array "^3.2.2" + d3-color "^3.1.0" + d3-geo "^3.1.0" + vega-dataflow "^5.7.5" + vega-expression "^5.0.1" + vega-scale "^7.3.0" + vega-scenegraph "^4.10.2" + vega-selections "^5.4.1" + vega-statistics "^1.8.1" + vega-time "^2.1.1" + vega-util "^1.17.1" + +vega-geo@~4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/vega-geo/-/vega-geo-4.4.1.tgz#3850232bf28c98fab5e26c5fb401acb6fb37b5e5" + integrity sha512-s4WeZAL5M3ZUV27/eqSD3v0FyJz3PlP31XNSLFy4AJXHxHUeXT3qLiDHoVQnW5Om+uBCPDtTT1ROx1smGIf2aA== + dependencies: + d3-array "^3.2.2" + d3-color "^3.1.0" + d3-geo "^3.1.0" + vega-canvas "^1.2.7" + vega-dataflow "^5.7.5" + vega-projection "^1.6.0" + vega-statistics "^1.8.1" + vega-util "^1.17.1" + +vega-hierarchy@~4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vega-hierarchy/-/vega-hierarchy-4.1.1.tgz#897974a477dfa70cc0d4efab9465b6cc79a9071f" + integrity sha512-h5mbrDtPKHBBQ9TYbvEb/bCqmGTlUX97+4CENkyH21tJs7naza319B15KRK0NWOHuhbGhFmF8T0696tg+2c8XQ== dependencies: - d3-hierarchy "^3.1.0" - vega-dataflow "^5.7.3" - vega-util "^1.15.2" + d3-hierarchy "^3.1.2" + vega-dataflow "^5.7.5" + vega-util "^1.17.1" "vega-interpreter@npm:@amoo-miki/vega-forced-csp-compliant-interpreter@1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@amoo-miki/vega-forced-csp-compliant-interpreter/-/vega-forced-csp-compliant-interpreter-1.0.6.tgz#5cffdf12b7fe12dc936194edd9e8519506c38716" integrity sha512-9S5nTTVd8JVKobcWp5iwirIeePiamwH1J9uSZPuG5kcF0TUBvGu++ERKjNdst5Qck7e4R6/7vjx2wVf58XUarg== -vega-label@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.2.0.tgz#bcb2659aec54f890f9debab3e41ab87a58292dce" - integrity sha512-1prOqkCAfXaUvMqavbGI0nbYGqV8UQR9qvuVwrPJ6Yxm3GIUIOA/JRqNY8eZR8USwMP/kzsqlfVEixj9+Y75VQ== +vega-label@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.2.1.tgz#ea45fa5a407991c44edfea9c4ca40874d544a3db" + integrity sha512-n/ackJ5lc0Xs9PInCaGumYn2awomPjJ87EMVT47xNgk2bHmJoZV1Ve/1PUM6Eh/KauY211wPMrNp/9Im+7Ripg== dependencies: vega-canvas "^1.2.6" vega-dataflow "^5.7.3" vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-lite@^4.16.8: - version "4.17.0" - resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-4.17.0.tgz#01ad4535e92f28c3852c1071711de272ddfb4631" - integrity sha512-MO2XsaVZqx6iWWmVA5vwYFamvhRUsKfVp7n0pNlkZ2/21cuxelSl92EePZ2YGmzL6z4/3K7r/45zaG8p+qNHeg== +vega-lite@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.6.0.tgz#0f0adfc8b86f5eea071df186b2877d828c870c11" + integrity sha512-aTjQk//SzL9ctHY4ItA8yZSGflHMWPJmCXEs8LeRlixuOaAbamZmeL8xNMbQpS/vAZQeFAqjcJ32Fuztz/oGww== dependencies: - "@types/clone" "~2.1.0" - "@types/fast-json-stable-stringify" "^2.0.0" - array-flat-polyfill "^1.0.1" + "@types/clone" "~2.1.1" clone "~2.1.2" fast-deep-equal "~3.1.3" fast-json-stable-stringify "~2.1.0" - json-stringify-pretty-compact "~2.0.0" - tslib "~2.0.3" - vega-event-selector "~2.0.6" - vega-expression "~3.0.0" - vega-util "~1.16.0" - yargs "~16.0.3" - -vega-loader@^4.3.2, vega-loader@^4.4.0, vega-loader@~4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.5.0.tgz#b15acc4c8f84191f500e94d35ddfb9721ac943ad" - integrity sha512-EkAyzbx0pCYxH3v3wghGVCaKINWxHfgbQ2pYDiYv0yo8e04S8Mv/IlRGTt6BAe7cLhrk1WZ4zh20QOppnGG05w== + json-stringify-pretty-compact "~3.0.0" + tslib "~2.4.0" + vega-event-selector "~3.0.0" + vega-expression "~5.0.0" + vega-util "~1.17.0" + yargs "~17.6.0" + +vega-loader@^4.5.1, vega-loader@~4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.5.1.tgz#b85262b3cb8376487db0c014a8a13c3a5e6d52ad" + integrity sha512-qy5x32SaT0YkEujQM2yKqvLGV9XWQ2aEDSugBFTdYzu/1u4bxdUSRDREOlrJ9Km3RWIOgFiCkobPmFxo47SKuA== dependencies: d3-dsv "^3.0.1" node-fetch "^2.6.7" topojson-client "^3.1.0" - vega-format "^1.1.0" - vega-util "^1.16.0" + vega-format "^1.1.1" + vega-util "^1.17.1" -vega-parser@~6.1.4: - version "6.1.4" - resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.4.tgz#4868e41af2c9645b6d7daeeb205cfad06b9d465c" - integrity sha512-tORdpWXiH/kkXcpNdbSVEvtaxBuuDtgYp9rBunVW9oLsjFvFXbSWlM1wvJ9ZFSaTfx6CqyTyGMiJemmr1QnTjQ== +vega-parser@~6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.2.0.tgz#c982aff0a6409486cbbe743a5799412b8b897654" + integrity sha512-as+QnX8Qxe9q51L1C2sVBd+YYYctP848+zEvkBT2jlI2g30aZ6Uv7sKsq7QTL6DUbhXQKR0XQtzlanckSFdaOQ== dependencies: - vega-dataflow "^5.7.3" - vega-event-selector "^3.0.0" - vega-functions "^5.12.1" - vega-scale "^7.1.1" - vega-util "^1.16.0" + vega-dataflow "^5.7.5" + vega-event-selector "^3.0.1" + vega-functions "^5.13.1" + vega-scale "^7.3.0" + vega-util "^1.17.1" -vega-projection@^1.4.5, vega-projection@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/vega-projection/-/vega-projection-1.5.0.tgz#51c5f0455170cd35b3c5f3e653e99c3616520640" - integrity sha512-aob7qojh555x3hQWZ/tr8cIJNSWQbm6EoWTJaheZgFOY2x3cDa4Qrg3RJbGw6KwVj/IQk2p40paRzixKZ2kr+A== +vega-projection@^1.6.0, vega-projection@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vega-projection/-/vega-projection-1.6.0.tgz#921acd3220e7d9d04ccd5ce0109433afb3236966" + integrity sha512-LGUaO/kpOEYuTlul+x+lBzyuL9qmMwP1yShdUWYLW+zXoeyGbs5OZW+NbPPwLYqJr5lpXDr/vGztFuA/6g2xvQ== dependencies: - d3-geo "^3.0.1" + d3-geo "^3.1.0" d3-geo-projection "^4.0.0" + vega-scale "^7.3.0" -vega-regression@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vega-regression/-/vega-regression-1.1.0.tgz#b4394db403ada93de52bb4536d04be336c983a8c" - integrity sha512-09K0RemY6cdaXBAyakDUNFfEkRcLkGjkDJyWQPAUqGK59hV2J+G3i4uxkZp18Vu0t8oqU7CgzwWim1s5uEpOcA== +vega-regression@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vega-regression/-/vega-regression-1.1.1.tgz#b53a964152a2fec4847e31571f522bfda23089af" + integrity sha512-98i/z0vdDhOIEhJUdYoJ2nlfVdaHVp2CKB39Qa7G/XyRw0+QwDFFrp8ZRec2xHjHfb6bYLGNeh1pOsC13FelJg== dependencies: - d3-array "^3.1.1" + d3-array "^3.2.2" vega-dataflow "^5.7.3" vega-statistics "^1.7.9" vega-util "^1.15.2" -vega-runtime@^6.1.3, vega-runtime@~6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/vega-runtime/-/vega-runtime-6.1.3.tgz#01e18246f7a80cee034a96017ac30113b92c4034" - integrity sha512-gE+sO2IfxMUpV0RkFeQVnHdmPy3K7LjHakISZgUGsDI/ZFs9y+HhBf8KTGSL5pcZPtQsZh3GBQ0UonqL1mp9PA== +vega-runtime@^6.1.4, vega-runtime@~6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/vega-runtime/-/vega-runtime-6.1.4.tgz#98b67160cea9554e690bfd44719f9d17f90c4220" + integrity sha512-0dDYXyFLQcxPQ2OQU0WuBVYLRZnm+/CwVu6i6N4idS7R9VXIX5581EkCh3pZ20pQ/+oaA7oJ0pR9rJgJ6rukRQ== dependencies: - vega-dataflow "^5.7.3" - vega-util "^1.15.2" + vega-dataflow "^5.7.5" + vega-util "^1.17.1" -vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@^7.2.0, vega-scale@~7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/vega-scale/-/vega-scale-7.2.0.tgz#9e298cc02ad340498cb56847436b19439911f0fc" - integrity sha512-QYltO/otrZHLrCGGf06Y99XtPtqWXITr6rw7rO9oL+l3d9o5RFl9sjHrVxiM7v+vGoZVWbBd5IPbFhPsXZ6+TA== +vega-scale@^7.3.0, vega-scale@~7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/vega-scale/-/vega-scale-7.3.0.tgz#02b83435a892c6d91a87ee7d3d350fac987f464b" + integrity sha512-pMOAI2h+e1z7lsqKG+gMfR6NKN2sTcyjZbdJwntooW0uFHwjLGjMSY7kSd3nSEquF0HQ8qF7zR6gs1eRwlGimw== dependencies: - d3-array "^3.1.1" + d3-array "^3.2.2" d3-interpolate "^3.0.1" d3-scale "^4.0.2" - vega-time "^2.1.0" - vega-util "^1.17.0" + vega-time "^2.1.1" + vega-util "^1.17.1" -vega-scenegraph@^4.10.0, vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.10.1.tgz#944da67b8a28758fab2e1306259fb7ff6be89f6b" - integrity sha512-takIpkmNxYHhJYALOYzhTin3EDzbys6U4g+l1yJZVlXG9YTdiCMuEVAdtaQOCqF9/7qytD6pCrMxJY2HaoN0qQ== - dependencies: - d3-path "^3.0.1" - d3-shape "^3.1.0" - vega-canvas "^1.2.5" - vega-loader "^4.4.0" - vega-scale "^7.2.0" - vega-util "^1.15.2" +vega-scenegraph@^4.10.2, vega-scenegraph@^4.9.2, vega-scenegraph@~4.10.2: + version "4.10.2" + resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.10.2.tgz#3ae9ad8e99bbf75e2a4f3ebf2c1f9dee7562d245" + integrity sha512-R8m6voDZO5+etwNMcXf45afVM3XAtokMqxuDyddRl9l1YqSJfS+3u8hpolJ50c2q6ZN20BQiJwKT1o0bB7vKkA== + dependencies: + d3-path "^3.1.0" + d3-shape "^3.2.0" + vega-canvas "^1.2.7" + vega-loader "^4.5.1" + vega-scale "^7.3.0" + vega-util "^1.17.1" vega-schema-url-parser@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.2.0.tgz#a0d1e02915adfbfcb1fd517c8c2ebe2419985c1e" integrity sha512-yAtdBnfYOhECv9YC70H2gEiqfIbVkq09aaE4y/9V/ovEFmH9gPKaEgzIZqgT7PSPQjKhsNkb6jk6XvSoboxOBw== -vega-selections@^5.3.1: - version "5.4.0" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.4.0.tgz#c2783897421fa39b674c015fa8f15a0023b8054e" - integrity sha512-Un3JdLDPjIpF9Dh4sw6m1c/QAcfam6m1YXHJ9vJxE/GdJ+sOrPxc7bcEU8VhOmTUN7IQUn4/1ry4JqqOVMbEhw== +vega-selections@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.4.1.tgz#3233acb920703bfc323df8b960aa52e55ac08c70" + integrity sha512-EtYc4DvA+wXqBg9tq+kDomSoVUPCmQfS7hUxy2qskXEed79YTimt3Hcl1e1fW226I4AVDBEqTTKebmKMzbSgAA== dependencies: - d3-array "3.1.1" - vega-expression "^5.0.0" - vega-util "^1.16.0" + d3-array "3.2.2" + vega-expression "^5.0.1" + vega-util "^1.17.1" vega-spec-injector@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/vega-spec-injector/-/vega-spec-injector-0.0.2.tgz#f1d990109dd9d845c524738f818baa4b72a60ca6" integrity sha512-wOMMqmpssn0/ZFPW7wl1v26vbseRX7zHPWzEyS9TwNXTRCu1TcjIBIR+X23lCWocxhoBqFxmqyn8UowMhlGtAg== -vega-statistics@^1.7.9, vega-statistics@^1.8.0, vega-statistics@~1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.8.0.tgz#ad66f7461473d58bc96671588981a059ffd60b59" - integrity sha512-dl+LCRS6qS4jWDme/NEdPVt5r649uB4IK6Kyr2/czmGA5JqjuFmtQ9lHQOnRu8945XLkqLf+JIQQo7vnw+nslA== - dependencies: - d3-array "^3.1.1" - -vega-time@^2.0.3, vega-time@^2.1.0, vega-time@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/vega-time/-/vega-time-2.1.0.tgz#acfbab88d7798b87ff63913b0dce2ca5eb0d46ca" - integrity sha512-Q9/l3S6Br1RPX5HZvyLD/cQ4K6K8DtpR09/1y7D66gxNorg2+HGzYZINH9nUvN3mxoXcBWg4cCUh3+JvmkDaEg== - dependencies: - d3-array "^3.1.1" - d3-time "^3.0.0" - vega-util "^1.15.2" - -vega-tooltip@^0.24.2: - version "0.24.2" - resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.24.2.tgz#da55a171a96ea48a8ff135a728622e1cbb1152af" - integrity sha512-b7IeYQl/piNVsMmTliOgTnwSOhBs67KqoZ9UzP1I3XpH7TKbSuc3YHA7b1CSxkRR0hHKdradby4UI8c9rdH74w== - dependencies: - vega-util "^1.15.2" - -vega-transforms@~4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.10.0.tgz#a1017ede13cf4e25499f588610a3be4da615d82d" - integrity sha512-Yk6ByzVq5F2niFfPlSsrU5wi+NZhsF7IBpJCcTfms4U7eoyNepUXagdFEJ3VWBD/Lit6GorLXFgO17NYcyS5gg== +vega-statistics@^1.7.9, vega-statistics@^1.8.1, vega-statistics@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.8.1.tgz#596fc3713ac68cc649bf28d0faf7def5ef34fef6" + integrity sha512-eRR3LZBusnTXUkc/uunAvWi1PjCJK+Ba4vFvEISc5Iv5xF4Aw2cBhEz1obEt6ID5fGVCTAl0E1LOSFxubS89hQ== dependencies: - d3-array "^3.1.1" - vega-dataflow "^5.7.4" - vega-statistics "^1.8.0" - vega-time "^2.1.0" - vega-util "^1.16.1" + d3-array "^3.2.2" -vega-typings@~0.22.0: - version "0.22.2" - resolved "https://registry.yarnpkg.com/vega-typings/-/vega-typings-0.22.2.tgz#c5f5037680778664f5246c19a56e4cfffeb0d90b" - integrity sha512-op4bNiyS150V7gnuIdjwFYB1belYB8qnapqLQQ+ZBJQ+r7a+IbvXphEUf8AkBlOoGPN1ITNdhlIsq9WWiuxu8Q== +vega-time@^2.1.1, vega-time@~2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/vega-time/-/vega-time-2.1.1.tgz#0f1fb4e220dd5ed57401b58fb2293241f049ada0" + integrity sha512-z1qbgyX0Af2kQSGFbApwBbX2meenGvsoX8Nga8uyWN8VIbiySo/xqizz1KrP6NbB6R+x5egKmkjdnyNThPeEWA== dependencies: - vega-event-selector "^3.0.0" - vega-expression "^5.0.0" - vega-util "^1.15.2" - -vega-util@^1.15.2, vega-util@^1.16.0, vega-util@^1.16.1, vega-util@^1.17.0, vega-util@~1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.17.0.tgz#b72ae0baa97f943bf591f8f5bb27ceadf06834ac" - integrity sha512-HTaydZd9De3yf+8jH66zL4dXJ1d1p5OIFyoBzFiOli4IJbwkL1jrefCKz6AHDm1kYBzDJ0X4bN+CzZSCTvNk1w== - -vega-util@~1.16.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.16.1.tgz#992bf3c3b6e145797214d99862841baea417ba39" - integrity sha512-FdgD72fmZMPJE99FxvFXth0IL4BbLA93WmBg/lvcJmfkK4Uf90WIlvGwaIUdSePIsdpkZjBPyQcHMQ8OcS8Smg== + d3-array "^3.2.2" + d3-time "^3.1.0" + vega-util "^1.17.1" -vega-view-transforms@~4.5.8: - version "4.5.8" - resolved "https://registry.yarnpkg.com/vega-view-transforms/-/vega-view-transforms-4.5.8.tgz#c8dc42c3c7d4aa725d40b8775180c9f23bc98f4e" - integrity sha512-966m7zbzvItBL8rwmF2nKG14rBp7q+3sLCKWeMSUrxoG+M15Smg5gWEGgwTG3A/RwzrZ7rDX5M1sRaAngRH25g== +vega-tooltip@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.30.0.tgz#b8a48a0d1be717b7410cf75021aaaff75818b212" + integrity sha512-dBuqp1HgNvxrc3MU4KAE3gU7AiD0AvCiyu7IMwubI6TQa0l9A5c+B+ZLjDZP2Ool0J9eAaGgVhqjXWaUjUAfAQ== dependencies: - vega-dataflow "^5.7.3" - vega-scenegraph "^4.9.2" - vega-util "^1.15.2" + vega-util "^1.17.0" -vega-view@~5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.11.0.tgz#8a7b29a36776e43cc6599e087ed7f48a918b805d" - integrity sha512-MI9NTRFmtFX6ADk6KOHhi8bhHjC9pPm42Bj2+74c6l1d3NQZf9Jv7lkiGqKohdkQDNH9LPwz/6slhKwPU9JdkQ== - dependencies: - d3-array "^3.1.1" +vega-transforms@~4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.10.1.tgz#5e51f4f3a745d43609e0d8ba1d74a7e53014030a" + integrity sha512-0uWrUZaYl8kjWrGbvPOQSKk6kcNXQFY9moME+bUmkADAvFptphCGbaEIn/nSsG6uCxj8E3rqKmKfjSWdU5yOqA== + dependencies: + d3-array "^3.2.2" + vega-dataflow "^5.7.5" + vega-statistics "^1.8.1" + vega-time "^2.1.1" + vega-util "^1.17.1" + +vega-typings@~0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/vega-typings/-/vega-typings-0.23.0.tgz#5b001f5b51a477e67d2446ef9b964e1dac48a20e" + integrity sha512-10ZRRGoUZoQLS5jMiIFhSZMDc3UkPhDP2VMUN/oHZXElvPCGjfjvgmiC6XzvvN4sRXdccMcZX1lZPoyYPERVkA== + dependencies: + "@types/geojson" "^7946.0.10" + vega-event-selector "^3.0.1" + vega-expression "^5.0.1" + vega-util "^1.17.1" + +vega-util@^1.15.2, vega-util@^1.17.0, vega-util@^1.17.1, vega-util@~1.17.0, vega-util@~1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.17.1.tgz#717865fc6b660ceb3ae16273d21166ed471c2db4" + integrity sha512-ToPkWoBdP6awoK+bnYaFhgdqZhsNwKxWbuMnFell+4K/Cb6Q1st5Pi9I7iI5Y6n5ZICDDsd6eL7/IhBjEg1NUQ== + +vega-view-transforms@~4.5.9: + version "4.5.9" + resolved "https://registry.yarnpkg.com/vega-view-transforms/-/vega-view-transforms-4.5.9.tgz#5f109555c08ee9ac23ff9183d578eb9cbac6fe61" + integrity sha512-NxEq4ZD4QwWGRrl2yDLnBRXM9FgCI+vvYb3ZC2+nVDtkUxOlEIKZsMMw31op5GZpfClWLbjCT3mVvzO2xaTF+g== + dependencies: + vega-dataflow "^5.7.5" + vega-scenegraph "^4.10.2" + vega-util "^1.17.1" + +vega-view@~5.11.1: + version "5.11.1" + resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.11.1.tgz#a703d7d6344489c6a6e9e9d9c7a732519bf4432c" + integrity sha512-RoWxuoEMI7xVQJhPqNeLEHCezudsf3QkVMhH5tCovBqwBADQGqq9iWyax3ZzdyX1+P3eBgm7cnLvpqtN2hU8kA== + dependencies: + d3-array "^3.2.2" d3-timer "^3.0.1" - vega-dataflow "^5.7.3" - vega-format "^1.1.0" - vega-functions "^5.13.0" - vega-runtime "^6.1.3" - vega-scenegraph "^4.10.0" - vega-util "^1.16.1" - -vega-voronoi@~4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/vega-voronoi/-/vega-voronoi-4.2.0.tgz#14c74c84f52d9a16be2facd1bede879d32d988f2" - integrity sha512-1iuNAVZgUHRlBpdq4gSga3KlQmrgFfwy+KpyDgPLQ8HbLkhcVeT7RDh2L6naluqD7Op0xVLms3clR920WsYryQ== + vega-dataflow "^5.7.5" + vega-format "^1.1.1" + vega-functions "^5.13.1" + vega-runtime "^6.1.4" + vega-scenegraph "^4.10.2" + vega-util "^1.17.1" + +vega-voronoi@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/vega-voronoi/-/vega-voronoi-4.2.1.tgz#521a22d3d4c545fe1d5eea19eac0fd3ac5e58b1b" + integrity sha512-zzi+fxU/SBad4irdLLsG3yhZgXWZezraGYVQfZFWe8kl7W/EHUk+Eqk/eetn4bDeJ6ltQskX+UXH3OP5Vh0Q0Q== dependencies: d3-delaunay "^6.0.2" - vega-dataflow "^5.7.3" - vega-util "^1.15.2" - -vega-wordcloud@~4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/vega-wordcloud/-/vega-wordcloud-4.1.3.tgz#ce90900333f4e0d3ee706ba4f36bb0905f8b4a9f" - integrity sha512-is4zYn9FMAyp9T4SAcz2P/U/wqc0Lx3P5YtpWKCbOH02a05vHjUQrQ2TTPOuvmMfAEDCSKvbMSQIJMOE018lJA== - dependencies: - vega-canvas "^1.2.5" - vega-dataflow "^5.7.3" - vega-scale "^7.1.1" - vega-statistics "^1.7.9" - vega-util "^1.15.2" - -vega@^5.17.3: - version "5.22.1" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.22.1.tgz#e028f3645de18e0070317bc04410282975549e1e" - integrity sha512-KJBI7OWSzpfCPbmWl3GQCqBqbf2TIdpWS0mzO6MmWbvdMhWHf74P9IVnx1B1mhg0ZTqWFualx9ZYhWzMMwudaQ== - dependencies: - vega-crossfilter "~4.1.0" - vega-dataflow "~5.7.4" - vega-encode "~4.9.0" - vega-event-selector "~3.0.0" - vega-expression "~5.0.0" - vega-force "~4.1.0" - vega-format "~1.1.0" - vega-functions "~5.13.0" - vega-geo "~4.4.0" - vega-hierarchy "~4.1.0" - vega-label "~1.2.0" - vega-loader "~4.5.0" - vega-parser "~6.1.4" - vega-projection "~1.5.0" - vega-regression "~1.1.0" - vega-runtime "~6.1.3" - vega-scale "~7.2.0" - vega-scenegraph "~4.10.1" - vega-statistics "~1.8.0" - vega-time "~2.1.0" - vega-transforms "~4.10.0" - vega-typings "~0.22.0" - vega-util "~1.17.0" - vega-view "~5.11.0" - vega-view-transforms "~4.5.8" - vega-voronoi "~4.2.0" - vega-wordcloud "~4.1.3" + vega-dataflow "^5.7.5" + vega-util "^1.17.1" -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" +vega-wordcloud@~4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/vega-wordcloud/-/vega-wordcloud-4.1.4.tgz#38584cf47ef52325d6a8dc38908b5d2378cc6e62" + integrity sha512-oeZLlnjiusLAU5vhk0IIdT5QEiJE0x6cYoGNq1th+EbwgQp153t4r026fcib9oq15glHFOzf81a8hHXHSJm1Jw== + dependencies: + vega-canvas "^1.2.7" + vega-dataflow "^5.7.5" + vega-scale "^7.3.0" + vega-statistics "^1.8.1" + vega-util "^1.17.1" + +vega@^5.23.0: + version "5.23.0" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.23.0.tgz#7e3899b65f1a84095545b74dcf71289890844c49" + integrity sha512-FjgDD/VmL9yl36ByLq66mEusDF/wZGRktK4JA5MkF68hQqj3F8BFMDDVNwCASuwY97H6wr7kw/RFqNI6XocjJQ== + dependencies: + vega-crossfilter "~4.1.1" + vega-dataflow "~5.7.5" + vega-encode "~4.9.1" + vega-event-selector "~3.0.1" + vega-expression "~5.0.1" + vega-force "~4.1.1" + vega-format "~1.1.1" + vega-functions "~5.13.1" + vega-geo "~4.4.1" + vega-hierarchy "~4.1.1" + vega-label "~1.2.1" + vega-loader "~4.5.1" + vega-parser "~6.2.0" + vega-projection "~1.6.0" + vega-regression "~1.1.1" + vega-runtime "~6.1.4" + vega-scale "~7.3.0" + vega-scenegraph "~4.10.2" + vega-statistics "~1.8.1" + vega-time "~2.1.1" + vega-transforms "~4.10.1" + vega-typings "~0.23.0" + vega-util "~1.17.1" + vega-view "~5.11.1" + vega-view-transforms "~4.5.9" + vega-voronoi "~4.2.1" + vega-wordcloud "~4.1.4" vfile-location@^2.0.0: version "2.0.6" @@ -18898,7 +18277,7 @@ which-typed-array@^1.1.2: has-tostringtag "^1.0.0" is-typed-array "^1.1.7" -which@1.3.1, which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -18912,14 +18291,7 @@ which@^2.0.1, which@^2.0.2, which@~2.0.2: dependencies: isexe "^2.0.0" -wide-align@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wide-align@^1.1.0: +wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -18931,13 +18303,6 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -windows-release@^3.1.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999" - integrity sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg== - dependencies: - execa "^1.0.0" - word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" @@ -18955,6 +18320,11 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + wrap-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" @@ -18963,15 +18333,6 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.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" @@ -18995,15 +18356,6 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^1.1.2: - version "1.3.4" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" - integrity sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8= - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - slide "^1.1.5" - write-file-atomic@^2.4.2: version "2.4.3" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" @@ -19074,13 +18426,6 @@ x-is-string@^0.1.0: resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82" integrity sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI= -xdg-basedir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" - integrity sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I= - dependencies: - os-homedir "^1.0.0" - xhr@^2.0.1: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" @@ -19147,7 +18492,7 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== -y18n@^5.0.1, y18n@^5.0.5: +y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== @@ -19177,13 +18522,10 @@ yaml@^2.0.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec" integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== -yargs-parser@13.1.2, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" @@ -19193,35 +18535,38 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.0.0, yargs-parser@^20.2.2, yargs-parser@^20.2.3: +yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-unparser@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" - integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: - flat "^4.1.0" - lodash "^4.17.15" - yargs "^13.3.0" + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" -yargs@13.3.2, yargs@^13.3.0: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== +yargs@16.2.0, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" yargs@^15.0.2, yargs@^15.3.1: version "15.4.1" @@ -19240,31 +18585,18 @@ yargs@^15.0.2, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== +yargs@~17.6.0: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== dependencies: - cliui "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" - string-width "^4.2.0" + string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@~16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" - integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== - dependencies: - cliui "^7.0.0" - escalade "^3.0.2" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.1" - yargs-parser "^20.0.0" + yargs-parser "^21.1.1" yauzl@^2.10.0: version "2.10.0"