diff --git a/.github/.codecov.yml b/.github/.codecov.yml index 1ce321b650d3..ba7dd34e7473 100644 --- a/.github/.codecov.yml +++ b/.github/.codecov.yml @@ -1,7 +1,11 @@ # https://docs.codecov.com/docs/codecov-yaml +codecov: + require_ci_to_pass: yes + coverage: status: project: default: - # https://docs.codecov.com/docs/commit-status#target - target: auto # coverage must be equal or above the previous commit + target: auto + threshold: 2% # the leniency in hitting the target + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9bd61e660b6c..2a9e814b5fe3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,8 +4,20 @@ ### Issues Resolved - - + + + +## Screenshot + + + +## Testing the changes + + ### Check List diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 5daef79cc8b9..7568b0653210 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -25,6 +25,8 @@ env: TEST_OPENSEARCH_TRANSPORT_PORT: 9403 TEST_OPENSEARCH_PORT: 9400 OSD_SNAPSHOT_SKIP_VERIFY_CHECKSUM: true + # Version 112.0.5615.0 + CHROME_VERSION: 1109208 jobs: build-lint-test: @@ -122,6 +124,7 @@ jobs: functional-tests: name: Run functional tests on ${{ matrix.name }} (ciGroup${{ matrix.group }}) strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] @@ -134,6 +137,22 @@ jobs: steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} + - name: Setup Chrome + id: setup-chrome + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: ${{ env.CHROME_VERSION }} + + - name: Set Chrome Path + if: matrix.os != 'windows-latest' + run: | + echo "TEST_BROWSER_BINARY_PATH=${{ steps.setup-chrome.outputs.chrome-path }}" >> $GITHUB_ENV + + - name: Set Chrome Path (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "TEST_BROWSER_BINARY_PATH=${{ steps.setup-chrome.outputs.chrome-path }}" >> $env:GITHUB_ENV + - name: Configure git's autocrlf (Windows only) if: matrix.os == 'windows-latest' run: | @@ -343,17 +362,6 @@ jobs: 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: | echo "VERSION=$(yarn --silent pkg-version)" >> $GITHUB_ENV diff --git a/.lycheeexclude b/.lycheeexclude index 07317835aabf..252db4e82787 100644 --- a/.lycheeexclude +++ b/.lycheeexclude @@ -88,6 +88,7 @@ https://opensearch.org/redirect http://www.opensearch.org/painlessDocs https://www.hostedgraphite.com/ https://connectionurl.com +http://169.254.169.254/latest/meta-data/ # External urls https://www.zeek.org/ @@ -117,3 +118,5 @@ http://www.creedthoughts.gov https://media-for-the-masses.theacademyofperformingartsandscience.org/ https://yarnpkg.com/latest.msi https://forum.opensearch.org/ +https://facebook.github.io/jest/ +https://facebook.github.io/jest/docs/cli.html diff --git a/CHANGELOG.md b/CHANGELOG.md index facbf0d924d6..2bb0133dfe6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - 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-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)) @@ -22,6 +22,9 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) - [CVE-2023-0842] Bump xml2js from 0.4.23 to 0.5.0 ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842)) +- [Multi DataSource] Add private IP blocking validation on server side ([#3912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3912)) +- Bump `joi` to v14 to avoid the possibility of prototype poisoning in a nested dependency ([#3952](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3952)) +- [CVE-2023-2251] Bump yaml to 2.2.2 ([#3947](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3947)) ### πŸ“ˆ Features/Enhancements @@ -71,14 +74,18 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) - [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544)) +- Add `osd-xsrf` header to all requests that incorrectly used `node-version` to satisfy XSRF protection ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) - [Data] Add geo shape filter field ([#3605](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3605)) +- [Notifications] Adds id to toast api for deduplication ([#3752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3752)) - [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [Dashboard] Indicate that IE is no longer supported ([#3641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3641)) - [UI] Add support for comma delimiters in the global filter bar ([#3686](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3686)) - [Multiple DataSource] Allow create and distinguish index pattern with same name but from different datasources ([#3571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3571)) - [Multiple DataSource] Integrate multiple datasource with dev tool console ([#3754](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3754)) - Add satisfaction survey link to help menu ([#3676] (https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3676)) - +- [Vis Builder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) +- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397)) +- [Multiple Datasource] Support Amazon OpenSearch Serverless ([#3957](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3957)) ### πŸ› Bug Fixes - [Vis Builder] Fixes auto bounds for timeseries bar chart visualization ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) @@ -117,14 +124,17 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) - Clean up and rebuild `@osd/pm` ([#3570](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3570)) +- Omit adding the `osd-version` header when the Fetch request is to an external origin ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) - [Vega] Add Filter custom label for opensearchDashboardsAddFilter ([#3640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3640)) - [Timeline] Fix y-axis label color in dark mode ([#3698](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3698)) - [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix indexpattern selection in filter bar ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) - [Table Visualization] Fix table rendering empty unused space ([#3797](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3797)) - [Table Visualization] Fix data table not adjusting height on the initial load ([#3816](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3816)) - Cleanup unused url ([#3847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3847)) +- [BUG] Docked navigation impacts visibility of bottom bar component ([#3978](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3978)) ### 🚞 Infrastructure @@ -139,6 +149,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - 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)) - Add an achievement badger to the PR ([#3721](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3721)) +- Install chrome driver for functional tests from path set by environment variable `TEST_BROWSER_BINARY_PATH`([#3997](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3997)) +- Adds threshold to code coverage config to prevent workflow failures ([#4040](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4040)) ### πŸ“ Documentation @@ -158,7 +170,9 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Doc] Update SECURITY.md with instructions for nested dependencies and backporting ([#3497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3497)) - [Doc] [Console] Fix/update documentation links in Dev Tools console ([#3724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3724)) - [Doc] Update DEVELOPER_GUIDE.md with added manual bootstrap timeout solution and max virtual memory error solution with docker ([#3764](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3764)) +- [Doc] Add COMMUNICATIONS.md with info about Slack, forum, office hours ([#3837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3837)) - [Doc] Add docker files and instructions for debugging Selenium functional tests ([#3747](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3747)) +- [Saved Object Service] Adds design doc for new Saved Object Service Interface for Custom Repository [#3954](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3954) ### πŸ›  Maintenance @@ -175,6 +189,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Remove the unused `renovate.json5` file ([3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) - Allow selecting the Node.js binary using `NODE_HOME` and `OSD_NODE_HOME` ([3508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3508)) - Bump `styled-components` from 5.3.5 to 5.3.9 ([#3678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3678)) +- Bump `js-yaml` from 3.14.0 to 4.1.0 ([#3770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3770)) +- Bump `oui` from `1.0.0` to `1.1.1` ([#3884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3884)) ### πŸͺ› Refactoring @@ -184,7 +200,9 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Replace jQuery.ajax with core.http when calling OSD APIs in console ([#3080](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3080)) - [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) - [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) -- [Console] Replace jQuery usage in console plugin with native methods ([#3733](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3733)) +- [Console] Remove unused ul element and its custom styling ([#3993](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3993)) +- Fix EUI/OUI type errors ([#3798](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3798)) +- Remove unused Sass in `tile_map` plugin ([#4110](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4110)) ### πŸ”© Tests @@ -200,6 +218,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - 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)) - [Tests] Fix unit tests for `get_keystore` ([#3854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3854)) - [Tests] Use `scripts/use_node` instead of `node` in functional test plugins ([#3783](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3783)) +- Temporarily hardcode the largest support `chromedriver` version to `112.0.0` to enable all ftr tests ([#3976](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3976)) ## [2.x] @@ -232,6 +251,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [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)) +- Replace re2 with RegExp in timeline and add unit tests ([#3908](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3908)) ### πŸ› Bug Fixes @@ -242,6 +262,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [TSVB] Fixes undefined serial diff aggregation documentation link ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) - [Console] Fix dev tool console autocomplete not loading issue ([#3775](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3775)) - [Console] Fix dev tool console run command with query parameter error ([#3813](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3813)) +- Add clarifying tooltips to header navigation ([#3573](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3573)) +- [Dashboards Listing] Fix listing limit to utilize `savedObjects:listingLimit` instead of `savedObjects:perPage` ([#4021](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4021)) ### 🚞 Infrastructure diff --git a/COMMUNICATIONS.md b/COMMUNICATIONS.md new file mode 100644 index 000000000000..c1dbfc114ce7 --- /dev/null +++ b/COMMUNICATIONS.md @@ -0,0 +1,83 @@ +# OpenSearch Dashboards Communication + +- [Overview](#overview) +- [Slack](#slack) +- [Forum](#forum) +- [Developer Office Hours](#developer-office-hours) + - [What it is](#what-it-is) + - [When](#when) + - [How to sign up](#how-to-sign-up) + - [FAQ](#faq) + +## Overview + +The purpose of this document is to provide information regarding the communication channels for OpenSearch Dashboards. All communication is subject to the [OpenSearch Code of Conduct](CODE_OF_CONDUCT.md). Please see [CONTRIBUTING](CONTRIBUTING.md) if you're interested in contributing to the project. + +## Slack + +The OpenSearch project has a public workspace on [Slack](https://opensearch.slack.com). See the [Getting Started guide]() for steps to register and setup the workspace. + +Once registered, check out these channels for discussion of OpenSearch Dashboards topics: + +- [#dashboards](https://opensearch.slack.com/archives/C01QENNTGUD) +- [#dashboards-ux](https://opensearch.slack.com/archives/C05389T9LJC) + +## Forum + +Slack conversations are not searchable outside the workspace. For this reason we encourage using the [OpenSearch Dashboards category](https://forum.opensearch.org/c/opensearch-dashboards/57) of the forum for technical support discussions or summarizing findings for the rest of the community. + +## Developer Office Hours + +### What it is + +A recurring 1-hour virtual meeting for community developers to chat with [OpenSearch Dashboards project maintainers](MAINTAINERS.md). Priority will be given to topics that are signed-up in advance, but ad-hoc discussions are welcome in any remaining time. + +While we'll always prioritize asynchronous communication, sometimes a community call is the most effective and efficient venue to share information and knowledge. Some reasons to sign up: + +1. Review a proposal or technical design for a new feature in OpenSearch Dashboards or an OpenSearch Dashboards plugin +2. Learn more about how to build and extend OpenSearch Dashboards - which APIs, plugins, resources, and services are available to speed development +3. Discuss OpenSearch Dashboard roadmap and technical initiatives + +Signing up isn't required to attend - all OpenSearch Dashboards contributors or interested developers are welcome as participants. + +Bring your ideas and projects early, while you still have time and flexibility to make significant changes. + +### When + +Every other Thursday, 10AM-11AM PT. + +### How to sign up + +There will be a forum post for each iteration of the meeting, with pre-defined slots. To sign-up, simply reply in the forum thread with the following template: + +* Topic: [a brief description of what you'd like to discuss] +* Requested by: [provide GitHub aliases of attendees] +* GitHub issues or PRs: [before signing up, make sure to create an issue, whether in the OpenSearch Dashboards repository or your own plugin repository] +* Time required [choose 15, 30, 45, or 60 minutes] +* Requested maintainer: [optional; provide GitHub alias of any particular maintainer you’d like to attend] + +### FAQ + +#### Will the meetings be recorded? + +Yes, we plan to record each office hours session and post to our YouTube channel so the information can be more easily shared and referenced. + +#### Will all maintainers attend? + +Generally no, but there will always be at least one maintainer. We'll review the sign-ups ahead of time to make sure the right subject-matter experts will attend, depending on the topics. + +#### What happens if there are no sign-ups for a particular session? + +The session will still occur, and the maintainers will present a brief knowledge-sharing session or demo. We'll also hold ad-hoc discussions, but the session may end early. + +#### Is it first come first serve or do we get to decide which topics we discuss in a session? + +For sign-ups, it’s first-come first served, until we decide we need another method. + +#### Will there also be meeting notes? or is the recording the only available transcript? + +No. But we’ll also post the chat transcript and any slides shared (see https://forum.opensearch.org/t/opensearch-community-meeting-2023-0131/11892/5 as example) + +#### How can I cancel or reschedule? + +Just leave another forum reply, as early as possible so other folks have the opportunity to sign-up for the same spot. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 045b17019b7d..4a605d04c052 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ - [Contributing to OpenSearch](#contributing-to-opensearch-dashboards) - [First Things First](#first-things-first) - [Ways to Contribute](#ways-to-contribute) + - [Join the Discussion](#join-the-discussion) - [Bug Reports](#bug-reports) - [Feature Requests](#feature-requests) - [Documentation Changes](#documentation-changes) @@ -19,6 +20,11 @@ OpenSearch is a community project that is built and maintained by people just li **Only submit your own work** (or work you have sufficient rights to submit) - Please make sure that any code or documentation you submit is your work or you have the rights to submit. We respect the intellectual property rights of others, and as part of contributing, we'll ask you to sign your contribution with a "Developer Certificate of Origin" (DCO) that states you have the rights to submit this work and you understand we'll use your contribution. There's more information about this topic in the [DCO section](#developer-certificate-of-origin). ## Ways to Contribute + +### Join the Discussion + +See the [communication guide](COMMUNICATION.md)for information on how to join our slack workspace, forum, or developer office hours. + ### Bug Reports A bug is when software behaves in a way that you didn't expect and the developer didn't intend. To help us understand what's going on, we first want to make sure you're working from the latest version. Please make sure you're testing against the [latest version](https://github.com/opensearch-project/OpenSearch-Dashboards). diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index d3c8b269c4c0..3df2106e9678 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -209,6 +209,10 @@ $ yarn start --run-examples - [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) +#### Join the discussion + +See the [communication guide](COMMUNICATION.md)for information on how to join our slack workspace, forum, or developer office hours. + ## 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. diff --git a/README.md b/README.md index d1b8fc9ed9dd..5c6c764f87f6 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,13 @@ ## Welcome -OpenSearch Dashboards is an open source search and analytics visualization. We aim to be the best community-driven platform and provide all the contributors a great open source experience. +OpenSearch Dashboards is an open-source data visualization tool designed to work with OpenSearch. OpenSearch Dashboards gives you data visualization tools to improve and automate business intelligence and support data-driven decision-making and strategic planning. -Feel free to take a look at what the community has been up to, and then head over to the [Project Board](https://github.com/opensearch-project/OpenSearch-Dashboards/projects) to track release targets, or jump in and [start opening issues](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose), [set up your development environment](DEVELOPER_GUIDE.md#getting-started), or [start contributing](CONTRIBUTING.md). +We aim to be an exceptional community-driven platform and to foster open participation and collective contribution with all contributors. Stay up to date on what's happening with the OpenSearch Project by tracking GitHub [issues](https://github.com/opensearch-project/OpenSearch-Dashboards/issues) and [pull requests](https://github.com/opensearch-project/OpenSearch-Dashboards/pulls). + +You can [contribute to this project](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/CONTRIBUTING.md) by [opening issues](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose) to give feedback, share ideas, identify bugs, and contribute code. + +Set up your [OpenSearch Dashboards development environment](ttps://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/DEVELOPER_GUIDE.md#getting-started-guide) today! The project team looks forward to your contributions. ## Code Summary @@ -23,7 +27,7 @@ Feel free to take a look at what the community has been up to, and then head ove * [Project Website](https://opensearch.org/) * [Downloads](https://opensearch.org/downloads.html) * [Documentation](https://opensearch.org/docs/) -* Need help? Try [Forums](https://discuss.opendistrocommunity.dev/) +* Need help? See the [communication guide](COMMUNICATION.md) for various options * [Project Principles](https://opensearch.org/#principles) * [Developer Guide](DEVELOPER_GUIDE.md) * [Contributing to OpenSearch](CONTRIBUTING.md) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 38377296bd20..d7e0d390b0fc 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -229,8 +229,7 @@ # functionality in Visualization. # vis_builder.enabled: false -# Set the value of this setting to true to enable the experimental multiple data source -# support feature. Use with caution. +# Set the value of this setting to true to enable multiple data source feature. #data_source.enabled: false # Set the value of these settings to customize crypto materials to encryption saved credentials # in data sources. @@ -238,5 +237,30 @@ #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.endpointDeniedIPs: [ +# '127.0.0.0/8', +# '::1/128', +# '169.254.0.0/16', +# 'fe80::/10', +# '10.0.0.0/8', +# '172.16.0.0/12', +# '192.168.0.0/16', +# 'fc00::/7', +# '0.0.0.0/8', +# '100.64.0.0/10', +# '192.0.0.0/24', +# '192.0.2.0/24', +# '198.18.0.0/15', +# '192.88.99.0/24', +# '198.51.100.0/24', +# '203.0.113.0/24', +# '224.0.0.0/4', +# '240.0.0.0/4', +# '255.255.255.255/32', +# '::/128', +# '2001:db8::/32', +# 'ff00::/8', +# ] + # Set the value of this setting to false to hide the help menu link to the OpenSearch Dashboards user survey -# opensearchDashboards.survey.url: "https://survey.opensearch.org" \ No newline at end of file +# opensearchDashboards.survey.url: "https://survey.opensearch.org" diff --git a/docs/saved_objects/img/current_saved_object_service_workflow.png b/docs/saved_objects/img/current_saved_object_service_workflow.png new file mode 100644 index 000000000000..b763101b6987 Binary files /dev/null and b/docs/saved_objects/img/current_saved_object_service_workflow.png differ diff --git a/docs/saved_objects/img/proposed_saved_object_service_workflow.png b/docs/saved_objects/img/proposed_saved_object_service_workflow.png new file mode 100644 index 000000000000..117fd0217d51 Binary files /dev/null and b/docs/saved_objects/img/proposed_saved_object_service_workflow.png differ diff --git a/docs/saved_objects/resources/current_saved_object_service_workflow.puml b/docs/saved_objects/resources/current_saved_object_service_workflow.puml new file mode 100644 index 000000000000..bc5bbf82c621 --- /dev/null +++ b/docs/saved_objects/resources/current_saved_object_service_workflow.puml @@ -0,0 +1,19 @@ +@startuml +title: Current Saved Object Service Flow +actor User +participant "Saved Object Client" as Client +participant "Saved Object Repository" as Repo +participant "Opensearch" as OS + +User -> Client: Create Saved Object +Client -> Repo: Create Saved Object +Repo -> OS: Index Saved Object +OS --> Repo: Saved Object Saved +Client -> User: Saved Object Created +User -> Client: Get Saved Object +Client -> Repo: Get Saved Object +Repo -> OS: Get Saved Object +OS --> Repo: Return Saved Object +Repo -> Client: Return Saved Object +Client -> User: Saved Object Data +@enduml \ No newline at end of file diff --git a/docs/saved_objects/resources/proposed_saved_object_service_workflow.puml b/docs/saved_objects/resources/proposed_saved_object_service_workflow.puml new file mode 100644 index 000000000000..27a5e1cd49ed --- /dev/null +++ b/docs/saved_objects/resources/proposed_saved_object_service_workflow.puml @@ -0,0 +1,38 @@ +@startuml + +title: Proposed Saved Object Service Flow + +actor User + +participant "OpenSearch-Dashboards" as OSD + +box "Saved Object Service" #LightBlue +participant "Saved Object Client" as Client +participant "Repository Factory Provider" as Factory +end box + +box "Dashboards Storage Plugin" #LightYellow +participant "Repository\n(e.g. PostgresRepository,\nDynamoDBRepository)" as Repo +participant "Metadata Storage\n(e.g. Postgres, \nDynamoDB etc)" as Meta +end box + +autonumber +group OSD Bootstrap +Repo -> Factory: Register custom repository +Factory -> Client: Returns repository +Client -> OSD: Returns Saved Object Client +end group +User -> Client: Create Saved Object +Client -> Repo: Create Saved Object +Repo -> Meta: Create/Update Record +Meta --> Repo: Saved Object Saved +Client -> User: Saved Object Created +User -> Client: Get Saved Object +Client -> Repo: Get Saved Object +Repo -> Meta: Fetch Saved Object from storage +Meta --> Repo: Return Saved Object +Repo -> Client: Return Saved Object +Client -> User: Saved Object Data + +skinparam BoxPadding 15 +@enduml \ No newline at end of file diff --git a/docs/saved_objects/saved_object_repository_factory_design.md b/docs/saved_objects/saved_object_repository_factory_design.md new file mode 100644 index 000000000000..d3fedd3575dc --- /dev/null +++ b/docs/saved_objects/saved_object_repository_factory_design.md @@ -0,0 +1,171 @@ +# Proposed Saved Object Service Interface for Custom Repository + +## Introduction + +The new saved object service interface for custom repository is a project that aims to improve scalability of the existing saved object service by introducing a new interface. The goal of this project is to provide a more efficient and flexible interface that will make it easier for developers to configure metadata of Dashboards in any different storage than OpenSearch, such as mysql, postgres, DDB, serverless (S3+ Athena). + +Currently, Dashboards stores its metadata configuration inside OpenSearch index (called .kibana). This approach is by design of Dashboards and biased towards product decision by upstream which works seamlessly and out of the box for customers but it introduces challenges while operating at scale and providing high availability for Dashboards. While choosing OpenSearch as a storage for Dashboards metadata, availability of Dashboards depends on OpenSearch cluster’s availability and other cluster parameters such as cluster health, state, versions which could make Dashboards unavailable. + +To mitigate above problem and unblock future extensibility of Dashboards, we are building Dashboards Meta storage adaptor to decouple Dashboards metadata storage from OpenSearch. This project will focus on introducing new interface in Saved Object Service using which developer can build their custom repository and save Dashboards metadata in storage of their choice. + +The stakeholders of this new interface include the developers of the Dashboards and community contributors who wants to use other metadata store. + +## Architecture Overview + +The Saved Object Service is a critical component of Dashboards that provides a way to store and manage application data. It is built using a modular architecture that provides a high degree of flexibility and extensibility. The new interface will be designed to replace [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) implementation so that developers can build plugins that leverage the power of existing saved object service and use their own database to store and retrieve metadata of OpenSearch Dashboards. + +### Current Architecture + +The repository interface named [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) in OpenSearch-Dashboards is a module that provides an interface for managing saved objects. The [SavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L139) is the implementation of [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134), which uses OpenSearch index as it’s data store. It is responsible for storing, retrieving, and deleting saved objects for Dashboards, such as visualizations, dashboards, and searches. + +The Saved Object Repository is built on top of the OpenSearch client and provides a simplified interface for working with OpenSearch. It uses the Saved Object Serializer to convert saved objects between their internal and external representations. The repository is then being consumed by Saved object client to create scoped saved object client. + +![img](./img/current_saved_object_service_workflow.png) + +### Proposed Architecture + +- **Approach 1 (Preferred)**: The proposed architecture will add one more layer of abstraction in Saved Object Service. `The Repository Factory Provider` in OpenSearch Dashboards will be responsible for creating and managing instances of the Repository (e.g. SavedObjectRepository, PostgresRepository, DynamoDBRepository etc.), which is used to interact with the metadata storage that stores the saved objects. Currently we have an repository interface named [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134), and the [SavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L139) is the implementation, which use an OpenSearch index as its data store. This approach would make the implementation of [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) replaceable by plugin. + + ![img](./img/proposed_saved_object_service_workflow.png) + + * Pros: + * Only change needed in Dashboard is to introduce one more abstraction layer in Saved Object Service. + * Adds opportunity for community developers to contribute for other meta store. + + * Cons + * Code reusability is low. +
+ +**POC**: +1) Core Dashboards Change: https://github.com/bandinib-amzn/OpenSearch-Dashboards/commit/b9cfc14 +2) Postgres Repository Plugin: https://github.com/bandinib-amzn/metadata_plugin/commit/dac35f0 + +`SavedObjectsServiceSetup` provides interface to create custom Saved Object Repository. +``` +/** +* Set the default {@link SavedObjectRepositoryFactoryProvider | factory provider} for creating Saved Objects repository. +* Only one repository can be set, subsequent calls to this method will fail. +*/ +registerRepositoryFactoryProvider: ( +respositoryFactoryProvider: SavedObjectRepositoryFactoryProvider +) => void; +``` + +Here are the main steps involved in using the Saved Objects Repository Factory in Dashboards: +1. Define the dependencies: The Saved Object Repository Factory Provider requires the function which creates instance of [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134). + ``` + export const repositoryFactoryProvider: SavedObjectRepositoryFactoryProvider = ( + options: SavedObjectsRepositoryOptions + ) => { + . + . + . + return new PostgresRepository({ + typeRegistry, + serializer, + migrator, + allowedTypes, + }); + } + ``` +2. Register the provider: Register the repository factory provider with right dependencies. + ``` + core.savedObjects.registerRepositoryFactoryProvider(repositoryFactoryProvider); + ``` +3. Implement the Saved Object Operations for chosen storage type: Implement the CRUD and other operations for contracts defined in [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) + ``` + async create( + type: string, + attributes: T, + options: SavedObjectsCreateOptions = {} + ): Promise> { + ... + } + + async get( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> { + ... + } + + async update( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ): Promise> { + ... + } + + async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ): Promise { + ... + } + . + . + . + ``` + +- **Approach 2**: Build external plugin and using saved object client wrapper or client factory provider injection mechanism we can build custom object for Postgres or other DB. + + * Pros: + * No changes in core Dashboards. That means we can keep Dashboards as it is with very minimal changes. + + + * Cons + * Code reusability is low. + * Some components of Saved object service such as Serializer, Type registry, interface to create internal and scoped repository are only available during Saved Object Service Start. As per the current architecture, first Saved Object Service Setup β†’ Plugin Setup β†’ Saved Object Service Start β†’ Plugin Start. Some core plugin (e.g. opensearch_dashboards_usage_collection) calls find operation before plugin start and it fails because some components are still not available before plugin start. +
+ + **POC**: https://github.com/bandinib-amzn/metadata_plugin/compare/f040daf...89213eb + + +- **Approach 3**: In this approach, we just extend the `SavedObjectsRepository` class and override CRUD and other saved object operation in core Dashboards. + + * Pros: + * As we are extending the repository in core saved object service itself, we can reuse the validation and utility functions for other database options. + + + * Cons + * Changes in core Dashboards : We will be making considerable changes in critical component of Dashboards. + * With this approach, user will have to use the data storage option that we choose. +
+ + **POC**: https://github.com/bandinib-amzn/OpenSearch-Dashboards/compare/main...22d7f30 + +## Implementation Details + + +| Repository | Component | Change | +| ----------- | ----------- | ----------- | +| OpenSearch-Dashboards | Saved Object Service | Add Saved object repository factory provider | +| OpenSearch-Dashboards | Config | Configuration for metadata storage | +| MetaStorage-Plugin [Name TBD] | Plugin / Extension | We will build new plugin for Postgres. This is use case for new interface in Saved Object Repository. | + +### Configuration for metadata storage: +``` +metaStorage.enabled: true +metaStorage.config: { + type: 'xxxx', + hostName: 'xxxx', + userName: 'xxxx', + password: 'xxxx', + port: xxxx, +} +``` + +## Testing and Quality Assurance + +### Testing Approach + +The following testing approach will be used to ensure the quality of the system: + +1. **Unit testing**: Metadata store plugin will be thoroughly unit tested to ensure it meets its requirements and performs as expected. Also we will add new test cases in OpenSearch-Dashboards to test new repository factory provider. +2. **Integration testing**: Components will be integrated and tested together to ensure they work together seamlessly and without conflicts. + diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx index c24d258fd7d8..7e742aba7cc7 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx @@ -82,7 +82,6 @@ export class SearchableListContainerComponentInner extends Component (checked[id] = false)); this.state = { checked, hasMatch, diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index 797b87513039..f466d120e218 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -164,6 +164,15 @@ export function ListContainerExample({ The first HelloWorldEmbeddable does not emit the hasMatch output variable, so the container chooses to hide it.

+

+ Type some strings in the search bar, and press Check matching button. If the search + string matches with any strings from the title or the description of the children + embeddables, the child embeddable's check box will be checked. Noted that the + search filter is case sensitive. However, even if the search string matches with the + strings in the HelloWorldEmbeddable, its check box will not be checked because of the + reason explained above. If we click Delete checked, all the selected child embeddables + will be deleted from the container. +

Check out the "Dynamically adding children" section, to see how to add diff --git a/package.json b/package.json index 998a5335fb40..06442016f42a 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,8 @@ "**/unset-value": "^2.0.1", "**/jest-config": "npm:@amoo-miki/jest-config@27.5.1", "**/jest-jasmine2": "npm:@amoo-miki/jest-jasmine2@27.5.1", - "**/xml2js": "^0.5.0" + "**/xml2js": "^0.5.0", + "**/yaml": "^2.2.2" }, "workspaces": { "packages": [ @@ -119,7 +120,7 @@ "dependencies": { "@aws-crypto/client-node": "^3.1.1", "@elastic/datemath": "5.0.3", - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@elastic/good": "^9.0.1-kibana3", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "2.0.0", @@ -135,7 +136,7 @@ "@hapi/podium": "^4.1.3", "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", - "@opensearch-project/opensearch": "^2.1.0", + "@opensearch-project/opensearch": "^2.2.0", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", "@osd/apm-config-loader": "1.0.0", @@ -168,7 +169,7 @@ "dns-sync": "^0.2.1", "elastic-apm-node": "^3.7.0", "elasticsearch": "^16.7.0", - "http-aws-es": "6.0.0", + "http-aws-es": "npm:@zhongnansu/http-aws-es@6.0.1", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", @@ -183,8 +184,8 @@ "https-proxy-agent": "^5.0.0", "inline-style": "^2.0.0", "ip-cidr": "^2.1.0", - "joi": "^13.5.2", - "js-yaml": "^3.14.0", + "joi": "^14.3.1", + "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", "json-stringify-safe": "5.0.1", "lodash": "^4.17.21", @@ -199,7 +200,6 @@ "pegjs": "0.10.0", "proxy-from-env": "1.0.0", "query-string": "^6.13.2", - "re2": "1.17.4", "react": "^16.14.0", "react-dom": "^16.12.0", "react-input-range": "^1.3.0", @@ -288,7 +288,7 @@ "@types/jest": "^27.4.0", "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", - "@types/js-yaml": "^3.11.1", + "@types/js-yaml": "^4.0.5", "@types/json-stable-stringify": "^1.0.32", "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", diff --git a/packages/opensearch-eslint-config-opensearch-dashboards/README.md b/packages/opensearch-eslint-config-opensearch-dashboards/README.md index 4fb3f13c8f29..a2f60c437e57 100644 --- a/packages/opensearch-eslint-config-opensearch-dashboards/README.md +++ b/packages/opensearch-eslint-config-opensearch-dashboards/README.md @@ -4,7 +4,7 @@ The eslint config used by the opensearch dashboards team ## Usage -To use this eslint config, just install the peer dependencies and reference it +To use this eslint config, just install the peer dependencies and reference it in your `.eslintrc`: ```javascript @@ -17,8 +17,8 @@ in your `.eslintrc`: ## Optional jest config -If the project uses the [jest test runner](https://facebook.github.io/jest/), -the `@elastic/eslint-config-kibana/jest` config can be extended as well to use +If the project uses the [jest test runner](https://jestjs.io), +the `@elastic/eslint-config-kibana/jest` config can be extended as well to use `eslint-plugin-jest` and add settings specific to it: ```javascript diff --git a/packages/osd-apm-config-loader/package.json b/packages/osd-apm-config-loader/package.json index c3c0249bd73e..fc06ff025e8a 100644 --- a/packages/osd-apm-config-loader/package.json +++ b/packages/osd-apm-config-loader/package.json @@ -13,7 +13,7 @@ "dependencies": { "@elastic/safer-lodash-set": "0.0.0", "@osd/utils": "1.0.0", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/osd-apm-config-loader/src/utils/read_config.ts b/packages/osd-apm-config-loader/src/utils/read_config.ts index 806a1ad2e92a..7076060cb04d 100644 --- a/packages/osd-apm-config-loader/src/utils/read_config.ts +++ b/packages/osd-apm-config-loader/src/utils/read_config.ts @@ -29,13 +29,13 @@ */ import { readFileSync } from 'fs'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import { set } from '@elastic/safer-lodash-set'; import { isPlainObject } from 'lodash'; import { ensureDeepObject } from './ensure_deep_object'; -const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); +const readYaml = (path: string) => load(readFileSync(path, 'utf8')); function replaceEnvVarRefs(val: string) { return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => { diff --git a/packages/osd-config-schema/package.json b/packages/osd-config-schema/package.json index 52471e29527c..c88afe609e1f 100644 --- a/packages/osd-config-schema/package.json +++ b/packages/osd-config-schema/package.json @@ -16,7 +16,7 @@ }, "peerDependencies": { "lodash": "^4.17.21", - "joi": "^13.5.2", + "joi": "^14.3.1", "moment": "^2.24.0", "type-detect": "^4.0.8" } diff --git a/packages/osd-config/package.json b/packages/osd-config/package.json index 7d2f68abc8d8..f6d363c9c386 100644 --- a/packages/osd-config/package.json +++ b/packages/osd-config/package.json @@ -14,7 +14,7 @@ "@osd/config-schema": "1.0.0", "@osd/logging": "1.0.0", "@osd/std": "1.0.0", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "load-json-file": "^6.2.0", "lodash": "^4.17.21", "moment": "^2.24.0", diff --git a/packages/osd-config/src/raw/read_config.ts b/packages/osd-config/src/raw/read_config.ts index 0c4a7c2330de..ad7820381eee 100644 --- a/packages/osd-config/src/raw/read_config.ts +++ b/packages/osd-config/src/raw/read_config.ts @@ -29,13 +29,13 @@ */ import { readFileSync } from 'fs'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import { set } from '@elastic/safer-lodash-set'; import { isPlainObject } from 'lodash'; import { ensureDeepObject } from './ensure_deep_object'; -const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); +const readYaml = (path: string) => load(readFileSync(path, 'utf8')); function replaceEnvVarRefs(val: string) { return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => { diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index 02fcddb36e6f..44404a9ae5a3 100644 --- a/packages/osd-opensearch/package.json +++ b/packages/osd-opensearch/package.json @@ -12,7 +12,7 @@ "osd:watch": "../../scripts/use_node scripts/build --watch" }, "dependencies": { - "@opensearch-project/opensearch": "^2.1.0", + "@opensearch-project/opensearch": "^2.2.0", "@osd/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/packages/osd-optimizer/package.json b/packages/osd-optimizer/package.json index 2aec5681ba91..5e82a7004fff 100644 --- a/packages/osd-optimizer/package.json +++ b/packages/osd-optimizer/package.json @@ -27,7 +27,7 @@ "execa": "^4.0.2", "fibers": "^5.0.3", "jest-diff": "^27.5.1", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", "lmdb-store": "^1.6.11", "normalize-path": "^3.0.0", diff --git a/packages/osd-optimizer/src/limits.ts b/packages/osd-optimizer/src/limits.ts index 86b186930275..d81137c3a9a4 100644 --- a/packages/osd-optimizer/src/limits.ts +++ b/packages/osd-optimizer/src/limits.ts @@ -51,7 +51,7 @@ export function readLimits(): Limits { } } - return yaml ? (Yaml.safeLoad(yaml) as any) : {}; + return yaml ? (Yaml.load(yaml) as any) : {}; } export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { @@ -109,6 +109,6 @@ export function updateBundleLimits(log: ToolingLog, config: OptimizerConfig) { pageLoadAssetSize, }; - Fs.writeFileSync(LIMITS_PATH, Yaml.safeDump(newLimits)); + Fs.writeFileSync(LIMITS_PATH, Yaml.dump(newLimits)); log.success(`wrote updated limits to ${LIMITS_PATH}`); } diff --git a/packages/osd-test/package.json b/packages/osd-test/package.json index 69fa50828fc0..c1ee4f1687cd 100644 --- a/packages/osd-test/package.json +++ b/packages/osd-test/package.json @@ -31,7 +31,7 @@ "exit-hook": "^2.2.0", "getopts": "^2.2.5", "glob": "^7.1.7", - "joi": "^13.5.2", + "joi": "^14.3.1", "lodash": "^4.17.21", "parse-link-header": "^2.0.0", "rxjs": "^6.5.5", diff --git a/packages/osd-ui-framework/package.json b/packages/osd-ui-framework/package.json index a89cc82a9d42..3ebc09f4a8ad 100644 --- a/packages/osd-ui-framework/package.json +++ b/packages/osd-ui-framework/package.json @@ -23,7 +23,7 @@ "enzyme-adapter-react-16": "^1.9.1" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@osd/babel-preset": "1.0.0", "@osd/optimizer": "1.0.0", "grunt": "^1.5.2", diff --git a/packages/osd-ui-shared-deps/package.json b/packages/osd-ui-shared-deps/package.json index c4d8103ecc00..61f7008fbc0f 100644 --- a/packages/osd-ui-shared-deps/package.json +++ b/packages/osd-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "31.1.0", - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@elastic/numeral": "^2.5.0", "@osd/i18n": "1.0.0", "@osd/monaco": "1.0.0", diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.10.md b/release-notes/opensearch-dashboards.release-notes-1.3.10.md new file mode 100644 index 000000000000..be08d46aac93 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.10.md @@ -0,0 +1,33 @@ +# Version 1.3.10 Release Notes + +### πŸ›‘ Security + +- [CVE-2020-15366][1.x] Bump ajv from 4.11.8 to 6.12.6 ([#4035](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4035)) +- [CVE-2022-48285][1.x] Bump jszip from 3.7.1 to 3.10.1 ([#4011](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4011)) +- [CVE-2021-35065][1.x] Bump glob-parent from 6.0.0 to 6.0.2 ([#4005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4005)) +- [CVE-2022-25851][1.x] Bump jpeg-js from 0.4.1 to 0.4.4 ([#3860](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3860)) +- [CVE-2022-25858][1.x] Bump terser from 4.8.0 to 4.8.1 ([#3786](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3786)) +- [CVE-2021-23490][1.x] Bump parse-link-header from 1.0.1 to 2.0.0 ([#3820](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3820)) +- [CVE-2021-3765][1.x] Bump validator from 8.2.0 to 13.9.0 ([#3753](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3753)) +- [CVE-2022-25758][1.x] Bump scss-tokenizer from 0.3.0 to 0.4.3 ([#3789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3789)) +- [CVE-2021-3803][1.x] Bump nth-check from 1.0.2 to 2.0.1 ([#3745](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3745)) +- Bump highlight.js from 9.18.5 to 10.7.3 to solve security concerns ([#4062](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4062)) + +### πŸ“ˆ Features/Enhancements + +- Add tooltip to help icon ([#3872](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3872)) + +### πŸ› Bug Fixes + +- [TSVB] Fix the link to "serial differencing aggregation" documentation ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) + +### πŸ“ Documentation + +- Update jest documentation links ([#3939](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3939)) + +### πŸ›  Maintenance + +- Add threshold to code coverage changes for project ([#4050](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4050)) +- Temporarily hardcode chromedriver to 112.0.0 to enable all ftr tests ([#4039]()) +- Update MAINTAINERS.md and CODEOWNERS ([#3938](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3938)) +- Add opensearch-dashboards-docker-dev to .gitignore ([#3781](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3781)) diff --git a/release-notes/opensearch-dashboards.release-notes-2.7.0.md b/release-notes/opensearch-dashboards.release-notes-2.7.0.md new file mode 100644 index 000000000000..60eda3832436 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.7.0.md @@ -0,0 +1,92 @@ +## Version 2.7.0 Release Notes + +### Deprecations + +### πŸ›‘ Security + +- [CVE-2023-26486] Bump vega from `5.22.1` to `5.23.0` ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) +- [CVE-2023-26487] Bump vega from `5.22.1` to `5.23.0` ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) +- [CVE-2023-0842] Bump xml2js from `0.4.23` to `0.5.0` ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842)) + +### πŸ“ˆ Features/Enhancements + +- Add satisfaction survey link to help menu ([#3676](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3676)) +- Add `osd-xsrf` header to all requests that incorrectly used `node-version` to satisfy XSRF protection ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) +- [Dashboard] Add Dashboards-list integrations for Plugins ([#3090](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3090) ) +- [Data] Add geo shape filter field ([#3605](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3605)) +- [Doc Links] Add downgrade logic for branch in DocLinkService ([#3483](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3483)) +- [Monaco editor] Add json worker support ([#3424](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3424)) +- [Multiple DataSource] Allow create and distinguish index pattern with same name but from different datasources ([#3604](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3604)) +- [Multiple DataSource] Integrate multiple datasource with dev tool console ([#3754](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3754)) +- [Notifications] Add id to toast api for deduplication ([#3752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3752)) +- [UI] Add support for comma delimiters in the global filter bar ([#3686](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3686)) +- [UI] Indicate that IE is no longer supported ([#3641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3641)) +- [Vega] Add Filter custom label for opensearchDashboardsAddFilter ([#3640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3640)) +- [VisBuilder] Add metric to metric, bucket to bucket aggregation persistence ([#3495](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3495)) +- [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) + +### πŸ› Bug Fixes + +- Clean up and rebuild `@osd/pm` ([#3570](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3570)) +- Omit adding the `osd-version` header when the Fetch request is to an external origin ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) +- [Console] Fix/update documentation links in Dev Tools console ([#3724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3724)) +- [Console] Fix dev tool console autocomplete not loading issue ([#3775](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3775)) +- [Console] Fix dev tool console run command with query parameter error ([#3813](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3813)) +- [Table Visualization] Fix table rendering empty unused space ([#3797](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3797)) +- [Table Visualization] Fix data table not adjusting height on the initial load ([#3816](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3816)) +- [Timeline] Fix y-axis label color in dark mode ([#3698](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3698)) +- [TSVB] Fix undefined serial diff aggregation documentation link ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) +- [UI] Add clarifying tooltips to header navigation icons ([#3626](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3626)) +- [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix indexpattern selection in filter bar ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) + +### 🚞 Infrastructure + +- Use mirrors to download Node.js binaries to escape sporadic 404 errors ([#3619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3619)) +- [CI] Update NOTICE file, add validation to GitHub CI ([#3051](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3051)) +- [CI] Reduce redundancy by using matrix strategy on Windows and Linux workflows ([#3514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3514)) +- [Darwin] Add support for Darwin for running OpenSearch snapshots with `yarn opensearch snapshot` ([#3537](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3537)) + +### πŸ“ Documentation + +- 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] Improve DEVELOPER_GUIDE to make first time setup quicker and easier ([#3421](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3421)) +- [Doc] Update DEVELOPER_GUIDE with added manual bootstrap timeout solution and max virtual memory error solution with docker ([#3764](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3764)) +- [Doc] Add second command to install yarn step in DEVELOPER_GUIDE ([#3633](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3633)) +- [Doc] Add docker dev set up instruction ([#3444](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3444)) +- [Doc] Add docker files and instructions for debugging Selenium functional tests ([#3747](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3747)) +- [Doc] Update SECURITY with instructions for nested dependencies and backporting ([#3497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3497)) +- [TSVB] Fix typo in TSVB README ([#3518](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3518)) +- [UI Actions] Improve UI actions explorer ([#3614](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3614)) + +### πŸ›  Maintenance + +- 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)) +- Allow selecting the Node.js binary using `NODE_HOME` and `OSD_NODE_HOME` ([#3508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3508)) +- Remove the unused `renovate.json5` file ([#3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) +- Bump `styled-components` from `5.3.5` to `5.3.9` ([#3678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3678)) +- Bump `oui` from `1.0.0` to `1.1.1` ([3884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3884)) +- [Timeline] Update default expressions from `.es(*)` to `.opensearch(*)`. ([#2720](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2720)) + +### πŸͺ› Refactoring + +- Remove automatic addition of `osd-version` header to requests outside of OpenSearch Dashboards ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) +- [Doc Links] Clean up docs_link_service organization so that strings are in the right categories. ([#3685](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3685)) +- [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) +- [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544)) +- [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) +- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397)) + +### πŸ”© Tests + +- Update caniuse to `1.0.30001460` to fix failed integration tests ([#3538](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3538)) +- [Tests] Fix unit tests for `get_keystore` ([#3854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3854)) + +## πŸŽ‰ Welcome + +Thank you to all the first-time contributors who made this release possible: @pjfitzgibbons, @djindjic, @jlabatut, @sayuree, @Nicksqain, @kappassov, @aswath86, @frost017, @curq, @Aigerim-ai, @andreymyssak, @Hailong-am! diff --git a/scripts/jest.js b/scripts/jest.js index 53c687e51a84..80f269c72498 100755 --- a/scripts/jest.js +++ b/scripts/jest.js @@ -38,7 +38,7 @@ // // node scripts/jest --coverage // -// See all cli options in https://facebook.github.io/jest/docs/cli.html +// See all cli options in https://jestjs.io/docs/cli var resolve = require('path').resolve; process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.js')); diff --git a/scripts/jest_integration.js b/scripts/jest_integration.js index f5bc937c4d79..265919f20708 100755 --- a/scripts/jest_integration.js +++ b/scripts/jest_integration.js @@ -38,7 +38,7 @@ // // node scripts/jest_integration --coverage // -// See all cli options in https://facebook.github.io/jest/docs/cli.html +// See all cli options in https://jestjs.io/docs/cli var resolve = require('path').resolve; process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.integration.js')); diff --git a/scripts/upgrade_chromedriver.js b/scripts/upgrade_chromedriver.js index 3aa896fd1fa9..ee992706a1fc 100644 --- a/scripts/upgrade_chromedriver.js +++ b/scripts/upgrade_chromedriver.js @@ -25,17 +25,34 @@ const versionCheckCommands = []; switch (process.platform) { case 'win32': versionCheckCommands.push( - 'powershell "(Get-Item \\"$Env:Programfiles/Google/Chrome/Application/chrome.exe\\").VersionInfo.FileVersion"' + ...[ + ...(process.env.TEST_BROWSER_BINARY_PATH + ? [ + `powershell "(Get-Item \\"${process.env.TEST_BROWSER_BINARY_PATH}\\").VersionInfo.FileVersion"`, + ] + : []), + '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' + ...[ + ...(process.env.TEST_BROWSER_BINARY_PATH + ? [`${process.env.TEST_BROWSER_BINARY_PATH} --version`] + : []), + '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --version', + ] ); break; default: + versionCheckCommands.push( + ...(process.env.TEST_BROWSER_BINARY_PATH + ? [`${process.env.TEST_BROWSER_BINARY_PATH} --version`] + : []) + ); versionCheckCommands.push( ...[ '/usr/bin', @@ -71,16 +88,31 @@ versionCheckCommands.some((cmd) => { const majorVersion = versionCheckOutput?.match?.(/(?:^|\s)(9\d|\d{3})\./)?.[1]; if (majorVersion) { + let targetVersion = `^${majorVersion}`; + + // TODO: Temporary fix to install chromedriver 112.0.0 if major version is 112. + // Exit if major version is greater than 112. + // Revert this once node is bumped to 16+. + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3975 + if (parseInt(majorVersion) === 112) { + targetVersion = '112.0.0'; + } else if (parseInt(majorVersion) > 112) { + console.error( + `::error::Chrome version (${majorVersion}) is not supported by this script. The largest chrome version we support is 112.` + ); + process.exit(1); + } + if (process.argv.includes('--install')) { - console.log(`Installing chromedriver@^${majorVersion}`); + console.log(`Installing chromedriver@${targetVersion}`); - spawnSync(`yarn add --dev chromedriver@^${majorVersion}`, { + spawnSync(`yarn add --dev chromedriver@${targetVersion}`, { stdio: 'inherit', cwd: process.cwd(), shell: true, }); } else { - console.log(`Upgrading to chromedriver@^${majorVersion}`); + console.log(`Upgrading to chromedriver@${targetVersion}`); let upgraded = false; const writeStream = createWriteStream('package.json.upgrading-chromedriver', { flags: 'w' }); @@ -92,7 +124,7 @@ if (majorVersion) { if (line.includes('"chromedriver": "')) { line = line.replace( /"chromedriver":\s*"[~^]?\d[\d.]*\d"/, - `"chromedriver": "^${majorVersion}"` + `"chromedriver": "${targetVersion}"` ); upgraded = true; } @@ -107,11 +139,11 @@ if (majorVersion) { renameSync('package.json', 'package.json.bak'); renameSync('package.json.upgrading-chromedriver', 'package.json'); - console.log(`Backed up package.json and updated chromedriver to ${majorVersion}`); + console.log(`Backed up package.json and updated chromedriver to ${targetVersion}`); } else { unlinkSync('package.json.upgrading-chromedriver'); console.error( - `Failed to update chromedriver to ${majorVersion}. Try adding the \`--install\` switch.` + `Failed to update chromedriver to ${targetVersion}. Try adding the \`--install\` switch.` ); } }); diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index fb3c63ffc71d..5950cb1fdfb0 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -36,7 +36,7 @@ import Del from 'del'; import * as Rx from 'rxjs'; import { map, filter, take } from 'rxjs/operators'; -import { safeDump } from 'js-yaml'; +import { dump } from 'js-yaml'; import { getConfigFromFiles } from '@osd/config'; const legacyConfig = follow('__fixtures__/reload_logging_config/opensearch_dashboards.test.yml'); @@ -89,7 +89,7 @@ function createConfigManager(configPath: string) { return { modify(fn: (input: Record) => Record) { const oldContent = getConfigFromFiles([configPath]); - const yaml = safeDump(fn(oldContent)); + const yaml = dump(fn(oldContent)); Fs.writeFileSync(configPath, yaml); }, }; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index c7e883d9b00b..4c2beb329a98 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -2798,6 +2798,7 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -2842,11 +2843,13 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -5414,6 +5417,7 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -5466,6 +5470,7 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -5507,11 +5512,13 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -8117,6 +8124,7 @@ exports[`Header renders condensed header 1`] = ` > @@ -8161,11 +8169,13 @@ exports[`Header renders condensed header 1`] = ` > @@ -10650,6 +10660,7 @@ exports[`Header renders condensed header 1`] = ` > @@ -10702,6 +10713,7 @@ exports[`Header renders condensed header 1`] = ` > @@ -10743,11 +10755,13 @@ exports[`Header renders condensed header 1`] = ` > diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap index 835c2d8d4a4e..036e2b4ee0ce 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap @@ -1624,6 +1624,7 @@ exports[`Header help menu hides survey link 1`] = ` > @@ -1676,6 +1677,7 @@ exports[`Header help menu hides survey link 1`] = ` > @@ -1717,11 +1719,13 @@ exports[`Header help menu hides survey link 1`] = ` > @@ -3780,6 +3784,7 @@ exports[`Header help menu renders survey link 1`] = ` > @@ -3832,6 +3837,7 @@ exports[`Header help menu renders survey link 1`] = ` > @@ -3873,11 +3879,13 @@ exports[`Header help menu renders survey link 1`] = ` > diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 917d1ebd4c46..a78371f4f264 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -173,7 +173,13 @@ export function Header({ aria-controls={navId} ref={toggleCollapsibleNavRef} > - + diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 6f536184edc1..0eba4c0c2673 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -330,7 +330,14 @@ class HeaderHelpMenuUI extends Component { })} onClick={this.onMenuButtonClick} > - + ); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index cefaa353f7fa..694372c46d99 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -31,6 +31,7 @@ import { omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; +import { isRelativeUrl } from '@osd/std'; import { IBasePath, @@ -144,7 +145,6 @@ export class Fetch { headers: removedUndefined({ 'Content-Type': 'application/json', ...options.headers, - 'osd-version': this.params.opensearchDashboardsVersion, }), }; @@ -158,6 +158,18 @@ export class Fetch { fetchOptions.headers['osd-system-request'] = 'true'; } + /* `osd-version` is used on the server-side to make sure that an incoming request originated from a front-end + * of the same version; see core/server/http/lifecycle_handlers.ts + * `osd-xsrf` is to satisfy XSRF protection but is only meaningful to OpenSearch Dashboards. + * + * If the `url` equals `basePath`, starts with `basePath` + '/', or is relative, add `osd-version` and `osd-xsrf` headers. + */ + const basePath = this.params.basePath.get(); + if (isRelativeUrl(url) || url === basePath || url.startsWith(`${basePath}/`)) { + fetchOptions.headers['osd-version'] = this.params.opensearchDashboardsVersion; + fetchOptions.headers['osd-xsrf'] = 'osd-fetch'; + } + return new Request(url, fetchOptions as RequestInit); } diff --git a/src/core/public/notifications/toasts/toasts_api.test.ts b/src/core/public/notifications/toasts/toasts_api.test.ts index ef4f469194d7..dacfa98b623a 100644 --- a/src/core/public/notifications/toasts/toasts_api.test.ts +++ b/src/core/public/notifications/toasts/toasts_api.test.ts @@ -105,6 +105,24 @@ describe('#get$()', () => { toasts.remove('bar'); expect(onToasts).not.toHaveBeenCalled(); }); + + it('does not emit a new toast list when a toast with the same id is passed to add()', () => { + const toasts = new ToastsApi(toastDeps()); + const onToasts = jest.fn(); + + toasts.get$().subscribe(onToasts); + toasts.add({ + id: 'foo', + title: 'foo', + }); + onToasts.mockClear(); + + toasts.add({ + id: 'foo', + title: 'bar', + }); + expect(onToasts).not.toHaveBeenCalled(); + }); }); describe('#add()', () => { @@ -135,6 +153,11 @@ describe('#add()', () => { const toasts = new ToastsApi(toastDeps()); expect(toasts.add('foo')).toHaveProperty('title', 'foo'); }); + + it('accepts an id and does not auto increment', async () => { + const toasts = new ToastsApi(toastDeps()); + expect(toasts.add({ id: 'foo', title: 'not foo' })).toHaveProperty('id', 'foo'); + }); }); describe('#remove()', () => { diff --git a/src/core/public/notifications/toasts/toasts_api.tsx b/src/core/public/notifications/toasts/toasts_api.tsx index 008c4fb3c507..76d0bf9cf9b2 100644 --- a/src/core/public/notifications/toasts/toasts_api.tsx +++ b/src/core/public/notifications/toasts/toasts_api.tsx @@ -42,12 +42,10 @@ import { I18nStart } from '../../i18n'; /** * Allowed fields for {@link ToastInput}. * - * @remarks - * `id` cannot be specified. - * * @public */ export type ToastInputFields = Pick> & { + id?: string; title?: string | MountPoint; text?: string | MountPoint; }; @@ -143,6 +141,16 @@ export class ToastsApi implements IToasts { * @returns a {@link Toast} */ public add(toastOrTitle: ToastInput) { + if (typeof toastOrTitle !== 'string') { + const toastObject = toastOrTitle; + const list = this.toasts$.getValue(); + const existingToast = list.find((toast) => toast.id === toastObject.id); + + if (existingToast) { + return existingToast; + } + } + const toast: Toast = { id: String(this.idCounter++), toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:info'), diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index e59008f08259..1333e48f6ca5 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -5,7 +5,6 @@ */ // SASSTODO: Naming here is too embedded and high up that changing them could cause major breaks #opensearch-dashboards-body { - overflow-x: hidden; min-height: 100%; } diff --git a/src/core/public/rendering/app_containers.test.tsx b/src/core/public/rendering/app_containers.test.tsx index fd43c79514c1..157253f5f757 100644 --- a/src/core/public/rendering/app_containers.test.tsx +++ b/src/core/public/rendering/app_containers.test.tsx @@ -43,6 +43,7 @@ describe('AppWrapper', () => { expect(component.getDOMNode()).toMatchInlineSnapshot(`

app-content
@@ -53,6 +54,7 @@ describe('AppWrapper', () => { expect(component.getDOMNode()).toMatchInlineSnapshot(`
app-content
@@ -63,6 +65,7 @@ describe('AppWrapper', () => { expect(component.getDOMNode()).toMatchInlineSnapshot(`
app-content
diff --git a/src/core/public/rendering/app_containers.tsx b/src/core/public/rendering/app_containers.tsx index dab85769bd0b..8ccf795f8dcf 100644 --- a/src/core/public/rendering/app_containers.tsx +++ b/src/core/public/rendering/app_containers.tsx @@ -37,7 +37,11 @@ export const AppWrapper: React.FunctionComponent<{ chromeVisible$: Observable; }> = ({ chromeVisible$, children }) => { const visible = useObservable(chromeVisible$); - return
{children}
; + return ( +
+ {children} +
+ ); }; export const AppContainer: React.FunctionComponent<{ diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap index f80d88dd91ff..52d04f52ff8b 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap @@ -9,6 +9,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -20,6 +21,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -36,6 +38,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -47,6 +50,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -63,6 +67,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -74,6 +79,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -113,6 +119,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index fc13c1ae3fbb..0742e8c08d6d 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -231,20 +231,23 @@ describe('core lifecycle handlers', () => { .expect(200, 'ok'); }); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection it('accepts requests with the version header', async () => { await getSupertest(method.toLowerCase(), testPath) .set(versionHeader, actualVersion) .expect(200, 'ok'); }); + // ToDo: Rename next; `osd-version` incorrectly used for satisfying XSRF protection it('rejects requests without either an xsrf or version header', async () => { await getSupertest(method.toLowerCase(), testPath).expect(400, { statusCode: 400, error: 'Bad Request', - message: 'Request must contain a osd-xsrf header.', + message: 'Request must contain the osd-xsrf header.', }); }); + // ToDo: Rename next; `osd-version` incorrectly used for satisfying XSRF protection it('accepts whitelisted requests without either an xsrf or version header', async () => { await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok'); }); diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index 6a49bbfa14fa..cd96f0ea7760 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -102,6 +102,7 @@ describe('xsrf post-auth handler', () => { expect(result).toEqual('next'); }); + // ToDo: Remove; `osd-version` incorrectly used for satisfying XSRF protection it('accepts requests with version header', () => { const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); @@ -129,7 +130,7 @@ describe('xsrf post-auth handler', () => { expect(responseFactory.badRequest).toHaveBeenCalledTimes(1); expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "body": "Request must contain a osd-xsrf header.", + "body": "Request must contain the osd-xsrf header.", } `); expect(result).toEqual('badRequest'); @@ -199,7 +200,7 @@ describe('versionCheck post-auth handler', () => { responseFactory = httpServerMock.createLifecycleResponseFactory(); }); - it('forward the request to the next interceptor if header matches', () => { + it('forward the request to the next interceptor if osd-version header matches the actual version', () => { const handler = createVersionCheckPostAuthHandler('actual-version'); const request = forgeRequest({ headers: { 'osd-version': 'actual-version' } }); @@ -212,7 +213,7 @@ describe('versionCheck post-auth handler', () => { expect(result).toBe('next'); }); - it('returns a badRequest error if header does not match', () => { + it('returns a badRequest error if osd-version header exists but does not match the actual version', () => { const handler = createVersionCheckPostAuthHandler('actual-version'); const request = forgeRequest({ headers: { 'osd-version': 'another-version' } }); @@ -236,7 +237,7 @@ describe('versionCheck post-auth handler', () => { expect(result).toBe('badRequest'); }); - it('forward the request to the next interceptor if header is not present', () => { + it('forward the request to the next interceptor if osd-version header is not present', () => { const handler = createVersionCheckPostAuthHandler('actual-version'); const request = forgeRequest({ headers: {} }); diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts index 636bb8af4522..f17b07942e6a 100644 --- a/src/core/server/http/lifecycle_handlers.ts +++ b/src/core/server/http/lifecycle_handlers.ts @@ -54,8 +54,9 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler const hasVersionHeader = VERSION_HEADER in request.headers; const hasXsrfHeader = XSRF_HEADER in request.headers; + // ToDo: Remove !hasVersionHeader; `osd-version` incorrectly used for satisfying XSRF protection if (!isSafeMethod(request.route.method) && !hasVersionHeader && !hasXsrfHeader) { - return response.badRequest({ body: `Request must contain a ${XSRF_HEADER} header.` }); + return response.badRequest({ body: `Request must contain the ${XSRF_HEADER} header.` }); } return toolkit.next(); 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 f3a3daa432c3..71553d39cd9c 100644 --- a/src/dev/build/tasks/patch_native_modules_task.test.ts +++ b/src/dev/build/tasks/patch_native_modules_task.test.ts @@ -9,8 +9,8 @@ import { createAnyInstanceSerializer, createAbsolutePathSerializer, } from '@osd/dev-utils'; -import { Build, Config } from '../lib'; -import { PatchNativeModules } from './patch_native_modules_task'; +import { Build, Config, read, download, untar, gunzip } from '../lib'; +import { createPatchNativeModulesTask } from './patch_native_modules_task'; const log = new ToolingLog(); const testWriter = new ToolingLogCollectingWriter(); @@ -19,16 +19,16 @@ expect.addSnapshotSerializer(createAnyInstanceSerializer(Config)); expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); expect.addSnapshotSerializer(createAbsolutePathSerializer()); -jest.mock('../lib/download'); -jest.mock('../lib/fs', () => ({ - ...jest.requireActual('../lib/fs'), - untar: jest.fn(), - gunzip: jest.fn(), -})); - -const { untar } = jest.requireMock('../lib/fs'); -const { gunzip } = jest.requireMock('../lib/fs'); -const { download } = jest.requireMock('../lib/download'); +jest.mock('../lib', () => { + const originalModule = jest.requireActual('../lib'); + return { + ...originalModule, + download: jest.fn(), + gunzip: jest.fn(), + untar: jest.fn(), + read: jest.fn(), + }; +}); async function setup() { const config = await Config.create({ @@ -38,14 +38,15 @@ async function setup() { linux: false, linuxArm: false, darwin: false, + windows: false, }, }); const build = new Build(config); - download.mockImplementation(() => {}); - untar.mockImplementation(() => {}); - gunzip.mockImplementation(() => {}); + (read as jest.MockedFunction).mockImplementation(async () => { + return JSON.stringify({ version: mockPackage.version }); + }); return { config, build }; } @@ -55,38 +56,77 @@ beforeEach(() => { jest.clearAllMocks(); }); -it('patch native modules task downloads the correct platform package', async () => { - const { config, build } = await setup(); - config.targetPlatforms.linuxArm = true; - await PatchNativeModules.run(config, log, build); - expect(download.mock.calls.length).toBe(1); - expect(download.mock.calls).toMatchInlineSnapshot(` +const mockPackage = { + name: 'mock-native-module', + version: '1.0.0', + destinationPath: 'path/to/destination', + extractMethod: 'untar', + archives: { + 'linux-arm64': { + url: 'https://example.com/mock-native-module/linux-arm64.tar.gz', + sha256: 'mock-sha256', + }, + 'linux-x64': { + url: 'https://example.com/mock-native-module/linux-x64.gz', + sha256: 'mock-sha256', + }, + }, +}; + +describe('patch native modules task', () => { + it('patch native modules task downloads the correct platform package', async () => { + const { config, build } = await setup(); + config.targetPlatforms.linuxArm = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackage]); + await PatchNativeModulesWithMock.run(config, log, build); + expect((download as jest.MockedFunction).mock.calls.length).toBe(1); + expect((download as jest.MockedFunction).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { - "destination": /.native_modules/re2/linux-arm64-83.tar.gz, + "destination": /.native_modules/mock-native-module/linux-arm64.tar.gz, "log": , "retries": 3, - "sha256": "d86ced75b794fbf518b90908847b3c09a50f3ff5a2815aa30f53080f926a2873", - "url": "https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.17.4/linux-arm64-83.tar.gz", + "sha256": "mock-sha256", + "url": "https://example.com/mock-native-module/linux-arm64.tar.gz", }, ], ] `); -}); + }); -it('for .tar.gz artifact, patch native modules task unzip it via untar', async () => { - const { config, build } = await setup(); - config.targetPlatforms.linuxArm = true; - await PatchNativeModules.run(config, log, build); - expect(untar.mock.calls.length).toBe(1); - expect(gunzip.mock.calls.length).toBe(0); -}); + it('for .tar.gz artifact, patch native modules task unzip it via untar', async () => { + const { config, build } = await setup(); + config.targetPlatforms.linuxArm = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackage]); + await PatchNativeModulesWithMock.run(config, log, build); + expect(untar).toHaveBeenCalled(); + expect(gunzip).not.toHaveBeenCalled(); + }); -it('for .gz artifact, patch native modules task unzip it via gunzip', async () => { - const { config, build } = await setup(); - config.targetPlatforms.linux = true; - await PatchNativeModules.run(config, log, build); - expect(untar.mock.calls.length).toBe(0); - expect(gunzip.mock.calls.length).toBe(1); + it('for .gz artifact, patch native modules task unzip it via gunzip', async () => { + const mockPackageGZ = { + ...mockPackage, + extractMethod: 'gunzip', + }; + const { config, build } = await setup(); + config.targetPlatforms.linux = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackageGZ]); + await PatchNativeModulesWithMock.run(config, log, build); + expect(gunzip).toHaveBeenCalled(); + expect(untar).not.toHaveBeenCalled(); + }); + + it('throws error for unsupported extract methods', async () => { + const mockPackageUnsupported = { + ...mockPackage, + extractMethod: 'unsupported', + }; + const { config, build } = await setup(); + config.targetPlatforms.linux = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackageUnsupported]); + await expect(PatchNativeModulesWithMock.run(config, log, build)).rejects.toThrow( + 'Extract method of unsupported is not supported' + ); + }); }); diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index 3bd9fa63c358..b8c8d8a5b9fb 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -52,45 +52,7 @@ 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.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.17.4/darwin-x64-83.gz', - sha256: '9112ed93c1544ecc6397f7ff20bd2b28f3b04c7fbb54024e10f9a376a132a87d', - }, - 'linux-x64': { - 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.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.17.4/win32-x64-83.gz', - sha256: '2f842d9757288afd4bd5dec0e7b370a4c3e89ac98050598b17abb9e8e00e3294', - }, - }, - }, -]; +export const packages: Package[] = []; async function getInstalledVersion(config: Config, packageName: string) { const packageJSONPath = config.resolveFromRepo( @@ -145,15 +107,20 @@ async function patchModule( } } -export const PatchNativeModules: Task = { - description: 'Patching platform-specific native modules', - async run(config, log, build) { - for (const pkg of packages) { - await Promise.all( - config.getTargetPlatforms().map(async (platform) => { - await patchModule(config, log, build, platform, pkg); - }) - ); - } - }, -}; +export function createPatchNativeModulesTask(customPackages?: Package[]): Task { + return { + description: 'Patching platform-specific native modules', + async run(config, log, build) { + const targetPackages = customPackages || packages; + for (const pkg of targetPackages) { + await Promise.all( + config.getTargetPlatforms().map(async (platform) => { + await patchModule(config, log, build, platform, pkg); + }) + ); + } + }, + }; +} + +export const PatchNativeModules = createPatchNativeModulesTask(); diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 65f244042a12..1add8e722a5a 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -53,7 +53,7 @@ export default class JestJUnitReporter { /** * Called by jest when all tests complete * @param {Object} contexts - * @param {JestResults} results see https://facebook.github.io/jest/docs/en/configuration.html#testresultsprocessor-string + * @param {JestResults} results see https://jestjs.io/docs/configuration/#testresultsprocessor-string * @return {undefined} */ onRunComplete(contexts, results) { diff --git a/src/dev/stylelint/lint_files.js b/src/dev/stylelint/lint_files.js index ead1fde5bb23..2d94f98b396c 100644 --- a/src/dev/stylelint/lint_files.js +++ b/src/dev/stylelint/lint_files.js @@ -30,13 +30,13 @@ import stylelint from 'stylelint'; import path from 'path'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import fs from 'fs'; import { createFailError } from '@osd/dev-utils'; // load the include globs from .stylelintrc.yml and convert them to regular expressions for filtering files const stylelintPath = path.resolve(__dirname, '..', '..', '..', '.stylelintrc.yml'); -const styleLintConfig = safeLoad(fs.readFileSync(stylelintPath)); +const styleLintConfig = load(fs.readFileSync(stylelintPath)); /** * Lints a list of files with eslint. eslint reports are written to the log diff --git a/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss b/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss index 82704fc93062..a33082c3ef64 100644 --- a/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss +++ b/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss @@ -76,7 +76,3 @@ .mgtAdvancedSettingsForm__button { width: 100%; } - -.osdBody--mgtAdvancedSettingsHasBottomBar .mgtPage__body { - padding-bottom: $euiSizeXL * 2; -} diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index c5a8a89157fe..4f17ab378d3c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -36,7 +36,7 @@ import { FieldSetting } from '../../types'; import { UiSettingsType, StringValidation } from '../../../../../../core/public'; import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { Field, getEditableValue } from './field'; jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 650d8b259f0c..a0edaa5ab602 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -29,15 +29,21 @@ */ import React from 'react'; +import ReactDOM from 'react-dom'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { UiSettingsType } from '../../../../../../core/public'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { notificationServiceMock } from '../../../../../../core/public/mocks'; import { SettingsChanges } from '../../types'; import { Form } from './form'; +jest.mock('react-dom', () => ({ + ...jest.requireActual('react-dom'), + createPortal: jest.fn((element) => element), +})); + jest.mock('../field', () => ({ Field: () => { return 'field'; @@ -45,6 +51,8 @@ jest.mock('../field', () => ({ })); beforeAll(() => { + ReactDOM.createPortal = jest.fn((children: any) => children); + const localStorage: Record = { 'core.chrome.isLocked': true, }; @@ -60,6 +68,7 @@ beforeAll(() => { }); afterAll(() => { + (ReactDOM.createPortal as jest.Mock).mockClear(); delete (window as any).localStorage; }); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 92b7a792d2d2..a74199771d2a 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -47,8 +47,9 @@ import { import { FormattedMessage } from '@osd/i18n/react'; import { isEmpty } from 'lodash'; import { i18n } from '@osd/i18n'; +import { DocLinksStart, ToastsStart } from 'opensearch-dashboards/public'; +import { createPortal } from 'react-dom'; import { toMountPoint } from '../../../../../opensearch_dashboards_react/public'; -import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; import { getCategoryName } from '../../lib'; import { Field, getEditableValue } from '../field'; @@ -336,63 +337,69 @@ export class Form extends PureComponent { }; renderBottomBar = () => { - const areChangesInvalid = this.areChangesInvalid(); - return ( - - - -

{this.renderCountOfUnsaved()}

-
- - - - {i18n.translate('advancedSettings.form.cancelButtonLabel', { - defaultMessage: 'Cancel changes', - })} - - - - - + + +

{this.renderCountOfUnsaved()}

+
+ + + - {i18n.translate('advancedSettings.form.saveButtonLabel', { - defaultMessage: 'Save changes', + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', })} -
-
-
-
-
- ); + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + + + + + + ); + + return createPortal(bottomBar, document.getElementById('app-wrapper')!); + } catch (e) { + return null; + } }; render() { @@ -401,12 +408,6 @@ export class Form extends PureComponent { const currentCategories: Category[] = []; const hasUnsavedChanges = !isEmpty(unsavedChanges); - if (hasUnsavedChanges) { - document.body.classList.add('osdBody--mgtAdvancedSettingsHasBottomBar'); - } else { - document.body.classList.remove('osdBody--mgtAdvancedSettingsHasBottomBar'); - } - categories.forEach((category) => { if (visibleSettings[category] && visibleSettings[category].length) { currentCategories.push(category); diff --git a/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx index 34b69888b3be..f362af408e0c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { Query } from '@elastic/eui'; import { Search } from './search'; diff --git a/src/plugins/advanced_settings/public/management_app/components/search/search.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx index a957fa793f8f..0b1de3bbfb24 100644 --- a/src/plugins/advanced_settings/public/management_app/components/search/search.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx @@ -31,7 +31,6 @@ import React, { Fragment, PureComponent } from 'react'; import { i18n } from '@osd/i18n'; import { EuiSearchBar, EuiFormErrorText, Query } from '@elastic/eui'; - import { getCategoryName } from '../../lib'; interface SearchProps { @@ -111,6 +110,7 @@ export class Search extends PureComponent { return ( + {/* @ts-ignore The Query types that typescript complains about here are identical and is a false flag. Once OUI migration is complete, this ignore can be removed */} {queryParseError} diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index 1a3a0bffc821..e115b35a44e1 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -95,6 +95,7 @@ export class BfetchPublicPlugin url: `${basePath}/${removeLeadingSlash(params.url)}`, headers: { 'Content-Type': 'application/json', + 'osd-xsrf': 'osd-bfetch', 'osd-version': version, ...(params.headers || {}), }, diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 876e94d4cbcb..1c47cc41e920 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -231,7 +231,6 @@ function EditorUI({ initialTextValue, dataSourceId }: EditorProps) { return (
-
    { @@ -45,10 +46,10 @@ describe('Input', () => { coreEditor = create(document.querySelector('#ConAppEditor')); - coreEditor.getContainer().style.display = ''; + $(coreEditor.getContainer()).show(); }); afterEach(() => { - coreEditor.getContainer().style.display = 'none'; + $(coreEditor.getContainer()).hide(); }); describe('.getLineCount', () => { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js index d143d72e15c2..4973011a2aaa 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js @@ -29,6 +29,7 @@ */ import '../legacy_core_editor.test.mocks'; +import $ from 'jquery'; import RowParser from '../../../../lib/row_parser'; import ace from 'brace'; import { createReadOnlyAceEditor } from '../create_readonly'; @@ -38,11 +39,11 @@ const tokenIterator = ace.acequire('ace/token_iterator'); describe('Output Tokenization', () => { beforeEach(() => { output = createReadOnlyAceEditor(document.querySelector('#ConAppOutput')); - output.container.style.display = ''; + $(output.container).show(); }); afterEach(() => { - output.container.style.display = 'none'; + $(output.container).hide(); }); function tokensAsList() { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 5fe93ca4e094..55ee5fe2a343 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -30,6 +30,7 @@ import ace from 'brace'; import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace'; +import $ from 'jquery'; import { CoreEditor, Position, @@ -53,11 +54,11 @@ const rangeToAceRange = ({ start, end }: Range) => export class LegacyCoreEditor implements CoreEditor { private _aceOnPaste: any; - actions: any; + $actions: any; resize: () => void; constructor(private readonly editor: IAceEditor, actions: HTMLElement) { - this.actions = actions; + this.$actions = $(actions); this.editor.setShowPrintMargin(false); const session = this.editor.getSession(); @@ -273,16 +274,20 @@ export class LegacyCoreEditor implements CoreEditor { private setActionsBar = (value?: any, topOrBottom: 'top' | 'bottom' = 'top') => { if (value === null) { - this.actions.style.visibility = 'hidden'; + this.$actions.css('visibility', 'hidden'); } else { if (topOrBottom === 'top') { - this.actions.style.bottom = 'auto'; - this.actions.style.top = value; - this.actions.style.visibility = 'visible'; + this.$actions.css({ + bottom: 'auto', + top: value, + visibility: 'visible', + }); } else { - this.actions.style.top = 'auto'; - this.actions.style.bottom = value; - this.actions.style.visibility = 'visible'; + this.$actions.css({ + top: 'auto', + bottom: value, + visibility: 'visible', + }); } } }; @@ -313,14 +318,14 @@ export class LegacyCoreEditor implements CoreEditor { } legacyUpdateUI(range: any) { - if (!this.actions) { + if (!this.$actions) { return; } if (range) { // elements are positioned relative to the editor's container // pageY is relative to page, so subtract the offset // from pageY to get the new top value - const offsetFromPage = this.editor.container.offsetTop; + const offsetFromPage = $(this.editor.container).offset()!.top; const startLine = range.start.lineNumber; const startColumn = range.start.column; const firstLine = this.getLineValue(startLine); @@ -340,11 +345,11 @@ export class LegacyCoreEditor implements CoreEditor { let offset = 0; if (isWrapping) { // Try get the line height of the text area in pixels. - const textArea = this.editor.container.querySelector('textArea'); + const textArea = $(this.editor.container.querySelector('textArea')!); const hasRoomOnNextLine = this.getLineValue(startLine).length < maxLineLength; if (textArea && hasRoomOnNextLine) { // Line height + the number of wraps we have on a line. - offset += this.getLineValue(startLine).length * textArea.getBoundingClientRect().height; + offset += this.getLineValue(startLine).length * textArea.height()!; } else { if (startLine > 1) { this.setActionsBar(getScreenCoords(startLine - 1)); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index 88f9acc27e7f..cf6df4d31b06 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -44,11 +44,11 @@ describe('Integration', () => { '
    '; senseEditor = create(document.querySelector('#ConAppEditor')); - senseEditor.getCoreEditor().getContainer().style.display = ''; + $(senseEditor.getCoreEditor().getContainer()).show(); senseEditor.autocomplete._test.removeChangeListener(); }); afterEach(() => { - senseEditor.getCoreEditor().getContainer().style.display = 'none'; + $(senseEditor.getCoreEditor().getContainer()).hide(); senseEditor.autocomplete._test.addChangeListener(); }); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js index de67ae1a8908..18d798c28c94 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js @@ -30,6 +30,7 @@ import '../sense_editor.test.mocks'; +import $ from 'jquery'; import _ from 'lodash'; import { create } from '../create'; @@ -50,11 +51,11 @@ describe('Editor', () => {
    `; input = create(document.querySelector('#ConAppEditor')); - input.getCoreEditor().getContainer().style.display = ''; + $(input.getCoreEditor().getContainer()).show(); input.autocomplete._test.removeChangeListener(); }); afterEach(function () { - input.getCoreEditor().getContainer().style.display = 'none'; + $(input.getCoreEditor().getContainer()).hide(); input.autocomplete._test.addChangeListener(); }); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts index 92e86d60104c..6474fcb0ec9d 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts @@ -31,3 +31,11 @@ /* eslint no-undef: 0 */ import '../legacy_core_editor/legacy_core_editor.test.mocks'; + +import jQuery from 'jquery'; +jest.spyOn(jQuery, 'ajax').mockImplementation( + () => + new Promise(() => { + // never resolve + }) as any +); diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts index a933fdeb8010..55c18381cb9c 100644 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts +++ b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts @@ -30,6 +30,8 @@ import '../../application/models/sense_editor/sense_editor.test.mocks'; +import $ from 'jquery'; + // TODO: // We import from application models as a convenient way to bootstrap loading up of an editor using // this lib. We also need to import application specific mocks which is not ideal. @@ -57,14 +59,14 @@ describe('Ace (legacy) token provider', () => { senseEditor = create(document.querySelector('#ConAppEditor')!); - senseEditor.getCoreEditor().getContainer().style.display = ''; + $(senseEditor.getCoreEditor().getContainer())!.show(); (senseEditor as any).autocomplete._test.removeChangeListener(); tokenProvider = senseEditor.getCoreEditor().getTokenProvider(); }); afterEach(async () => { - senseEditor.getCoreEditor().getContainer().style.display = 'none'; + $(senseEditor.getCoreEditor().getContainer())!.hide(); (senseEditor as any).autocomplete._test.addChangeListener(); await senseEditor.update('', true); }); diff --git a/src/plugins/console/public/lib/osd/osd.js b/src/plugins/console/public/lib/osd/osd.js index cf8126271072..529fba754a93 100644 --- a/src/plugins/console/public/lib/osd/osd.js +++ b/src/plugins/console/public/lib/osd/osd.js @@ -38,6 +38,7 @@ import { UsernameAutocompleteComponent, } from '../autocomplete/components'; +import $ from 'jquery'; import _ from 'lodash'; import Api from './api'; @@ -173,19 +174,20 @@ function loadApisFromJson( // like this, it looks like a minor security issue. export function setActiveApi(api) { if (!api) { - fetch('../api/console/api_server', { - method: 'GET', + $.ajax({ + url: '../api/console/api_server', + dataType: 'json', // disable automatic guessing headers: { 'osd-xsrf': 'opensearch-dashboards', }, - }) - .then(function (response) { - response.json(); - }) - .then(function (data) { + }).then( + function (data) { setActiveApi(loadApisFromJson(data)); - }) - .catch((error) => console.log(`failed to load API '${api}': ${error}`)); + }, + function (jqXHR) { + console.log("failed to load API '" + api + "': " + jqXHR.responseText); + } + ); return; } diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index c149832ec1db..161d1f200afd 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -77,18 +77,6 @@ z-index: $euiZLevel1; } -// SASSTODO: This component seems to not be used anymore? -// Possibly replaced by the Ace version -.conApp__autoComplete { - position: absolute; - left: -1000px; - visibility: hidden; - - /* by pass any other element in ace and resize bar, but not modal popups */ - z-index: $euiZLevel1 + 2; - margin-top: 22px; -} - .conApp__settingsModal { min-width: 460px; } diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index e527565b3c4e..35899cddf69d 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -37,6 +37,7 @@ import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@osd/i18n/angular'; import { ChromeStart, + ToastsStart, IUiSettingsClient, CoreStart, SavedObjectsClientContract, @@ -81,6 +82,7 @@ export interface RenderDeps { }; uiSettings: IUiSettingsClient; chrome: ChromeStart; + addBasePath: (path: string) => string; savedQueryService: DataPublicPluginStart['query']['savedQueries']; embeddable: EmbeddableStart; localStorage: Storage; @@ -92,6 +94,7 @@ export interface RenderDeps { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedObjects: SavedObjectsStart; restorePreviousUrl: () => void; + toastNotifications: ToastsStart; } let angularModuleInstance: IModule | null = null; diff --git a/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx b/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx index fd4291b29455..ac22b069135c 100644 --- a/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx +++ b/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { coreMock } from '../../../../core/public/mocks'; describe('DashboardEmptyScreen', () => { diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index b68958143275..0c10653d7f41 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -114,7 +114,6 @@ export function initDashboardApp(app, deps) { deps.core.chrome.docTitle.change( i18n.translate('dashboard.dashboardPageTitle', { defaultMessage: 'Dashboards' }) ); - const service = deps.savedDashboards; const dashboardConfig = deps.dashboardConfig; // syncs `_g` portion of url with query services @@ -151,7 +150,7 @@ export function initDashboardApp(app, deps) { type: $scope.dashboardListTypes, search: search ? `${search}*` : undefined, fields: ['title', 'type', 'description', 'updated_at'], - perPage: $scope.initialPageSize, + perPage: $scope.listingLimit, page: 1, searchFields: ['title^3', 'type', 'description'], defaultSearchOperator: 'AND', @@ -164,14 +163,33 @@ export function initDashboardApp(app, deps) { }; }; - $scope.editItem = ({ editUrl }) => { - history.push(deps.addBasePath(editUrl)); + $scope.editItem = ({ appId, editUrl }) => { + if (appId === 'dashboard') { + history.push(editUrl); + } else { + deps.core.application.navigateToUrl(editUrl); + } }; - $scope.viewItem = ({ viewUrl }) => { - history.push(deps.addBasePath(viewUrl)); + $scope.viewItem = ({ appId, viewUrl }) => { + if (appId === 'dashboard') { + history.push(viewUrl); + } else { + deps.core.application.navigateToUrl(viewUrl); + } }; $scope.delete = (dashboards) => { - return service.delete(dashboards.map((d) => d.id)); + const ids = dashboards.map((d) => ({ id: d.id, appId: d.appId })); + return Promise.all( + ids.map(({ id, appId }) => { + return deps.savedObjectsClient.delete(appId, id); + }) + ).catch((error) => { + deps.toastNotifications.addError(error, { + title: i18n.translate('dashboard.dashboardListingDeleteErrorTitle', { + defaultMessage: 'Error deleting dashboard', + }), + }); + }); }; $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); $scope.initialFilter = parse(history.location.search).filter || EMPTY_FILTER; diff --git a/src/plugins/dashboard/public/application/listing/create_button.test.tsx b/src/plugins/dashboard/public/application/listing/create_button.test.tsx index 5d2a200f55df..9521df8590e6 100644 --- a/src/plugins/dashboard/public/application/listing/create_button.test.tsx +++ b/src/plugins/dashboard/public/application/listing/create_button.test.tsx @@ -57,7 +57,7 @@ describe('create button with props', () => { expect(createButtons.length).toBe(0); const createDropdown = findTestSubject(component, 'createMenuDropdown'); createDropdown.simulate('click'); - const contextMenus = findTestSubject(component, 'contextMenuItem'); + const contextMenus = findTestSubject(component, 'contextMenuItem-test'); expect(contextMenus.length).toBe(2); expect(contextMenus.at(0).prop('href')).toBe('test1'); }); diff --git a/src/plugins/dashboard/public/application/listing/create_button.tsx b/src/plugins/dashboard/public/application/listing/create_button.tsx index 04e6df883779..4959603fa271 100644 --- a/src/plugins/dashboard/public/application/listing/create_button.tsx +++ b/src/plugins/dashboard/public/application/listing/create_button.tsx @@ -38,7 +38,7 @@ const CreateButton = (props: CreateButtonProps) => { {provider.createLinkText} diff --git a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index d3198f6e1e31..2746997947c9 100644 --- a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx @@ -28,7 +28,7 @@ * under the License. */ -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import React from 'react'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 56388cfc64e5..87f934925899 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -344,7 +344,7 @@ export class DashboardPlugin registerDashboardProvider({ savedObjectsType: 'dashboard', savedObjectsName: 'Dashboard', - appId: 'dashboards', + appId: 'dashboard', viewUrlPathFn: (obj) => `#/view/${obj.id}`, editUrlPathFn: (obj) => `/view/${obj.id}?_a=(viewMode:edit)`, createUrl: core.http.basePath.prepend('/app/dashboards#/create'), diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx index 1ff83cf474c2..9a6b71ae6459 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx @@ -34,7 +34,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import ShardFailureOpenModalButton from './shard_failure_open_modal_button'; import { shardFailureRequest } from './__mocks__/shard_failure_request'; import { shardFailureResponse } from './__mocks__/shard_failure_response'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('ShardFailureOpenModalButton', () => { it('triggers the openModal function when "Show details" button is clicked', () => { diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index 366e5a0f3f55..8763c5306c15 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -25,6 +25,7 @@ export interface SigV4Content extends SavedObjectAttributes { accessKey: string; secretKey: string; region: string; + service?: SigV4ServiceName; } export interface UsernamePasswordTypedContent extends SavedObjectAttributes { @@ -37,3 +38,8 @@ export enum AuthType { UsernamePasswordType = 'username_password', SigV4 = 'sigv4', } + +export enum SigV4ServiceName { + OpenSearch = 'es', + OpenSearchServerless = 'aoss', +} diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts index 1fc4e00c3e23..09ce35978921 100644 --- a/src/plugins/data_source/config.ts +++ b/src/plugins/data_source/config.ts @@ -37,6 +37,7 @@ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), appender: fileAppenderSchema, }), + endpointDeniedIPs: schema.maybe(schema.arrayOf(schema.string())), }); export type DataSourcePluginConfigType = TypeOf; diff --git a/src/plugins/data_source/opensearch_dashboards.json b/src/plugins/data_source/opensearch_dashboards.json index 71183a411c79..871858403cf3 100644 --- a/src/plugins/data_source/opensearch_dashboards.json +++ b/src/plugins/data_source/opensearch_dashboards.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": [], - "optionalPlugins": [] + "optionalPlugins": [], + "extraPublicDirs": ["common/data_sources"] } 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 1499ccd411c2..aa367f0a6f89 100644 --- a/src/plugins/data_source/server/client/configure_client.test.ts +++ b/src/plugins/data_source/server/client/configure_client.test.ts @@ -167,6 +167,30 @@ describe('configureClient', () => { expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(2); }); + test('configure client with auth.type == sigv4, service == aoss, should successfully call new Client()', async () => { + savedObjectsMock.get.mockReset().mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: { + ...dataSourceAttr, + auth: { + type: AuthType.SigV4, + credentials: { ...sigV4AuthContent, service: 'aoss' }, + }, + }, + references: [], + }); + + jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ + decryptedText: 'accessKey', + encryptionContext: { endpoint: 'http://localhost' }, + }); + + await configureClient(dataSourceClientParams, clientPoolSetup, config, logger); + + expect(ClientMock).toHaveBeenCalledTimes(1); + }); + 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', diff --git a/src/plugins/data_source/server/client/configure_client.ts b/src/plugins/data_source/server/client/configure_client.ts index 8b43ffa80b23..77a0e067bbc0 100644 --- a/src/plugins/data_source/server/client/configure_client.ts +++ b/src/plugins/data_source/server/client/configure_client.ts @@ -160,7 +160,7 @@ const getBasicAuthClient = ( }; const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): Client => { - const { accessKey, secretKey, region } = credential; + const { accessKey, secretKey, region, service } = credential; const credentialProvider = (): Promise => { return new Promise((resolve) => { @@ -172,6 +172,7 @@ const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): C ...AwsSigv4Signer({ region, getCredentials: credentialProvider, + service, }), ...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 index 3ef8acc97b58..2ca6c6f2a83f 100644 --- a/src/plugins/data_source/server/client/configure_client_utils.ts +++ b/src/plugins/data_source/server/client/configure_client_utils.ts @@ -91,7 +91,7 @@ export const getAWSCredential = async ( cryptography: CryptographyServiceSetup ): Promise => { const { endpoint } = dataSource; - const { accessKey, secretKey, region } = dataSource.auth.credentials! as SigV4Content; + const { accessKey, secretKey, region, service } = dataSource.auth.credentials! as SigV4Content; const { decryptedText: accessKeyText, @@ -122,6 +122,7 @@ export const getAWSCredential = async ( region, accessKey: accessKeyText, secretKey: secretKeyText, + service, }; return credential; 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 c047da70b285..59c110d06dc5 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 @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from '../../../../core/server'; import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; -import { AuthType, DataSourceAttributes } from '../../common/data_sources'; +import { AuthType, DataSourceAttributes, SigV4Content } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; import { cryptographyServiceSetupMock } from '../cryptography_service.mocks'; import { CryptographyServiceSetup } from '../cryptography_service'; @@ -27,6 +27,7 @@ describe('configureLegacyClient', () => { let clientPoolSetup: OpenSearchClientPoolSetup; let configOptions: ConfigOptions; let dataSourceAttr: DataSourceAttributes; + let sigV4AuthContent: SigV4Content; let mockOpenSearchClientInstance: { close: jest.Mock; @@ -71,6 +72,12 @@ describe('configureLegacyClient', () => { }, } as DataSourceAttributes; + sigV4AuthContent = { + region: 'us-east-1', + accessKey: 'accessKey', + secretKey: 'secretKey', + }; + clientPoolSetup = { getClientFromPool: jest.fn(), addClientToPool: jest.fn(), @@ -157,6 +164,42 @@ describe('configureLegacyClient', () => { expect(mockResult).toBeDefined(); }); + test('configure client with auth.type == sigv4 and service param, should call new Client() with service param', async () => { + savedObjectsMock.get.mockReset().mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: { + ...dataSourceAttr, + auth: { + type: AuthType.SigV4, + credentials: { ...sigV4AuthContent, service: 'aoss' }, + }, + }, + references: [], + }); + + parseClientOptionsMock.mockReturnValue(configOptions); + + jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ + decryptedText: 'accessKey', + encryptionContext: { endpoint: 'http://localhost' }, + }); + + await configureLegacyClient( + dataSourceClientParams, + callApiParams, + clientPoolSetup, + config, + logger + ); + + expect(parseClientOptionsMock).toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).toHaveBeenCalledWith(expect.objectContaining({ service: 'aoss' })); + + expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); + }); + test('configure client with auth.type == username_password and password contaminated', async () => { const decodeAndDecryptSpy = jest .spyOn(cryptographyMock, 'decodeAndDecrypt') 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 3a9b65634a28..0d074cf77d4a 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.ts @@ -5,10 +5,9 @@ import { Client } from '@opensearch-project/opensearch'; import { Client as LegacyClient, ConfigOptions } from 'elasticsearch'; -import { Credentials } from 'aws-sdk'; +import { Credentials, Config } from 'aws-sdk'; import { get } from 'lodash'; import HttpAmazonESConnector from 'http-aws-es'; -import { Config } from 'aws-sdk'; import { Headers, LegacyAPICaller, @@ -27,7 +26,7 @@ import { CryptographyServiceSetup } from '../cryptography_service'; import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types'; import { OpenSearchClientPoolSetup } from '../client'; import { parseClientOptions } from './client_config'; -import { createDataSourceError, DataSourceError } from '../lib/error'; +import { createDataSourceError } from '../lib/error'; import { getRootClient, getAWSCredential, @@ -195,13 +194,14 @@ const getBasicAuthClient = async ( }; const getAWSClient = (credential: SigV4Content, clientOptions: ConfigOptions): LegacyClient => { - const { accessKey, secretKey, region } = credential; + const { accessKey, secretKey, region, service } = credential; const client = new LegacyClient({ connectionClass: HttpAmazonESConnector, awsConfig: new Config({ region, credentials: new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey }), }), + service, ...clientOptions, }); return client; diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index e038a0f7685e..0f3c47be4b4c 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -58,7 +58,8 @@ export class DataSourcePlugin implements Plugin(); + // Amazon OpenSearch Serverless does not support .info() API + if (this.dataSourceAttr.auth?.credentials?.service === SigV4ServiceName.OpenSearchServerless) + return await this.callDataCluster.cat.indices(); + 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 index f36702171b45..cba42517e535 100644 --- a/src/plugins/data_source/server/routes/test_connection.ts +++ b/src/plugins/data_source/server/routes/test_connection.ts @@ -5,7 +5,7 @@ import { schema } from '@osd/config-schema'; import { IRouter, OpenSearchClient } from 'opensearch-dashboards/server'; -import { AuthType, DataSourceAttributes } from '../../common/data_sources'; +import { AuthType, DataSourceAttributes, SigV4ServiceName } from '../../common/data_sources'; import { DataSourceConnectionValidator } from './data_source_connection_validator'; import { DataSourceServiceSetup } from '../data_source_service'; import { CryptographyServiceSetup } from '../cryptography_service'; @@ -40,6 +40,10 @@ export const registerTestConnectionRoute = ( region: schema.string(), accessKey: schema.string(), secretKey: schema.string(), + service: schema.oneOf([ + schema.literal(SigV4ServiceName.OpenSearch), + schema.literal(SigV4ServiceName.OpenSearchServerless), + ]), }), ]) ), @@ -61,9 +65,13 @@ export const registerTestConnectionRoute = ( testClientDataSourceAttr: dataSourceAttr as DataSourceAttributes, } ); - const dsValidator = new DataSourceConnectionValidator(dataSourceClient); - await dsValidator.validate(); + const dataSourceValidator = new DataSourceConnectionValidator( + dataSourceClient, + dataSourceAttr + ); + + await dataSourceValidator.validate(); return response.ok({ body: { 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 6b79248d1a94..12d60b8da51e 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 @@ -24,14 +24,13 @@ import { UsernamePasswordTypedContent, } from '../../common/data_sources'; import { EncryptionContext, CryptographyServiceSetup } from '../cryptography_service'; +import { isValidURL } from '../util/endpoint_validator'; /** * Describes the Credential Saved Objects Client Wrapper class, * which contains the factory used to create Saved Objects Client Wrapper instances */ export class DataSourceSavedObjectsClientWrapper { - constructor(private cryptography: CryptographyServiceSetup, private logger: Logger) {} - /** * Describes the factory used to create instances of Saved Objects Client Wrappers * for data source specific operations such as credentials encryption @@ -138,14 +137,11 @@ export class DataSourceSavedObjectsClientWrapper { }; }; - private isValidUrl(endpoint: string) { - try { - const url = new URL(endpoint); - return Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:'); - } catch (e) { - return false; - } - } + constructor( + private cryptography: CryptographyServiceSetup, + private logger: Logger, + private endpointBlockedIps?: string[] + ) {} private async validateAndEncryptAttributes(attributes: T) { this.validateAttributes(attributes); @@ -254,8 +250,10 @@ export class DataSourceSavedObjectsClientWrapper { ); } - if (!this.isValidUrl(endpoint)) { - throw SavedObjectsErrorHelpers.createBadRequestError('"endpoint" attribute is not valid'); + if (!isValidURL(endpoint, this.endpointBlockedIps)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"endpoint" attribute is not valid or allowed' + ); } if (!auth) { @@ -303,7 +301,7 @@ export class DataSourceSavedObjectsClientWrapper { ); } - const { accessKey, secretKey, region } = credentials as SigV4Content; + const { accessKey, secretKey, region, service } = credentials as SigV4Content; if (!accessKey) { throw SavedObjectsErrorHelpers.createBadRequestError( @@ -322,6 +320,12 @@ export class DataSourceSavedObjectsClientWrapper { '"auth.credentials.region" attribute is required' ); } + + if (!service) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"auth.credentials.service" attribute is required' + ); + } break; default: throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`); @@ -459,7 +463,7 @@ export class DataSourceSavedObjectsClientWrapper { private async encryptSigV4Credential(auth: T, encryptionContext: EncryptionContext) { const { - credentials: { accessKey, secretKey, region }, + credentials: { accessKey, secretKey, region, service }, } = auth; return { @@ -468,6 +472,7 @@ export class DataSourceSavedObjectsClientWrapper { region, accessKey: await this.cryptography.encryptAndEncode(accessKey, encryptionContext), secretKey: await this.cryptography.encryptAndEncode(secretKey, encryptionContext), + service, }, }; } diff --git a/src/plugins/data_source/server/util/endpoint_validator.test.js b/src/plugins/data_source/server/util/endpoint_validator.test.js new file mode 100644 index 000000000000..618bf52d4d95 --- /dev/null +++ b/src/plugins/data_source/server/util/endpoint_validator.test.js @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as validator from './endpoint_validator'; + +describe('endpoint_validator', function () { + it('Url1 that should be blocked should return false', function () { + expect(validator.isValidURL('http://127.0.0.1', ['127.0.0.0/8'])).toEqual(false); + }); + + it('Url2 that is invalid should return false', function () { + expect(validator.isValidURL('www.test.com', [])).toEqual(false); + }); + + it('Url3 that is invalid should return false', function () { + expect(validator.isValidURL('ftp://www.test.com', [])).toEqual(false); + }); + + it('Url4 that should be blocked should return false', function () { + expect( + validator.isValidURL('http://169.254.169.254/latest/meta-data/', ['169.254.0.0/16']) + ).toEqual(false); + }); + + it('Url5 that should not be blocked should return true', function () { + expect(validator.isValidURL('https://www.opensearch.org', ['127.0.0.0/8'])).toEqual(true); + }); + + it('Url6 that should not be blocked should return true when null IPs', function () { + expect(validator.isValidURL('https://www.opensearch.org')).toEqual(true); + }); +}); diff --git a/src/plugins/data_source/server/util/endpoint_validator.ts b/src/plugins/data_source/server/util/endpoint_validator.ts new file mode 100644 index 000000000000..1c032037d2f5 --- /dev/null +++ b/src/plugins/data_source/server/util/endpoint_validator.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dns from 'dns-sync'; +import IPCIDR from 'ip-cidr'; + +export function isValidURL(endpoint: string, deniedIPs?: string[]) { + // Check the format of URL, URL has be in the format as + // scheme://server/path/resource otherwise an TypeError + // would be thrown. + let url; + try { + url = new URL(endpoint); + } catch (err) { + return false; + } + + if (!(Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:'))) { + return false; + } + + const ip = getIpAddress(url); + if (!ip) { + return false; + } + + // IP CIDR check if a specific IP address fall in the + // range of an IP address block + for (const deniedIP of deniedIPs ?? []) { + const cidr = new IPCIDR(deniedIP); + if (cidr.contains(ip)) { + return false; + } + } + return true; +} + +/** + * Resolve hostname to IP address + * @param {object} urlObject + * @returns {string} configuredIP + * or null if it cannot be resolve + * According to RFC, all IPv6 IP address needs to be in [] + * such as [::1]. + * So if we detect a IPv6 address, we remove brackets. + */ +function getIpAddress(urlObject: URL) { + const hostname = urlObject.hostname; + const configuredIP = dns.resolve(hostname); + if (configuredIP) { + return configuredIP; + } + if (hostname.startsWith('[') && hostname.endsWith(']')) { + return hostname.substr(1).slice(0, -1); + } + return null; +} 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 efb3453b6f35..ad1d02c87db4 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 @@ -27,6 +27,7 @@ describe('Datasource Management: Create Datasource form', () => { let component: ReactWrapper, React.Component<{}, {}, any>>; const mockSubmitHandler = jest.fn(); const mockTestConnectionHandler = jest.fn(); + const mockCancelHandler = jest.fn(); const getFields = (comp: ReactWrapper, React.Component<{}, {}, any>>) => { return { @@ -65,6 +66,7 @@ describe('Datasource Management: Create Datasource form', () => { ), 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 1c08da5d6371..8b36e13e7deb 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 @@ -5,7 +5,9 @@ import React from 'react'; import { + EuiBottomBar, EuiButton, + EuiButtonEmpty, EuiFieldPassword, EuiFieldText, EuiFlexGroup, @@ -19,13 +21,14 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { AuthType, credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, UsernamePasswordTypedContent, - SigV4Content, + sigV4ServiceOptions, } from '../../../../types'; import { Header } from '../header'; import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; @@ -41,6 +44,7 @@ export interface CreateDataSourceProps { existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; handleTestConnection: (formValues: DataSourceAttributes) => void; + handleCancel: () => void; } export interface CreateDataSourceState { /* Validation */ @@ -121,7 +125,31 @@ export class CreateDataSourceForm extends React.Component< }; onChangeAuthType = (e: React.ChangeEvent) => { - this.setState({ auth: { ...this.state.auth, type: e.target.value as AuthType } }); + const authType = e.target.value as AuthType; + this.setState({ + auth: { + ...this.state.auth, + type: authType, + credentials: { + ...this.state.auth.credentials, + service: + (this.state.auth.credentials.service as SigV4ServiceName) || + SigV4ServiceName.OpenSearch, + }, + }, + }); + }; + + onChangeSigV4ServiceName = (e: React.ChangeEvent) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { + ...this.state.auth.credentials, + service: e.target.value as SigV4ServiceName, + }, + }, + }); }; onChangeUsername = (e: { target: { value: any } }) => { @@ -267,6 +295,7 @@ export class CreateDataSourceForm extends React.Component< region: this.state.auth.credentials.region, accessKey: this.state.auth.credentials.accessKey, secretKey: this.state.auth.credentials.secretKey, + service: this.state.auth.credentials.service || SigV4ServiceName.OpenSearch, } as SigV4Content; } @@ -391,6 +420,19 @@ export class CreateDataSourceForm extends React.Component< data-test-subj="createDataSourceFormRegionField" /> + + this.onChangeSigV4ServiceName(e)} + name="ServiceName" + data-test-subj="createDataSourceFormAuthTypeSelect" + /> + { return ( - - {this.renderHeader()} - - - {/* Endpoint section */} - {this.renderSectionHeader( - 'dataSourceManagement.connectToDataSource.connectionDetails', - 'Connection Details' - )} - - - {/* Title */} - - + + {this.renderHeader()} + + + {/* Endpoint section */} + {this.renderSectionHeader( + 'dataSourceManagement.connectToDataSource.connectionDetails', + 'Connection Details' + )} + + + {/* Title */} + - + error={this.state.formErrorsByField.title} + > + + - {/* Description */} - - - - - {/* Endpoint URL */} - - + + + + {/* Endpoint URL */} + - + error={this.state.formErrorsByField.endpoint} + > + + - {/* Authentication Section: */} + {/* Authentication Section: */} - + - {this.renderSectionHeader( - 'dataSourceManagement.connectToDataSource.authenticationHeader', - 'Authentication Method' - )} - + {this.renderSectionHeader( + 'dataSourceManagement.connectToDataSource.authenticationHeader', + 'Authentication Method' + )} + - - - - + + - - - - - {/* Credential source */} - - - this.onChangeAuthType(e)} - name="Credential" - data-test-subj="createDataSourceFormAuthTypeSelect" - /> - - - {/* Create New credentials */} - {this.state.auth.type === AuthType.UsernamePasswordType - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} - - {this.state.auth.type === AuthType.SigV4 - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} - - - - - - {/* Test Connection button*/} - - - - - {/* Create Data Source button*/} - - + - - - - - - + + + + + {/* Credential source */} + + + this.onChangeAuthType(e)} + name="Credential" + data-test-subj="createDataSourceFormAuthTypeSelect" + /> + + + {/* Create New credentials */} + {this.state.auth.type === AuthType.UsernamePasswordType + ? this.renderCreateNewCredentialsForm(this.state.auth.type) + : null} + + {this.state.auth.type === AuthType.SigV4 + ? this.renderCreateNewCredentialsForm(this.state.auth.type) + : null} + + + + + + {/* Test Connection button*/} + + + + + + + + + + + {this.renderBottomBar()} + + ); + }; + + renderBottomBar = () => { + return ( + + + + + + + + + + + + + + + + ); }; 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 162af4c891f7..adfbe8808637 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,6 +46,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); component.update(); }); + test('should create datasource successfully', async () => { spyOn(utils, 'createSingleDataSource').and.returnValue({}); @@ -58,6 +59,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { expect(utils.createSingleDataSource).toHaveBeenCalled(); expect(history.push).toBeCalledWith(''); }); + test('should fail to create datasource', async () => { spyOn(utils, 'createSingleDataSource').and.throwError('error'); await act(async () => { @@ -93,7 +95,17 @@ describe('Datasource Management: Create Datasource Wizard', () => { component.update(); expect(utils.testConnection).toHaveBeenCalled(); }); + + test('should go back to listing page if clicked on cancel button', async () => { + await act(async () => { + // @ts-ignore + await component.find(formIdentifier).first().prop('handleCancel')(); + }); + + expect(history.push).toBeCalledWith(''); + }); }); + describe('case2: should fail to load resources', () => { beforeEach(async () => { spyOn(utils, 'getDataSources').and.throwError(''); @@ -116,6 +128,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); component.update(); }); + test('should not render component and go back to listing page', () => { 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 83477b7a2426..05489ca6258a 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 @@ -116,6 +116,7 @@ export const CreateDataSourceWizard: React.FunctionComponent props.history.push('')} existingDatasourceNamesList={existingDatasourceNamesList} /> {isLoading ? : null} 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 8e3128c7e987..2ea63e252295 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 @@ -23,13 +23,14 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { Header } from '../header'; import { AuthType, credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, - SigV4Content, + sigV4ServiceOptions, ToastMessageItem, UsernamePasswordTypedContent, } from '../../../../types'; @@ -46,9 +47,9 @@ import { UpdateAwsCredentialModal } from '../update_aws_credential_modal'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; existingDatasourceNamesList: string[]; - handleSubmit: (formValues: DataSourceAttributes) => void; - handleTestConnection: (formValues: DataSourceAttributes) => void; - onDeleteDataSource?: () => void; + handleSubmit: (formValues: DataSourceAttributes) => Promise; + handleTestConnection: (formValues: DataSourceAttributes) => Promise; + onDeleteDataSource?: () => Promise; displayToastMessage: (info: ToastMessageItem) => void; } export interface EditDataSourceState { @@ -123,7 +124,10 @@ export class EditDataSourceForm extends React.Component { const isValid = !!this.state.auth.credentials.username?.trim().length; this.setState({ @@ -221,6 +226,18 @@ export class EditDataSourceForm extends React.Component) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { + ...this.state.auth.credentials, + service: e.target.value as SigV4ServiceName, + } as SigV4Content, + }, + }); + }; + onChangeRegion = (e: { target: { value: any } }) => { this.setState({ auth: { @@ -253,7 +270,7 @@ export class EditDataSourceForm extends React.Component { - const isValid = !!this.state.auth.credentials.accessKey; + const isValid = !!this.state.auth.credentials?.accessKey; this.setState({ formErrorsByField: { ...this.state.formErrorsByField, @@ -275,7 +292,7 @@ export class EditDataSourceForm extends React.Component { - const isValid = !!this.state.auth.credentials.secretKey; + const isValid = !!this.state.auth.credentials?.secretKey; this.setState({ formErrorsByField: { ...this.state.formErrorsByField, @@ -338,9 +355,9 @@ export class EditDataSourceForm extends React.Component { + onClickDeleteDataSource = async () => { if (this.props.onDeleteDataSource) { - this.props.onDeleteDataSource(); + await this.props.onDeleteDataSource(); } }; @@ -359,6 +376,7 @@ export class EditDataSourceForm extends React.Component @@ -788,6 +807,19 @@ export class EditDataSourceForm extends React.Component + + this.onChangeSigV4ServiceName(e)} + name="ServiceName" + data-test-subj="createDataSourceFormAuthTypeSelect" + /> + void; closeUpdateAwsCredentialModal: () => void; } export const UpdateAwsCredentialModal = ({ region, + service, handleUpdateAwsCredential, closeUpdateAwsCredentialModal, }: UpdateAwsCredentialModalProps) => { @@ -87,6 +91,16 @@ export const UpdateAwsCredentialModal = ({ + {/* Service Name */} + + + {sigV4ServiceOptions.find((option) => option.value === service)?.text} + + {/* Region */} @@ -733,7 +733,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Confirm Updated password" - spellCheck={false} + spellCheck="false" type="password" value="" /> 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 1abde2d54edb..aecf6e517305 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 @@ -20,6 +20,7 @@ export interface CreateEditDataSourceValidation { region: string[]; accessKey: string[]; secretKey: string[]; + service: string[]; }; } @@ -34,6 +35,7 @@ export const defaultValidation: CreateEditDataSourceValidation = { region: [], accessKey: [], secretKey: [], + service: [], }, }; @@ -110,6 +112,11 @@ export const performDataSourceFormValidation = ( if (!formValues.auth.credentials?.region) { return false; } + + /* Service Name */ + if (!formValues.auth.credentials?.service) { + return false; + } } return true; diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index b6d937e5000e..1bede8fbfca9 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -16,6 +16,7 @@ import { import { ManagementAppMountParams } from 'src/plugins/management/public'; import { SavedObjectAttributes } from 'src/core/types'; import { i18n } from '@osd/i18n'; +import { SigV4ServiceName } from '../../data_source/common/data_sources'; import { OpenSearchDashboardsReactContextValue } from '../../opensearch_dashboards_react/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -78,6 +79,21 @@ export const credentialSourceOptions = [ }, ]; +export const sigV4ServiceOptions = [ + { + value: SigV4ServiceName.OpenSearch, + text: i18n.translate('dataSourceManagement.SigV4ServiceOptions.OpenSearch', { + defaultMessage: 'Amazon OpenSearch Service', + }), + }, + { + value: SigV4ServiceName.OpenSearchServerless, + text: i18n.translate('dataSourceManagement.SigV4ServiceOptions.OpenSearchServerless', { + defaultMessage: 'Amazon OpenSearch Serverless', + }), + }, +]; + export interface DataSourceAttributes extends SavedObjectAttributes { title: string; description?: string; @@ -97,4 +113,5 @@ export interface SigV4Content extends SavedObjectAttributes { accessKey: string; secretKey: string; region: string; + service?: SigV4ServiceName; } diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 0c0b3b07f5c1..02663da57686 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -78,7 +78,7 @@ export class DevToolsPlugin implements Plugin { defaultMessage: 'Dev Tools', }), updater$: this.appStateUpdater, - euiIconType: '/plugins/home/assets/logos/opensearch_mark_default.svg', + icon: '/plugins/home/public/assets/logos/opensearch_mark_default.svg', order: 9010, category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx index db5abf3dbed4..2f7cc40b7d9a 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ActionBar, ActionBarProps } from './action_bar'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants'; describe('Test Discover Context ActionBar for successor | predecessor records', () => { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx index 204d0b161c68..2ac06b2b6ebf 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; test('it renders ToolBarPagerButtons', () => { const props = { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx index 2e486976bbdf..0e8efc18efd5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TableHeader } from './table_header'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { SortOrder } from './helpers'; import { IndexPattern, IFieldType } from '../../../../../opensearch_dashboards_services'; diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx index 30b57eaf0dc5..a1ef06b81cf2 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -34,7 +34,7 @@ import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; // @ts-ignore import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('loading spinner', function () { let component: ReactWrapper; diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index 7385f0d360a1..4a3fb740492a 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -33,7 +33,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { Doc, DocProps } from './doc'; const mockSearchApi = jest.fn(); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx index 843ad5788253..ccab0be41ed2 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { DocViewer } from './doc_viewer'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { getDocViewsRegistry } from '../../../opensearch_dashboards_services'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx index 05435ccae99e..998ababbc47f 100644 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { HitsCounter, HitsCounterProps } from './hits_counter'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('hits counter', function () { let props: HitsCounterProps; diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx index 5bc6aa29136b..fbc98e2550e0 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { LoadingSpinner } from './loading_spinner'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('loading spinner', function () { let component: ReactWrapper; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 9170adccc7e7..f78505e11f1e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { DiscoverFieldSearch, Props } from './discover_field_search'; import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index dbc8c8962466..fa692ca22b5b 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -30,7 +30,7 @@ import _ from 'lodash'; import { ReactWrapper } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; // @ts-ignore import realHits from 'fixtures/real_hits.js'; // @ts-ignore diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index bb10fc137bcb..220ac57feae2 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index 4a88e469a086..9011c38a6acb 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -33,7 +33,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { TimechartHeader, TimechartHeaderProps } from './timechart_header'; import { EuiIconTip } from '@elastic/eui'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('timechart header', function () { let props: TimechartHeaderProps; diff --git a/src/plugins/embeddable/docs/input_and_output_state.md b/src/plugins/embeddable/docs/input_and_output_state.md index 2fd8799099b6..a0cd4cc5c0ef 100644 --- a/src/plugins/embeddable/docs/input_and_output_state.md +++ b/src/plugins/embeddable/docs/input_and_output_state.md @@ -274,9 +274,6 @@ There are no real life examples showcasing this, it may not even be really neede the thinking being that it would support any type of rendering of child embeddables - whether in a "snap to grid" style like dashboard, or in a free form layout like canvas. -The only real implementation of a container in production code at the time this is written is Dashboard however, with no plans to migrate -Canvas over to use it (this was the original impetus for an abstraction). The container code is quite complicated with child management, -so it makes creating a new container very easy, as you can see in the developer examples of containers. But, it's possible this layer was - an over abstraction without a real prod use case (I can say that because I wrote it, I'm only insulting myself!) :). +The only real implementation of a container in production code at the time is written in the Dashboard plugin, however, with no plans to migrate over to Canvas (this was the original impetus for an abstraction). The container code is quite complicated with child management, so it makes creating a new container very easy, as you can see in the developer examples of containers. But, it's possible this layer was an over abstraction without a real prod use case (I can say that because I wrote it, I'm only insulting myself!) :). Be sure to read [Common mistakes with embeddable containers and inherited input state](./containers_and_inherited_state.md) next! \ No newline at end of file diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx index badeda5d4aef..486e99b1c281 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { HelloWorldEmbeddable } from '../../../../../../examples/embeddable_examples/public'; import { EmbeddableRoot } from './embeddable_root'; import { mount } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; test('EmbeddableRoot renders an embeddable', async () => { const embeddable = new HelloWorldEmbeddable({ id: 'hello' }); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 072bcd93d32f..4db6b30c9b57 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { I18nProvider } from '@osd/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; import { Action, UiActionsStart, ActionType } from '../../../../ui_actions/public'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 232c2ca82d15..ac7d6e7aa082 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -41,7 +41,7 @@ import { ContainerInput } from '../../../../containers'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { embeddablePluginMock } from '../../../../../mocks'; function DummySavedObjectFinder(props: { children: React.ReactNode }) { diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index e2355799d72d..06ed4a66606c 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; import { Vis } from '../../../../visualizations/public'; diff --git a/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx index d8a36ac9b6c9..6837c14fafd2 100644 --- a/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { getIndexPatternMock } from '../../test_utils/get_index_pattern_mock'; diff --git a/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx index a8dcc0761f2c..76b1e15a9df7 100644 --- a/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx @@ -33,7 +33,7 @@ import { shallow } from 'enzyme'; import { SinonSpy, spy, assert } from 'sinon'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { RangeControlEditor } from './range_control_editor'; import { ControlParams } from '../../editor_utils'; diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index 1e931c084a55..ea1ef17d79f4 100644 --- a/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { InputControlVis } from './input_control_vis'; import { ListControl } from '../../control/list_control_factory'; diff --git a/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx b/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx index ef01d29e4b14..fbe36a289d70 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx +++ b/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx @@ -159,6 +159,8 @@ export const $setupXsrfRequestInterceptor = (version: string) => { // Configure jQuery prefilter $.ajaxPrefilter(({ osdXsrfToken = true }: any, originalOptions, jqXHR) => { if (osdXsrfToken) { + jqXHR.setRequestHeader('osd-xsrf', 'osd-legacy'); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection jqXHR.setRequestHeader('osd-version', version); } }); @@ -170,6 +172,8 @@ export const $setupXsrfRequestInterceptor = (version: string) => { request(opts) { const { osdXsrfToken = true } = opts as any; if (osdXsrfToken) { + set(opts, ['headers', 'osd-xsrf'], 'osd-legacy'); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection set(opts, ['headers', 'osd-version'], version); } return opts; diff --git a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index 7347975dbc4a..e15a1921b734 100644 --- a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -29,7 +29,7 @@ */ import { accessSync, constants, readFileSync, statSync } from 'fs'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import { dirname, join } from 'path'; import { Observable } from 'rxjs'; @@ -78,7 +78,7 @@ export async function readTelemetryFile( try { if (isFileReadable(configPath)) { const yaml = readFileSync(configPath); - const data = safeLoad(yaml.toString()); + const data = load(yaml.toString()); // don't bother returning empty objects if (Object.keys(data).length) { diff --git a/src/plugins/tile_map/public/_tile_map.scss b/src/plugins/tile_map/public/_tile_map.scss deleted file mode 100644 index 5e4b20f79fed..000000000000 --- a/src/plugins/tile_map/public/_tile_map.scss +++ /dev/null @@ -1,15 +0,0 @@ -// SASSTODO: Does this selector exist today? -.tilemap { - margin-bottom: 6px; - border: $euiBorderThin; - position: relative; -} - -/** -* 1. Visualizations have some padding by default but tilemaps look nice flush against the edge to maximize viewing -* space. -*/ -// SASSTODO: Does this selector exist today? -.tile_map { - padding: 0; /* 1. */ -} diff --git a/src/plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss deleted file mode 100644 index aa2117cb0c01..000000000000 --- a/src/plugins/tile_map/public/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -// Prefix all styles with "tlm" to avoid conflicts. -// Examples -// tlmChart -// tlmChart__legend -// tlmChart__legend--small -// tlmChart__legend-isLoading - -@import "tile_map"; diff --git a/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts b/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts index 243e455d7157..3e157f9bf6df 100644 --- a/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts +++ b/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts @@ -13,6 +13,7 @@ export interface VisBuilderSavedObjectAttributes extends SavedObjectAttributes { visualizationState?: string; updated_at?: string; styleState?: string; + uiState?: string; version: number; searchSourceFields?: { index?: string; 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 5d3f298c6bf3..fde4f3110d1c 100644 --- a/src/plugins/vis_builder/public/application/components/right_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/right_nav.tsx @@ -122,6 +122,6 @@ const OptionItem = ({ icon, title }: { icon: IconType; title: string }) => ( ); -// The app uses EuiResizableContainer that triggers a rerender for ever mouseover action. +// The app uses EuiResizableContainer that triggers a rerender for every mouseover action. // To prevent this child component from unnecessarily rerendering in that instance, it needs to be memoized export const RightNav = React.memo(RightNavUI); diff --git a/src/plugins/vis_builder/public/application/components/workspace.tsx b/src/plugins/vis_builder/public/application/components/workspace.tsx index 214a735f6ba8..31880e93bb7f 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.tsx +++ b/src/plugins/vis_builder/public/application/components/workspace.tsx @@ -10,7 +10,7 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react import { IExpressionLoaderParams } from '../../../../expressions/public'; import { VisBuilderServices } from '../../types'; import { validateSchemaState, validateAggregations } from '../utils/validations'; -import { useTypedSelector } from '../utils/state_management'; +import { useTypedDispatch, useTypedSelector, setUIStateState } from '../utils/state_management'; import { useAggs, useVisualizationType } from '../utils/use'; import { PersistedState } from '../../../../visualizations/public'; @@ -39,8 +39,25 @@ export const WorkspaceUI = () => { timeRange: data.query.timefilter.timefilter.getTime(), }); const rootState = useTypedSelector((state) => state); - // Visualizations require the uiState to persist even when the expression changes - const uiState = useMemo(() => new PersistedState(), []); + const dispatch = useTypedDispatch(); + // Visualizations require the uiState object to persist even when the expression changes + // eslint-disable-next-line react-hooks/exhaustive-deps + const uiState = useMemo(() => new PersistedState(rootState.ui), []); + + useEffect(() => { + if (rootState.metadata.editor.state === 'loaded') { + uiState.setSilent(rootState.ui); + } + // To update uiState once saved object data is loaded + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rootState.metadata.editor.state, uiState]); + + useEffect(() => { + uiState.on('change', (args) => { + // Store changes to UI state + dispatch(setUIStateState(uiState.toJSON())); + }); + }, [dispatch, uiState]); useEffect(() => { async function loadExpression() { @@ -56,7 +73,11 @@ export const WorkspaceUI = () => { const err = schemaValidation.errorMsg || aggValidation.errorMsg; - if (err) toasts.addWarning(err); + if (err) + toasts.addWarning({ + id: 'vb_expression_validation', + title: err, + }); return; } @@ -133,6 +154,6 @@ export const WorkspaceUI = () => { ); }; -// The app uses EuiResizableContainer that triggers a rerender for ever mouseover action. +// The app uses EuiResizableContainer that triggers a rerender for every mouseover action. // To prevent this child component from unnecessarily rerendering in that instance, it needs to be memoized export const Workspace = React.memo(WorkspaceUI); diff --git a/src/plugins/vis_builder/public/application/utils/schema.json b/src/plugins/vis_builder/public/application/utils/schema.json index 9effed97b2be..7cf8bbc2534f 100644 --- a/src/plugins/vis_builder/public/application/utils/schema.json +++ b/src/plugins/vis_builder/public/application/utils/schema.json @@ -1,28 +1,47 @@ { - "type": "object", - "properties": { - "styleState": { - "type": "object" - }, - "visualizationState": { - "type": "object", - "properties": { - "activeVisualization": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "aggConfigParams": { "type": "array" } + "type": "object", + "properties": { + "styleState": { + "type": "object" + }, + "visualizationState": { + "type": "object", + "properties": { + "activeVisualization": { + "type": "object", + "properties": { + "name": { + "type": "string" }, - "required": ["name", "aggConfigParams"], - "additionalProperties": false + "aggConfigParams": { + "type": "array" + } }, - "indexPattern": { "type": "string" }, - "searchField": { "type": "string" } + "required": [ + "name", + "aggConfigParams" + ], + "additionalProperties": false }, - "required": ["searchField"], - "additionalProperties": false - } + "indexPattern": { + "type": "string" + }, + "searchField": { + "type": "string" + } + }, + "required": [ + "searchField" + ], + "additionalProperties": false }, - "required": ["styleState", "visualizationState"], - "additionalProperties": false + "uiState": { + "type": "object" + } + }, + "required": [ + "styleState", + "visualizationState" + ], + "additionalProperties": false } \ No newline at end of file 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 05ceb324aaa1..880c15f3e44a 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 @@ -11,7 +11,7 @@ import { VisBuilderServices } from '../../../types'; * Clean state: when viz finished loading and ready to be edited * Dirty state: when there are changes applied to the viz after it finished loading */ -type EditorState = 'loading' | 'clean' | 'dirty'; +type EditorState = 'loading' | 'loaded' | 'clean' | 'dirty'; export interface MetadataState { editor: { diff --git a/src/plugins/vis_builder/public/application/utils/state_management/preload.ts b/src/plugins/vis_builder/public/application/utils/state_management/preload.ts index 43aa2e7b8ede..f7a0f6bd7ad3 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/preload.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/preload.ts @@ -8,6 +8,7 @@ import { VisBuilderServices } from '../../..'; import { getPreloadedState as getPreloadedStyleState } from './style_slice'; import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; +import { getPreloadedState as getPreloadedUIState } from './ui_state_slice'; import { RootState } from './store'; export const getPreloadedState = async ( @@ -16,10 +17,12 @@ export const getPreloadedState = async ( const styleState = await getPreloadedStyleState(services); const visualizationState = await getPreloadedVisualizationState(services); const metadataState = await getPreloadedMetadataState(services); + const uiState = await getPreloadedUIState(services); return { style: styleState, visualization: visualizationState, metadata: metadataState, + ui: uiState, }; }; 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 index 91f760bbf231..a46d5c027656 100644 --- 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 @@ -18,6 +18,7 @@ describe('test redux state persistence', () => { style: 'style', visualization: 'visualization', metadata: 'metadata', + ui: 'ui', }; }); @@ -33,6 +34,7 @@ describe('test redux state persistence', () => { editor: { errors: {}, state: 'loading' }, originatingApp: undefined, }, + ui: {}, }; const returnStates = await loadReduxState(mockServices); 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 index a531986a9ac9..3ebfa47268ec 100644 --- 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 @@ -12,22 +12,21 @@ export const loadReduxState = async (services: VisBuilderServices) => { const serializedState = services.osdUrlStateStorage.get('_a'); if (serializedState !== null) return serializedState; } catch (err) { - /* eslint-disable no-console */ + // eslint-disable-next-line no-console console.error(err); - /* eslint-enable no-console */ } return await getPreloadedState(services); }; export const persistReduxState = ( - { style, visualization, metadata }, + { style, visualization, metadata, ui }: RootState, services: VisBuilderServices ) => { try { services.osdUrlStateStorage.set( '_a', - { style, visualization, metadata }, + { style, visualization, metadata, ui }, { replace: true, } 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 f1b1c0eeae2a..8fe5c23fd657 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 @@ -8,12 +8,14 @@ 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 { reducer as uiStateReducer } from './ui_state_slice'; import { VisBuilderServices } from '../../..'; import { loadReduxState, persistReduxState } from './redux_persistence'; import { handlerEditorState } from './handlers/editor_state'; import { handlerParentAggs } from './handlers/parent_aggs'; const rootReducer = combineReducers({ + ui: uiStateReducer, style: styleReducer, visualization: visualizationReducer, metadata: metadataReducer, @@ -47,9 +49,9 @@ export const getPreloadedStore = async (services: VisBuilderServices) => { }; // the store subscriber will automatically detect changes and call handleChange function - store.subscribe(handleChange); + const unsubscribe = store.subscribe(handleChange); - return store; + return { store, unsubscribe }; }; // Infer the `RootState` and `AppDispatch` types from the store itself @@ -60,3 +62,5 @@ export type AppDispatch = Store['dispatch']; export { setState as setStyleState, StyleState } from './style_slice'; export { setState as setVisualizationState, VisualizationState } from './visualization_slice'; +export { MetadataState } from './metadata_slice'; +export { setState as setUIStateState, UIStateState } from './ui_state_slice'; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts new file mode 100644 index 000000000000..826fe9d9873d --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { VisBuilderServices } from '../../../types'; + +export type UIStateState = T; + +const initialState = {} as UIStateState; + +export const getPreloadedState = async ({ + types, + data, +}: VisBuilderServices): Promise => { + return initialState; +}; + +export const uiStateSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + setState(state: T, action: PayloadAction>) { + return action.payload; + }, + updateState(state: T, action: PayloadAction>>) { + state = { + ...state, + ...action.payload, + }; + }, + }, +}); + +// Exposing the state functions as generics +export const setState = uiStateSlice.actions.setState as (payload: T) => PayloadAction; +export const updateState = uiStateSlice.actions.updateState as ( + payload: Partial +) => PayloadAction>; + +export const { reducer } = uiStateSlice; 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 604c90a25ccd..44ffbaf75953 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 @@ -14,7 +14,12 @@ import { import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; import { VisBuilderServices } from '../../../types'; import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; -import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management'; +import { + useTypedDispatch, + setStyleState, + setVisualizationState, + setUIStateState, +} from '../state_management'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { setEditorState } from '../state_management/metadata_slice'; import { getStateFromSavedObject } from '../../../saved_visualizations/transforms'; @@ -46,6 +51,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined const loadSavedVisBuilderVis = async () => { try { + dispatch(setEditorState({ state: 'loading' })); const savedVisBuilderVis = await getSavedVisBuilderVis( savedVisBuilderLoader, visualizationIdFromUrl @@ -56,8 +62,10 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined chrome.setBreadcrumbs(getEditBreadcrumbs(title, navigateToApp)); chrome.docTitle.change(title); + dispatch(setUIStateState(state.ui)); dispatch(setStyleState(state.style)); dispatch(setVisualizationState(state.visualization)); + dispatch(setEditorState({ state: 'loaded' })); } else { chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp)); } 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 7142c9c3c0cf..a931877ffe6d 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx @@ -22,6 +22,7 @@ import { } from '../../../expressions/public'; import { Filter, + IIndexPattern, opensearchFilters, Query, TimefilterContract, @@ -37,20 +38,23 @@ import { import { PersistedState } from '../../../visualizations/public'; import { VisBuilderSavedVis } from '../saved_visualizations/transforms'; import { handleVisEvent } from '../application/utils/handle_vis_event'; +import { VisBuilderEmbeddableFactoryDeps } from './vis_builder_embeddable_factory'; // Apparently this needs to match the saved object type for the clone and replace panel actions to work export const VISBUILDER_EMBEDDABLE = VISBUILDER_SAVED_OBJECT; export interface VisBuilderEmbeddableConfiguration { savedVis: VisBuilderSavedVis; - // TODO: add indexPatterns as part of configuration - // indexPatterns?: IIndexPattern[]; + indexPatterns?: IIndexPattern[]; editPath: string; editUrl: string; editable: boolean; + deps: VisBuilderEmbeddableFactoryDeps; } -export type VisBuilderInput = SavedObjectEmbeddableInput; +export interface VisBuilderInput extends SavedObjectEmbeddableInput { + uiState?: any; +} export interface VisBuilderOutput extends EmbeddableOutput { /** @@ -58,11 +62,12 @@ export interface VisBuilderOutput extends EmbeddableOutput { * `input.savedObjectId`. If the id is invalid, this may be undefined. */ savedVis?: VisBuilderSavedVis; + indexPatterns?: IIndexPattern[]; } type ExpressionLoader = InstanceType; -export class VisBuilderEmbeddable extends Embeddable { +export class VisBuilderEmbeddable extends Embeddable { public readonly type = VISBUILDER_EMBEDDABLE; private handler?: ExpressionLoader; private timeRange?: TimeRange; @@ -75,11 +80,19 @@ export class VisBuilderEmbeddable extends Embeddable s.unsubscribe()); + this.uiState.off('change', this.uiStateChangeHandler); + this.uiState.off('reload', this.reload); if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } @@ -249,6 +268,7 @@ export class VisBuilderEmbeddable extends Embeddable { + this.updateInput({ + uiState: this.uiState.toJSON(), + }); + }; + + private transferInputToUiState = () => { + if (JSON.stringify(this.input.uiState) !== this.uiState.toString()) + this.uiState.set(this.input.uiState); + }; + // TODO: we may eventually need to add support for visualizations that use triggers like filter or brush, but current VisBuilder vis types don't support triggers // public supportedTriggers(): TriggerId[] { // return this.visType.getSupportedTriggers?.() ?? []; diff --git a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx index f80ad18ee363..90048ba91322 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx @@ -5,7 +5,6 @@ import { i18n } from '@osd/i18n'; import { - EmbeddableFactory, EmbeddableFactoryDefinition, EmbeddableOutput, ErrorEmbeddable, @@ -35,16 +34,14 @@ import { getTimeFilter, getUISettings, } from '../plugin_services'; +import { StartServicesGetter } from '../../../opensearch_dashboards_utils/public'; +import { VisBuilderPluginStartDependencies } from '../types'; -// TODO: use or remove? -export type VisBuilderEmbeddableFactory = EmbeddableFactory< - SavedObjectEmbeddableInput, - VisBuilderOutput | EmbeddableOutput, - VisBuilderEmbeddable | DisabledEmbeddable, - VisBuilderSavedObjectAttributes ->; +export interface VisBuilderEmbeddableFactoryDeps { + start: StartServicesGetter; +} -export class VisBuilderEmbeddableFactoryDefinition +export class VisBuilderEmbeddableFactory implements EmbeddableFactoryDefinition< SavedObjectEmbeddableInput, @@ -62,7 +59,7 @@ export class VisBuilderEmbeddableFactoryDefinition }; // TODO: Would it be better to explicitly declare start service dependencies? - constructor() {} + constructor(private readonly deps: VisBuilderEmbeddableFactoryDeps) {} public canCreateNew() { // Because VisBuilder creation starts with the visualization modal, no need to have a separate entry for VisBuilder until it's separate @@ -90,13 +87,22 @@ export class VisBuilderEmbeddableFactoryDefinition return new DisabledEmbeddable(PLUGIN_NAME, input); } + const savedVis = getStateFromSavedObject(savedObject); + const indexPatternService = this.deps.start().plugins.data.indexPatterns; + const indexPattern = await indexPatternService.get( + savedVis.state.visualization.indexPattern || '' + ); + const indexPatterns = indexPattern ? [indexPattern] : []; + return new VisBuilderEmbeddable( getTimeFilter(), { - savedVis: getStateFromSavedObject(savedObject), + savedVis, editUrl, editPath, editable: true, + deps: this.deps, + indexPatterns, }, { ...input, diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 0c1d569f6bed..1445de923010 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -23,7 +23,7 @@ import { VisBuilderSetup, VisBuilderStart, } from './types'; -import { VisBuilderEmbeddableFactoryDefinition, VISBUILDER_EMBEDDABLE } from './embeddable'; +import { VisBuilderEmbeddableFactory, VISBUILDER_EMBEDDABLE } from './embeddable'; import visBuilderIconSecondaryFill from './assets/vis_builder_icon_secondary_fill.svg'; import visBuilderIcon from './assets/vis_builder_icon.svg'; import { @@ -54,6 +54,7 @@ import { ConfigSchema } from '../config'; import { createOsdUrlStateStorage, createOsdUrlTracker, + createStartServicesGetter, withNotifyOnErrors, } from '../../opensearch_dashboards_utils/public'; import { opensearchFilters } from '../../data/public'; @@ -163,7 +164,7 @@ export class VisBuilderPlugin }; // Instantiate the store - const store = await getPreloadedStore(services); + const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); const unmount = renderApp(params, services, store); // Render the application @@ -171,15 +172,14 @@ export class VisBuilderPlugin unlistenParentHistory(); unmount(); appUnMounted(); + unsubscribeStore(); }; }, }); // Register embeddable - // TODO: investigate simplification via getter a la visualizations: - // const start = createStartServicesGetter(core.getStartServices)); - // const embeddableFactory = new VisBuilderEmbeddableFactoryDefinition({ start }); - const embeddableFactory = new VisBuilderEmbeddableFactoryDefinition(); + const start = createStartServicesGetter(core.getStartServices); + const embeddableFactory = new VisBuilderEmbeddableFactory({ start }); embeddable.registerEmbeddableFactory(VISBUILDER_EMBEDDABLE, embeddableFactory); // Register the plugin as an alias to create visualization diff --git a/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts b/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts index 53ccc7a9d7d0..021af777df17 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts @@ -22,6 +22,7 @@ export function createSavedVisBuilderVisClass(services: SavedObjectOpenSearchDas description: 'text', visualizationState: 'text', styleState: 'text', + uiState: 'text', version: 'integer', }; @@ -44,7 +45,8 @@ export function createSavedVisBuilderVisClass(services: SavedObjectOpenSearchDas description: '', visualizationState: '{}', styleState: '{}', - version: 2, + uiState: '{}', + version: 3, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts b/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts index efbcfd23f799..68c24dfe4af1 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts @@ -25,7 +25,7 @@ describe('transforms', () => { savedObject = {} as VisBuilderSavedObject; rootState = { metadata: { editor: { state: 'loading', errors: {} } }, - style: '', + style: {}, visualization: { searchField: '', indexPattern: TEST_INDEX_PATTERN_ID, @@ -34,6 +34,7 @@ describe('transforms', () => { aggConfigParams: [], }, }, + ui: {}, }; indexPattern = getStubIndexPattern( TEST_INDEX_PATTERN_ID, @@ -49,6 +50,7 @@ describe('transforms', () => { expect(savedObject.visualizationState).not.toContain(TEST_INDEX_PATTERN_ID); expect(savedObject.styleState).toEqual(JSON.stringify(rootState.style)); + expect(savedObject.uiState).toEqual(JSON.stringify(rootState.ui)); expect(savedObject.searchSourceFields?.index?.id).toEqual(TEST_INDEX_PATTERN_ID); }); @@ -69,6 +71,7 @@ describe('transforms', () => { visualizationState: JSON.stringify({ searchField: '', }), + uiState: '{}', searchSourceFields: { index: 'test-index', }, @@ -80,6 +83,7 @@ describe('transforms', () => { expect(state).toMatchInlineSnapshot(` Object { "style": Object {}, + "ui": Object {}, "visualization": Object { "indexPattern": "test-index", "searchField": "", diff --git a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts index 0a7a6e529a6b..9f8dd705e3e4 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts @@ -27,6 +27,7 @@ export const saveStateToSavedObject = ( ); obj.styleState = JSON.stringify(state.style); obj.searchSourceFields = { index: indexPattern }; + obj.uiState = JSON.stringify(state.ui); return obj; }; @@ -40,7 +41,8 @@ export const getStateFromSavedObject = ( obj: VisBuilderSavedObjectAttributes ): VisBuilderSavedVis => { const { id, title, description } = obj; - const styleState = JSON.parse(obj.styleState || ''); + const styleState = JSON.parse(obj.styleState || '{}'); + const uiState = JSON.parse(obj.uiState || '{}'); const vizStateWithoutIndex = JSON.parse(obj.visualizationState || ''); const visualizationState: VisualizationState = { searchField: '', @@ -48,7 +50,7 @@ export const getStateFromSavedObject = ( indexPattern: obj.searchSourceFields?.index, }; - const validateResult = validateVisBuilderState({ styleState, visualizationState }); + const validateResult = validateVisBuilderState({ styleState, visualizationState, uiState }); if (!validateResult.valid) { throw new InvalidJSONProperty( @@ -75,6 +77,7 @@ export const getStateFromSavedObject = ( state: { visualization: visualizationState, style: styleState, + ui: uiState, }, }; }; diff --git a/src/plugins/vis_builder/public/types.ts b/src/plugins/vis_builder/public/types.ts index 5221a1c513ec..1ba8843e016a 100644 --- a/src/plugins/vis_builder/public/types.ts +++ b/src/plugins/vis_builder/public/types.ts @@ -62,6 +62,7 @@ export interface ISavedVis { description?: string; visualizationState?: string; styleState?: string; + uiState?: string; version?: number; } diff --git a/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts b/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts index 15d785b3b451..029557010bee 100644 --- a/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts +++ b/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts @@ -46,6 +46,10 @@ export const visBuilderSavedObjectType: SavedObjectsType = { type: 'text', index: false, }, + uiState: { + type: 'text', + index: false, + }, version: { type: 'integer' }, // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow // When we save a saved object, the saved object plugin will extract the search source into two parts 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 index 876847667418..aafcd40e7382 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_app.scss +++ b/src/plugins/vis_type_table/public/components/table_vis_app.scss @@ -1,21 +1,29 @@ +// Container for the Table Visualization component .visTable { display: flex; flex-direction: column; flex: 1 0 0; overflow: auto; + + @include euiScrollBar; } +// Group container for table visualization components .visTable__group { padding: $euiSizeS; margin-bottom: $euiSizeL; + display: flex; + flex-direction: column; + flex: 0 0 auto; +} - > h3 { - text-align: center; - } +// Style for table component title +.visTable__component__title { + text-align: center; } +// Modifier for visTables__group when displayed in columns .visTable__groupInColumns { - display: flex; flex-direction: row; align-items: flex-start; } diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_app.test.tsx new file mode 100644 index 000000000000..37cb753765f8 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_app.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { coreMock } from '../../../../core/public/mocks'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { TableVisApp } from './table_vis_app'; +import { TableVisConfig } from '../types'; +import { TableVisData } from '../table_vis_response_handler'; + +jest.mock('./table_vis_component_group', () => ({ + TableVisComponentGroup: () => ( +
    TableVisComponentGroup
    + ), +})); + +jest.mock('./table_vis_component', () => ({ + TableVisComponent: () =>
    TableVisComponent
    , +})); + +describe('TableVisApp', () => { + const serviceMock = coreMock.createStart(); + const handlersMock = ({ + done: jest.fn(), + uiState: { + get: jest.fn((key) => { + switch (key) { + case 'vis.sortColumn': + return {}; + case 'vis.columnsWidth': + return []; + default: + return undefined; + } + }), + set: jest.fn(), + }, + event: 'event', + } as unknown) as IInterpreterRenderHandlers; + const visConfigMock = ({} as unknown) as TableVisConfig; + + it('should render TableVisComponent if no split table', () => { + const visDataMock = { + table: { + columns: [], + rows: [], + formattedColumns: [], + }, + tableGroups: [], + } as TableVisData; + const { getByTestId } = render( + + ); + expect(getByTestId('TableVisComponent')).toBeInTheDocument(); + }); + + it('should render TableVisComponentGroup component if split direction is column', () => { + const visDataMock = { + tableGroups: [], + direction: 'column', + } as TableVisData; + const { container, getByTestId } = render( + + ); + expect(container.outerHTML.includes('visTable visTable__groupInColumns')).toBe(true); + expect(getByTestId('TableVisComponentGroup')).toBeInTheDocument(); + }); + + it('should render TableVisComponentGroup component if split direction is row', () => { + const visDataMock = { + tableGroups: [], + direction: 'row', + } as TableVisData; + const { container, getByTestId } = render( + + ); + expect(container.outerHTML.includes('visTable')).toBe(true); + expect(getByTestId('TableVisComponentGroup')).toBeInTheDocument(); + }); +}); 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 index af10500a1a92..81f4d775f1e5 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_app.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_app.tsx @@ -4,20 +4,22 @@ */ import './table_vis_app.scss'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } 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 { PersistedState } from '../../../visualizations/public'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; -import { TableContext } from '../table_vis_response_handler'; -import { TableVisConfig, ColumnSort, ColumnWidth, TableUiState } from '../types'; +import { TableVisData } from '../table_vis_response_handler'; +import { TableVisConfig } from '../types'; import { TableVisComponent } from './table_vis_component'; import { TableVisComponentGroup } from './table_vis_component_group'; +import { getTableUIState, TableUiState } from '../utils'; interface TableVisAppProps { services: CoreStart; - visData: TableContext; + visData: TableVisData; visConfig: TableVisConfig; handlers: IInterpreterRenderHandlers; } @@ -38,12 +40,7 @@ export const TableVisApp = ({ 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({ colIndex: null, direction: null }); - const [width, setWidth] = useState([]); - - const tableUiState: TableUiState = { sort, setSort, width, setWidth }; + const tableUiState: TableUiState = getTableUIState(handlers.uiState as PersistedState); return ( diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx new file mode 100644 index 000000000000..9b1b1c02ac40 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { FormattedColumn } from '../types'; +import { getTableVisCellValue } from './table_vis_cell'; +import { FieldFormat } from 'src/plugins/data/public'; + +class MockFieldFormat extends FieldFormat { + convert = jest.fn(); +} + +describe('getTableVisCellValue', () => { + const mockFormatter = new MockFieldFormat(); + + const columns: FormattedColumn[] = [ + { + id: 'testId', + title: 'Test Column', + formatter: mockFormatter, + filterable: true, + }, + ]; + + const sortedRows: OpenSearchDashboardsDatatableRow[] = [ + { + testId: 'Test Value 1', + }, + { + testId: 'Test Value 2', + }, + ]; + + const TableCell = ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + const getCellValue = getTableVisCellValue(sortedRows, columns); + return getCellValue({ rowIndex, columnId }); + }; + + beforeEach(() => { + mockFormatter.convert.mockClear(); + }); + + test('should render cell value with correct formatting', () => { + mockFormatter.convert.mockReturnValueOnce('Test Value 1'); + const { getByText } = render(); + expect(mockFormatter.convert).toHaveBeenCalledWith('Test Value 1', 'html'); + expect(getByText('Test Value 1')).toBeInTheDocument(); + expect(getByText('Test Value 1').closest('strong')).toBeInTheDocument(); + }); + + test('should return null when rowIndex is out of bounds', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + test('should return null when no matching columnId is found', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx new file mode 100644 index 000000000000..30c0877df701 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import dompurify from 'dompurify'; + +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { FormattedColumn } from '../types'; + +export const getTableVisCellValue = ( + sortedRows: OpenSearchDashboardsDatatableRow[], + columns: FormattedColumn[] +) => ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + if (rowIndex < 0 || rowIndex >= sortedRows.length) { + return null; + } + const row = sortedRows[rowIndex]; + if (!row || !row.hasOwnProperty(columnId)) { + return null; + } + const rawContent = row[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). + */ +
    // eslint-disable-line react/no-danger + ); + return formattedContent || null; +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_component.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_component.test.tsx new file mode 100644 index 000000000000..6e2d0090aa36 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component.test.tsx @@ -0,0 +1,234 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TableVisConfig, ColumnSort } from '../types'; +import { TableVisComponent } from './table_vis_component'; +import { FormattedColumn } from '../types'; +import { FormattedTableContext } from '../table_vis_response_handler'; +import { getTableVisCellValue } from './table_vis_cell'; +import { getDataGridColumns } from './table_vis_grid_columns'; +import { EuiDataGridColumn } from '@elastic/eui'; + +jest.mock('./table_vis_cell', () => ({ + getTableVisCellValue: jest.fn(() => () => {}), +})); + +const mockGetDataGridColumns = jest.fn(() => []); +jest.mock('./table_vis_grid_columns', () => ({ + getDataGridColumns: jest.fn(() => mockGetDataGridColumns()), +})); + +const table = { + formattedColumns: [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + }, + { + id: 'col-1-1', + title: 'Count', + formatter: {}, + filterable: false, + sumTotal: 5, + formattedTotal: 5, + total: 5, + }, + ] as FormattedColumn[], + rows: [ + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, + ], + columns: [ + { id: 'col-0-2', name: 'Name' }, + { id: 'col-1-1', name: 'Count' }, + ], +} as FormattedTableContext; + +const visConfig = { + buckets: [ + { + accessor: 0, + aggType: 'terms', + format: { + id: 'terms', + params: { + id: 'number', + missingBucketLabel: 'Missing', + otherBucketLabel: 'Other', + parsedUrl: { + basePath: '/arf', + origin: '', + pathname: '/arf/app/home', + }, + }, + }, + label: 'age: Descending', + params: {}, + }, + ], + metrics: [ + { + accessor: 1, + aggType: 'count', + format: { + id: 'number', + }, + label: 'Count', + params: {}, + }, + ], + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + title: '', + totalFunc: 'sum', +} as TableVisConfig; + +const uiState = { + sort: {} as ColumnSort, + setSort: jest.fn(), + colWidth: [], + setWidth: jest.fn(), +}; + +describe('TableVisComponent', function () { + const props = { + title: '', + table, + visConfig, + event: jest.fn(), + uiState, + }; + + const dataGridColumnsValue = [ + { + id: 'col-0-2', + display: 'name.keyword: Descending', + displayAsText: 'name.keyword: Descending', + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: {}, + showSortDesc: {}, + }, + cellActions: expect.any(Function), + }, + { + id: 'col-1-1', + display: 'Count', + displayAsText: 'Count', + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: {}, + showSortDesc: {}, + }, + cellActions: undefined, + }, + ] as EuiDataGridColumn[]; + + it('should render data grid', () => { + const comp = shallow(); + expect(comp.find('EuiDataGrid')).toHaveLength(1); + }); + + it('should render title when provided', () => { + const compWithTitle = shallow(); + const titleElement = compWithTitle.find('EuiTitle'); + expect(titleElement).toHaveLength(1); + expect(titleElement.find('h3').text()).toEqual('Test Title'); + }); + + it('should not render title when not provided', () => { + const compWithoutTitle = shallow(); + const titleElement = compWithoutTitle.find('EuiTitle'); + expect(titleElement).toHaveLength(0); + }); + + it('should set sort if sort column', () => { + mockGetDataGridColumns.mockReturnValueOnce(dataGridColumnsValue); + const comp = shallow(); + const { onSort } = comp.find('EuiDataGrid').prop('sorting') as any; + onSort([]); + expect(props.uiState.setSort).toHaveBeenCalledWith([]); + onSort([{ id: 'col-0-2', direction: 'asc' }]); + expect(props.uiState.setSort).toHaveBeenCalledWith({ colIndex: 0, direction: 'asc' }); + onSort([ + { id: 'col-0-2', direction: 'asc' }, + { id: 'col-1-1', direction: 'desc' }, + ]); + expect(props.uiState.setSort).toHaveBeenCalledWith({ colIndex: 1, direction: 'desc' }); + }); + + it('should set width if adjust column width', () => { + const uiStateProps = { + ...props.uiState, + width: [ + { colIndex: 0, width: 12 }, + { colIndex: 1, width: 8 }, + ], + }; + const comp = shallow(); + const onColumnResize = comp.find('EuiDataGrid').prop('onColumnResize') as any; + onColumnResize({ columnId: 'col-0-2', width: 18 }); + expect(props.uiState.setWidth).toHaveBeenCalledWith({ colIndex: 0, width: 18 }); + const updatedComp = shallow(); + const onColumnResizeUpdate = updatedComp.find('EuiDataGrid').prop('onColumnResize') as any; + onColumnResizeUpdate({ columnId: 'col-0-2', width: 18 }); + expect(props.uiState.setWidth).toHaveBeenCalledWith({ colIndex: 0, width: 18 }); + }); + + it('should create sortedRows and pass to getTableVisCellValue', () => { + const uiStateProps = { + ...props.uiState, + sort: { colIndex: 1, direction: 'asc' } as ColumnSort, + }; + const sortedRows = [ + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + ]; + mockGetDataGridColumns.mockReturnValueOnce(dataGridColumnsValue); + shallow(); + expect(getTableVisCellValue).toHaveBeenCalledWith(sortedRows, table.formattedColumns); + expect(getDataGridColumns).toHaveBeenCalledWith(table, props.event, props.uiState.colWidth); + }); + + it('should return formattedTotal from footerCellValue', () => { + let comp = shallow(); + let renderFooterCellValue = comp.find('EuiDataGrid').prop('renderFooterCellValue') as any; + expect(renderFooterCellValue).toEqual(undefined); + comp = shallow(); + renderFooterCellValue = comp.find('EuiDataGrid').prop('renderFooterCellValue'); + expect(renderFooterCellValue({ columnId: 'col-1-1' })).toEqual(5); + expect(renderFooterCellValue({ columnId: 'col-0-2' })).toEqual(null); + }); + + it('should apply pagination correctly', () => { + const comp = shallow(); + const paginationProps = comp.find('EuiDataGrid').prop('pagination'); + expect(paginationProps).toMatchObject({ + pageIndex: 0, + pageSize: 3, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should not call renderFooterCellValue when showTotal is false', () => { + const comp = shallow(); + const renderFooterCellValue = comp.find('EuiDataGrid').prop('renderFooterCellValue'); + expect(renderFooterCellValue).toBeUndefined(); + }); +}); 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 index 4576e3420e22..1b16ec170a84 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_component.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_component.tsx @@ -5,20 +5,20 @@ 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 { FormattedTableContext } from '../table_vis_response_handler'; +import { TableVisConfig, ColumnSort } from '../types'; import { getDataGridColumns } from './table_vis_grid_columns'; +import { getTableVisCellValue } from './table_vis_cell'; import { usePagination } from '../utils'; -import { convertToFormattedData } from '../utils/convert_to_formatted_data'; import { TableVisControl } from './table_vis_control'; +import { TableUiState } from '../utils'; interface TableVisComponentProps { title?: string; - table: Table; + table: FormattedTableContext; visConfig: TableVisConfig; event: IInterpreterRenderHandlers['event']; uiState: TableUiState; @@ -29,52 +29,44 @@ export const TableVisComponent = ({ table, visConfig, event, - uiState, + uiState: { sort, setSort, colWidth, setWidth }, }: TableVisComponentProps) => { - const { formattedRows: rows, formattedColumns: columns } = convertToFormattedData( - table, - visConfig - ); + const { rows, formattedColumns } = table; 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 sortColumnId = + sort.colIndex !== null && sort.colIndex !== undefined + ? formattedColumns[sort.colIndex]?.id + : undefined; + + if (sortColumnId && sort.direction) { + return orderBy(rows, sortColumnId, sort.direction); + } else { + return rows; + } + }, [formattedColumns, rows, sort]); - 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). - */ -
    // eslint-disable-line react/no-danger - ); - return sortedRows.hasOwnProperty(rowIndex) ? formattedContent || null : null; - }) as EuiDataGridProps['renderCellValue']; - }, [sortedRows, columns]); + const renderCellValue = useMemo(() => getTableVisCellValue(sortedRows, formattedColumns), [ + sortedRows, + formattedColumns, + ]); - const dataGridColumns = getDataGridColumns(sortedRows, columns, table, event, uiState.width); + const dataGridColumns = getDataGridColumns(table, event, colWidth); 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]); + if ( + sort.colIndex !== null && + sort.colIndex !== undefined && + dataGridColumns[sort.colIndex].id && + sort.direction + ) { + return [{ id: dataGridColumns[sort.colIndex].id, direction: sort.direction }]; + } else { + return []; + } + }, [dataGridColumns, sort]); const onSort = useCallback( (sortingCols: EuiDataGridSorting['columns'] | []) => { @@ -85,47 +77,34 @@ export const TableVisComponent = ({ colIndex: dataGridColumns.findIndex((col) => col.id === nextSortValue?.id), direction: nextSortValue.direction, } - : { - colIndex: null, - direction: null, - }; - uiState.setSort(nextSort); + : []; + setSort(nextSort); return nextSort; }, - [dataGridColumns, uiState] + [dataGridColumns, setSort] ); 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); + ({ columnId, width }: { columnId: string; width: number }) => { + const colIndex = formattedColumns.findIndex((col) => col.id === columnId); + // update width in uiState + setWidth({ colIndex, width }); }, - [columns, uiState] + [formattedColumns, setWidth] ); const ariaLabel = title || visConfig.title || 'tableVis'; const footerCellValue = visConfig.showTotal ? ({ columnId }: { columnId: any }) => { - return columns.find((col) => col.id === columnId)?.formattedTotal || null; + return formattedColumns.find((col) => col.id === columnId)?.formattedTotal || null; } : undefined; return ( <> {title && ( - +

    {title}

    )} @@ -133,7 +112,7 @@ export const TableVisComponent = ({ aria-label={ariaLabel} columns={dataGridColumns} columnVisibility={{ - visibleColumns: columns.map(({ id }) => id), + visibleColumns: formattedColumns.map(({ id }) => id), setVisibleColumns: () => {}, }} rowCount={rows.length} @@ -153,7 +132,11 @@ export const TableVisComponent = ({ showFullScreenSelector: false, showStyleSelector: false, additionalControls: ( - + ), }} /> diff --git a/src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx new file mode 100644 index 000000000000..570c8c0b853b --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TableVisComponentGroup } from './table_vis_component_group'; +import { TableVisConfig, ColumnSort } from '../types'; +import { Table, TableGroup } from '../table_vis_response_handler'; + +jest.mock('./table_vis_component', () => ({ + TableVisComponent: () =>
    TableVisComponent
    , +})); + +const table1 = { + table: { + columns: [], + rows: [], + formattedColumns: [], + } as Table, + title: '', +} as TableGroup; + +const table2 = { + table: { + columns: [], + rows: [], + formattedColumns: [], + } as Table, + title: '', +} as TableGroup; + +const tableUiStateMock = { + sort: { colIndex: undefined, direction: undefined } as ColumnSort, + setSort: jest.fn(), + width: [], + setWidth: jest.fn(), +}; + +describe('TableVisApp', () => { + it('should not render table or table group components if no table', () => { + const { container, queryAllByText } = render( + + ); + expect(queryAllByText('TableVisComponent')).toHaveLength(0); + expect(container.outerHTML.includes('visTable__group')).toBe(false); + }); + + it('should render table component 2 times', () => { + const { container, queryAllByText } = render( + + ); + expect(queryAllByText('TableVisComponent')).toHaveLength(2); + expect(container.outerHTML.includes('visTable__group')).toBe(true); + }); +}); 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 index 633b9d2230bd..af8fd8048cbc 100644 --- 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 @@ -7,8 +7,9 @@ import React, { memo } from 'react'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { TableGroup } from '../table_vis_response_handler'; -import { TableVisConfig, TableUiState } from '../types'; +import { TableVisConfig } from '../types'; import { TableVisComponent } from './table_vis_component'; +import { TableUiState } from '../utils'; interface TableVisGroupComponentProps { tableGroups: TableGroup[]; @@ -21,11 +22,11 @@ export const TableVisComponentGroup = memo( ({ tableGroups, visConfig, event, uiState }: TableVisGroupComponentProps) => { return ( <> - {tableGroups.map(({ tables, title }) => ( + {tableGroups.map(({ table, title }) => (
    { 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: { @@ -28,10 +23,10 @@ export const getDataGridColumns = ( { table: { columns: table.columns, - rows, + rows: table.rows, }, row: rowIndex, - column: rawColumnIndex, + column: columnIndex, }, ], negate, @@ -39,11 +34,11 @@ export const getDataGridColumns = ( }); }; - return cols.map((col, colIndex) => { + return table.formattedColumns.map((col, colIndex) => { const cellActions = col.filterable ? [ ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { - const filterValue = rows[rowIndex][columnId]; + const filterValue = table.rows[rowIndex][columnId]; const filterContent = col.formatter?.convert(filterValue); const filterForValueText = i18n.translate( @@ -79,7 +74,7 @@ export const getDataGridColumns = ( ); }, ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { - const filterValue = rows[rowIndex][columnId]; + const filterValue = table.rows[rowIndex][columnId]; const filterContent = col.formatter?.convert(filterValue); const filterOutValueText = i18n.translate( 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 4f0fb2c0ba1f..556cfaf24e00 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -4,7 +4,7 @@ */ import { i18n } from '@osd/i18n'; -import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; +import { tableVisResponseHandler, TableVisData } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, OpenSearchDashboardsDatatable, @@ -19,7 +19,7 @@ interface Arguments { } export interface TableVisRenderValue { - visData: TableContext; + visData: TableVisData; visType: 'table'; visConfig: TableVisConfig; } diff --git a/src/plugins/vis_type_table/public/table_vis_renderer.test.tsx b/src/plugins/vis_type_table/public/table_vis_renderer.test.tsx new file mode 100644 index 000000000000..ee18bcfaf734 --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_renderer.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { unmountComponentAtNode } from 'react-dom'; +import { act } from '@testing-library/react'; + +import { CoreStart } from 'opensearch-dashboards/public'; +import { getTableVisRenderer } from './table_vis_renderer'; +import { TableVisData } from './table_vis_response_handler'; +import { TableVisConfig } from './types'; +import { TableVisRenderValue } from './table_vis_fn'; + +const mockVisData = { + tableGroups: [], + direction: 'row', +} as TableVisData; + +const mockVisConfig = { + title: 'My Table', + metrics: [] as any, + buckets: [] as any, +} as TableVisConfig; + +const mockHandlers = { + done: jest.fn(), + reload: jest.fn(), + update: jest.fn(), + event: jest.fn(), + onDestroy: jest.fn(), +}; + +const mockCoreStart = {} as CoreStart; + +describe('getTableVisRenderer', () => { + let container: any = null; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container.remove(); + container = null; + }); + + it('should render table visualization', async () => { + const renderer = getTableVisRenderer(mockCoreStart); + const mockTableVisRenderValue = { + visData: mockVisData, + visType: 'table', + visConfig: mockVisConfig, + } as TableVisRenderValue; + await act(async () => { + renderer.render(container, mockTableVisRenderValue, mockHandlers); + }); + expect(container.querySelector('.tableVis')).toBeTruthy(); + }); + + it('should destroy table on unmount', async () => { + const renderer = getTableVisRenderer(mockCoreStart); + const mockTableVisRenderValue = { + visData: mockVisData, + visType: 'table', + visConfig: mockVisConfig, + } as TableVisRenderValue; + await act(async () => { + renderer.render(container, mockTableVisRenderValue, mockHandlers); + }); + await act(async () => { + unmountComponentAtNode(container); + }); + expect(mockHandlers.onDestroy).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_table/public/table_vis_response_handler.test.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.test.ts new file mode 100644 index 000000000000..89627854b449 --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_response_handler.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { tableVisResponseHandler } from './table_vis_response_handler'; + +jest.mock('./services', () => { + const formatService = { + deserialize: jest.fn(() => ({ + convert: jest.fn((value) => value), + })), + }; + + return { + getFormatService: () => formatService, + }; +}); + +const createTableGroup = (title, rows) => ({ + title, + table: { + columns: [ + { id: 'col-0', meta: { type: 'string' }, name: 'Column 1' }, + { id: 'col-1', meta: { type: 'number' }, name: 'Column 2' }, + ], + formattedColumns: [ + { + id: 'col-0', + title: 'Column 1', + formatter: { convert: expect.any(Function) }, + filterable: true, + }, + { + id: 'col-1', + title: 'Column 2', + formatter: { convert: expect.any(Function) }, + filterable: false, + }, + ], + rows, + }, +}); + +describe('tableVisResponseHandler', () => { + const input = { + type: 'datatable', + columns: [ + { id: 'col-0', name: 'Column 1', meta: { type: 'string' } }, + { id: 'col-1', name: 'Column 2', meta: { type: 'number' } }, + ], + rows: [ + { 'col-0': 'Group 1', 'col-1': 100 }, + { 'col-0': 'Group 2', 'col-1': 200 }, + ], + }; + + const baseVisConfig = { + title: 'My Table', + buckets: [ + { + accessor: 0, + label: 'Column 1', + format: { + id: 'string', + params: {}, + }, + params: {}, + aggType: 'terms', + }, + ], + metrics: [ + { + accessor: 1, + label: 'Count', + format: { + id: 'number', + }, + params: {}, + aggType: 'count', + }, + ], + }; + + const splitConfig = { + accessor: 0, + label: 'Column 1', + format: { + id: 'string', + params: {}, + }, + params: {}, + aggType: 'terms', + }; + + it('should correctly format data with splitRow', () => { + const visConfig = { ...baseVisConfig, splitRow: [splitConfig] }; + + const expected = { + table: undefined, + tableGroups: [ + createTableGroup('Group 1: Column 1', [{ 'col-0': 'Group 1', 'col-1': 100 }]), + createTableGroup('Group 2: Column 1', [{ 'col-0': 'Group 2', 'col-1': 200 }]), + ], + direction: 'row', + }; + + const result = tableVisResponseHandler(input, visConfig); + expect(result).toEqual(expected); + }); + + it('should correctly format data with splitColumn', () => { + const visConfig = { ...baseVisConfig, splitColumn: [splitConfig] }; + + const expected = { + table: undefined, + tableGroups: [ + createTableGroup('Group 1: Column 1', [{ 'col-0': 'Group 1', 'col-1': 100 }]), + createTableGroup('Group 2: Column 1', [{ 'col-0': 'Group 2', 'col-1': 200 }]), + ], + direction: 'column', + }; + + const result = tableVisResponseHandler(input, visConfig); + expect(result).toEqual(expected); + }); + + it('should correctly format data with no split', () => { + const visConfig = baseVisConfig; + + const expected = { + table: { + columns: input.columns, + formattedColumns: [ + { + id: 'col-0', + title: 'Column 1', + formatter: { convert: expect.any(Function) }, + filterable: true, + }, + { + id: 'col-1', + title: 'Column 2', + formatter: { convert: expect.any(Function) }, + filterable: false, + }, + ], + rows: input.rows, + }, + tableGroups: [], + direction: undefined, + }; + + const result = tableVisResponseHandler(input, visConfig); + expect(result).toEqual(expected); + }); +}); 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 b1d41edfff8b..975038c4c11f 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 @@ -30,25 +30,25 @@ import { getFormatService } from './services'; import { OpenSearchDashboardsDatatable } from '../../expressions/public'; -import { TableVisConfig } from './types'; +import { FormattedColumn, TableVisConfig } from './types'; +import { convertToFormattedData } from './utils/convert_to_formatted_data'; export interface Table { columns: OpenSearchDashboardsDatatable['columns']; rows: OpenSearchDashboardsDatatable['rows']; } +export interface FormattedTableContext extends Table { + formattedColumns: FormattedColumn[]; +} + export interface TableGroup { - table: OpenSearchDashboardsDatatable; - tables: Table[]; + table: FormattedTableContext; title: string; - name: string; - key: any; - column: number; - row: number; } -export interface TableContext { - table?: Table; +export interface TableVisData { + table?: FormattedTableContext; tableGroups: TableGroup[]; direction?: 'row' | 'column'; } @@ -56,10 +56,10 @@ export interface TableContext { export function tableVisResponseHandler( input: OpenSearchDashboardsDatatable, config: TableVisConfig -): TableContext { - let table: Table | undefined; +): TableVisData { + let table: FormattedTableContext | undefined; const tableGroups: TableGroup[] = []; - let direction: TableContext['direction']; + let direction: TableVisData['direction']; const split = config.splitColumn || config.splitRow; @@ -78,30 +78,32 @@ export function tableVisResponseHandler( (splitMap as any)[splitValue] = splitIndex++; const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, - name: splitColumn.name, - key: splitValue, - column: splitColumnIndex, - row: rowIndex, - table: input, - tables: [], + table: { + formattedColumns: [], + rows: [], + columns: input.columns, + }, }; - tableGroup.tables.push({ - columns: input.columns, - rows: [], - }); - tableGroups.push(tableGroup); } - const tableIndex = (splitMap as any)[splitValue]; - (tableGroups[tableIndex] as any).tables[0].rows.push(row); + const tableIndex = splitMap[splitValue]; + tableGroups[tableIndex].table.rows.push(row); + }); + + // format tables + tableGroups.forEach((tableGroup) => { + tableGroup.table = convertToFormattedData(tableGroup.table, config); }); } else { - table = { - columns: input.columns, - rows: input.rows, - }; + table = convertToFormattedData( + { + columns: input.columns, + rows: input.rows, + }, + config + ); } return { diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 814a86f5ac69..a14767f96302 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -75,10 +75,3 @@ 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/__snapshots__/add_percentage_col.test.ts.snap b/src/plugins/vis_type_table/public/utils/__snapshots__/add_percentage_col.test.ts.snap new file mode 100644 index 000000000000..41f4f24ecffb --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/__snapshots__/add_percentage_col.test.ts.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPercentageCol should add new percentage column 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 5, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": 3, + "col-1-1-percents": 0.6, + }, + Object { + "col-0-2": "Anthony", + "col-1-1": 1, + "col-1-1-percents": 0.2, + }, + Object { + "col-0-2": "Timmy", + "col-1-1": 1, + "col-1-1-percents": 0.2, + }, + ], +} +`; + +exports[`addPercentageCol should handle empty input data 1`] = ` +Object { + "cols": Array [], + "rows": Array [], +} +`; + +exports[`addPercentageCol should handle input data with null values 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 5, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": null, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Anthony", + "col-1-1": null, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Timmy", + "col-1-1": null, + "col-1-1-percents": 0, + }, + ], +} +`; + +exports[`addPercentageCol should handle input data with one row 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 5, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": 3, + "col-1-1-percents": 0.6, + }, + ], +} +`; + +exports[`addPercentageCol should handle sumTotal being 0 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 0, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": 3, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Anthony", + "col-1-1": 1, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Timmy", + "col-1-1": 1, + "col-1-1-percents": 0, + }, + ], +} +`; diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts b/src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts new file mode 100644 index 000000000000..5f27cb5e49ea --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { addPercentageCol } from './add_percentage_col'; +import { FormattedColumn } from '../types'; +import { Table } from '../table_vis_response_handler'; + +const mockDeserialize = jest.fn(() => ({})); +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +let formattedColumns: FormattedColumn[]; +const rows = [ + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, +] as Table['rows']; + +beforeEach(() => { + formattedColumns = [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + }, + { + id: 'col-1-1', + title: 'Count', + formatter: {}, + filterable: false, + sumTotal: 5, + }, + ] as FormattedColumn[]; +}); + +describe('addPercentageCol', () => { + it('should add new percentage column', () => { + const result = addPercentageCol(formattedColumns, 'count', rows, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle sumTotal being 0', () => { + formattedColumns[1].sumTotal = 0; + const result = addPercentageCol(formattedColumns, 'count', rows, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle empty input data', () => { + const emptyFormattedColumns: FormattedColumn[] = []; + const emptyRows: Table['rows'] = []; + const result = addPercentageCol(emptyFormattedColumns, 'count', emptyRows, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle input data with one row', () => { + const oneRow = [{ 'col-0-2': 'Alice', 'col-1-1': 3 }] as Table['rows']; + const result = addPercentageCol(formattedColumns, 'count', oneRow, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle input data with null values', () => { + const nullValueRows = [ + { 'col-0-2': 'Alice', 'col-1-1': null }, + { 'col-0-2': 'Anthony', 'col-1-1': null }, + { 'col-0-2': 'Timmy', 'col-1-1': null }, + ] as Table['rows']; + const result = addPercentageCol(formattedColumns, 'count', nullValueRows, 1); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_col.ts b/src/plugins/vis_type_table/public/utils/add_percentage_col.ts new file mode 100644 index 000000000000..8c300f29d06a --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_col.ts @@ -0,0 +1,76 @@ +/* + * 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 { Table } from '../table_vis_response_handler'; +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) + */ +export function addPercentageCol( + columns: FormattedColumn[], + title: string, + rows: Table['rows'], + insertAtIndex: number +) { + if (columns.length === 0) { + return { cols: columns, rows }; + } + 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]: sumTotal === 0 ? 0 : (row[id] as number) / (sumTotal as number), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} diff --git a/src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts new file mode 100644 index 000000000000..591dbe5454ce --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IUiSettingsClient } from 'opensearch-dashboards/public'; +import { FormattedColumn } from '../types'; +import { toCsv } from './convert_to_csv_data'; +import { IFieldFormat } from 'src/plugins/data/common'; + +const mockConvert = jest.fn((x) => x); +const defaultFormatter = { convert: (x) => mockConvert(x) } as IFieldFormat; + +function implementConvert(nRow: number) { + for (let i = 0; i < nRow; i++) { + mockConvert.mockImplementationOnce((x) => x); + mockConvert.mockImplementationOnce((x) => x); + mockConvert.mockImplementationOnce((x) => { + return parseFloat(x) * 100 + '%'; + }); + } +} + +const columns = [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: defaultFormatter, + filterable: true, + }, + { + id: 'col-1-1', + title: 'Count', + formatter: defaultFormatter, + filterable: false, + sumTotal: 5, + formattedTotal: 5, + total: 5, + }, + { + id: 'col-1-1-percents', + title: 'Count percentages', + formatter: defaultFormatter, + filterable: false, + }, +] as FormattedColumn[]; + +const rows = [ + { 'col-1-1-percents': 0.6, 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-1-1-percents': 0.2, 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-1-1-percents': 0.2, 'col-0-2': 'Timmy', 'col-1-1': 1 }, +]; + +const uiSettings = { + get: (key: string) => { + if (key === 'csv:separator') return ','; + else if (key === 'csv:quoteValues') return true; + }, +} as IUiSettingsClient; + +describe('toCsv', () => { + it('should create csv rows if not formatted', () => { + const result = toCsv(false, { rows, columns, uiSettings }); + expect(result).toEqual( + '"name.keyword: Descending",Count,"Count percentages"\r\nAlice,3,"0.6"\r\nAnthony,1,"0.2"\r\nTimmy,1,"0.2"\r\n' + ); + }); + + it('should create csv rows if formatted', () => { + implementConvert(3); + const result = toCsv(true, { rows, columns, uiSettings }); + expect(result).toEqual( + '"name.keyword: Descending",Count,"Count percentages"\r\nAlice,3,"60%"\r\nAnthony,1,"20%"\r\nTimmy,1,"20%"\r\n' + ); + }); +}); 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 index 2c37df1aa3d5..3d4781736689 100644 --- 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 @@ -46,7 +46,7 @@ interface CSVDataProps { uiSettings: CoreStart['uiSettings']; } -const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { +export const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { const separator = uiSettings.get(CSV_SEPARATOR_SETTING); const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING); diff --git a/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts new file mode 100644 index 000000000000..34085b70a278 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts @@ -0,0 +1,136 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { convertToFormattedData } from './convert_to_formatted_data'; +import { TableVisConfig } from '../types'; +import { Table } from '../table_vis_response_handler'; +import { AggTypes } from '../types'; + +const mockDeserialize = jest.fn(() => ({})); +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +const table = { + type: 'opensearch_dashboards_datatable', + columns: [ + { id: 'col-0-2', name: 'name.keyword: Descending', meta: { type: 'terms' } }, + { id: 'col-1-1', name: 'Count', meta: { type: 'count' } }, + ], + rows: [ + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, + ], +} as Table; + +let visConfig = {} as TableVisConfig; + +function implementDeserialize() { + mockDeserialize.mockImplementationOnce(() => ({})); + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((x: number) => x), + })); +} + +describe('convertToFormattedData', () => { + beforeEach(() => { + visConfig = { + buckets: [ + { + accessor: 0, + aggType: 'terms', + format: { + id: 'terms', + params: { + id: 'string', + missingBucketLabel: 'Missing', + otherBucketLabel: 'Other', + parsedUrl: { + basePath: '/arf', + origin: '', + pathname: '/arf/app/home', + }, + }, + }, + label: 'name.keyword: Descending', + params: {}, + }, + ], + metrics: [ + { + accessor: 1, + aggType: 'count', + format: { + id: 'number', + }, + label: 'Count', + params: {}, + }, + ], + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + title: '', + totalFunc: 'sum', + } as TableVisConfig; + }); + + it('should create formatted data', () => { + const result = convertToFormattedData(table, visConfig); + expect(result.rows).toEqual(table.rows); + expect(result.formattedColumns).toEqual([ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + }, + { id: 'col-1-1', title: 'Count', formatter: {}, filterable: false }, + ]); + }); + + describe.each([ + [AggTypes.SUM, 5], + [AggTypes.AVG, 1.6666666666666667], + [AggTypes.MIN, 1], + [AggTypes.MAX, 3], + [AggTypes.COUNT, 3], + ])('with totalFunc as %s', (totalFunc, expectedTotal) => { + beforeEach(() => { + implementDeserialize(); + visConfig.showTotal = true; + visConfig.totalFunc = totalFunc; + }); + + it(`should add ${totalFunc} total`, () => { + const result = convertToFormattedData(table, visConfig); + const expectedFormattedColumns = [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + ...(totalFunc === AggTypes.COUNT ? { sumTotal: 0, formattedTotal: 3, total: 3 } : {}), + }, + { + id: 'col-1-1', + title: 'Count', + formatter: { allowsNumericalAggregations: true, convert: expect.any(Function) }, + filterable: false, + sumTotal: 5, + formattedTotal: expectedTotal, + total: expectedTotal, + }, + ]; + expect(result.formattedColumns).toEqual(expectedFormattedColumns); + }); + }); +}); 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 index 2ab67e3b0a67..afb2906af8a1 100644 --- 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 @@ -28,56 +28,20 @@ * under the License. */ -import { i18n } from '@osd/i18n'; import { chain } from 'lodash'; -import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { + OpenSearchDashboardsDatatableRow, + OpenSearchDashboardsDatatableColumn, +} 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 }; -} +import { addPercentageCol } from './add_percentage_col'; export interface FormattedDataProps { - formattedRows: OpenSearchDashboardsDatatableRow[]; + rows: OpenSearchDashboardsDatatableRow[]; + columns: OpenSearchDashboardsDatatableColumn[]; formattedColumns: FormattedColumn[]; } @@ -107,12 +71,15 @@ export const convertToFormattedData = ( 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); + // only calculate the sumTotal for numerical columns + const sum = isBucket + ? 0 + : 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) { @@ -164,7 +131,7 @@ export const convertToFormattedData = ( ); // column to show percentage was removed - if (insertAtIndex < 0) return { formattedRows, formattedColumns }; + if (insertAtIndex < 0) return { rows: table.rows, columns: table.columns, formattedColumns }; const { cols, rows } = addPercentageCol( formattedColumns, @@ -175,5 +142,5 @@ export const convertToFormattedData = ( formattedRows = rows; formattedColumns = cols; } - return { formattedRows, formattedColumns }; + return { rows: formattedRows, columns: table.columns, formattedColumns }; }; diff --git a/src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts b/src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts new file mode 100644 index 000000000000..64488d9275eb --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PersistedState } from '../../../visualizations/public'; +import { TableUiState, getTableUIState } from './get_table_ui_state'; +import { ColumnWidth, ColumnSort } from '../types'; + +describe('getTableUIState', () => { + let uiState: PersistedState; + let tableUiState: TableUiState; + + beforeEach(() => { + uiState = ({ + get: jest.fn(), + set: jest.fn(), + emit: jest.fn(), + } as unknown) as PersistedState; + tableUiState = getTableUIState(uiState); + }); + + it('should get initial sort and width values from uiState', () => { + const initialSort: ColumnSort = { colIndex: 1, direction: 'asc' }; + const initialWidth: ColumnWidth[] = [{ colIndex: 0, width: 100 }]; + + (uiState.get as jest.Mock).mockImplementation((key: string) => { + if (key === 'vis.sortColumn') return initialSort; + if (key === 'vis.columnsWidth') return initialWidth; + }); + + const newTableUiState = getTableUIState(uiState); + expect(newTableUiState.sort).toEqual(initialSort); + expect(newTableUiState.colWidth).toEqual(initialWidth); + }); + + it('should set and emit sort values', () => { + const newSort: ColumnSort = { colIndex: 2, direction: 'desc' }; + tableUiState.setSort(newSort); + + expect(uiState.set).toHaveBeenCalledWith('vis.sortColumn', newSort); + expect(uiState.emit).toHaveBeenCalledWith('reload'); + }); + + it('should set and emit width values for a new column', () => { + const newWidth: ColumnWidth = { colIndex: 1, width: 150 }; + tableUiState.setWidth(newWidth); + + expect(uiState.set).toHaveBeenCalledWith('vis.columnsWidth', [newWidth]); + expect(uiState.emit).toHaveBeenCalledWith('reload'); + }); + + it('should update and emit width values for an existing column', () => { + const initialWidth: ColumnWidth[] = [{ colIndex: 0, width: 100 }]; + (uiState.get as jest.Mock).mockReturnValue(initialWidth); + + const updatedTableUiState = getTableUIState(uiState); + + const updatedWidth: ColumnWidth = { colIndex: 0, width: 150 }; + updatedTableUiState.setWidth(updatedWidth); + + const expectedWidths = [{ colIndex: 0, width: 150 }]; + expect(uiState.set).toHaveBeenCalledWith('vis.columnsWidth', expectedWidths); + expect(uiState.emit).toHaveBeenCalledWith('reload'); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/get_table_ui_state.ts b/src/plugins/vis_type_table/public/utils/get_table_ui_state.ts new file mode 100644 index 000000000000..58fc6b472a40 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/get_table_ui_state.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PersistedState } from '../../../visualizations/public'; +import { ColumnSort, ColumnWidth } from '../types'; + +export interface TableUiState { + sort: ColumnSort; + setSort: (sort: ColumnSort) => void; + colWidth: ColumnWidth[]; + setWidth: (columnWidths: ColumnWidth) => void; +} + +export function getTableUIState(uiState: PersistedState): TableUiState { + const sort: ColumnSort = uiState.get('vis.sortColumn') || {}; + const colWidth: ColumnWidth[] = uiState.get('vis.columnsWidth') || []; + + const setSort = (newSort: ColumnSort) => { + uiState.set('vis.sortColumn', newSort); + uiState.emit('reload'); + }; + + const setWidth = (columnWidth: ColumnWidth) => { + const nextState = [...colWidth]; + const curColIndex = colWidth.findIndex((col) => col.colIndex === columnWidth.colIndex); + + if (curColIndex < 0) { + nextState.push(columnWidth); + } else { + nextState[curColIndex] = columnWidth; + } + + uiState.set('vis.columnsWidth', nextState); + uiState.emit('reload'); + }; + + return { sort, setSort, colWidth, setWidth }; +} diff --git a/src/plugins/vis_type_table/public/utils/index.ts b/src/plugins/vis_type_table/public/utils/index.ts index 1fd0e3f1e0fd..3277d92efc71 100644 --- a/src/plugins/vis_type_table/public/utils/index.ts +++ b/src/plugins/vis_type_table/public/utils/index.ts @@ -6,3 +6,5 @@ export * from './convert_to_csv_data'; export * from './convert_to_formatted_data'; export * from './use_pagination'; +export * from './add_percentage_col'; +export * from './get_table_ui_state'; diff --git a/src/plugins/vis_type_table/public/utils/use_pagination.test.ts b/src/plugins/vis_type_table/public/utils/use_pagination.test.ts new file mode 100644 index 000000000000..d8e7a02a0799 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use_pagination.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AggTypes, TableVisParams } from '../types'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + const visParams = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + } as TableVisParams; + + it('should not set pagination if perPage is empty string', () => { + const params = { + ...visParams, + perPage: '', + }; + const { result } = renderHook(() => usePagination(params, 20)); + expect(result.current).toEqual(undefined); + }); + + it('should init pagination', () => { + const { result } = renderHook(() => usePagination(visParams, 20)); + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should init pagination with pageSize as the minimum of perPage and nRow', () => { + const { result } = renderHook(() => usePagination(visParams, 8)); + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 8, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should set pageSize to the lesser of perPage and nRow when nRow is less than perPage', () => { + const { result } = renderHook(() => usePagination(visParams, 5)); + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should set page index via onChangePage', () => { + const { result } = renderHook(() => usePagination(visParams, 50)); + act(() => { + // set page index to 3 + result.current?.onChangePage(3); + }); + expect(result.current?.pageIndex).toEqual(3); + }); + + it('should set to max page index via onChangePage if exceed maxiPageIndex', () => { + const { result, rerender } = renderHook((props) => usePagination(props.visParams, props.nRow), { + initialProps: { + visParams, + nRow: 55, + }, + }); + + act(() => { + // set page index to the last page + result.current?.onChangePage(5); + }); + + rerender({ visParams, nRow: 15 }); + // when the number of rows decreases, page index should + // be set to maxiPageIndex + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should pagination via onChangeItemsPerPage', () => { + const { result } = renderHook(() => usePagination(visParams, 20)); + act(() => { + // set page size to 5 + result.current?.onChangeItemsPerPage(5); + }); + + expect(result.current?.pageSize).toEqual(5); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use_pagination.ts index e3993f1c0868..97ecfd6b85e6 100644 --- a/src/plugins/vis_type_table/public/utils/use_pagination.ts +++ b/src/plugins/vis_type_table/public/utils/use_pagination.ts @@ -4,12 +4,12 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TableVisConfig } from '../types'; +import { TableVisParams } from '../types'; -export const usePagination = (visConfig: TableVisConfig, nRow: number) => { +export const usePagination = (visParams: TableVisParams, nRow: number) => { const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: Math.min(visConfig.perPage || 10, nRow), + pageSize: Math.min(visParams.perPage || 0, nRow), }); const onChangeItemsPerPage = useCallback( (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), @@ -20,20 +20,23 @@ export const usePagination = (visConfig: TableVisConfig, nRow: number) => { ]); useEffect(() => { - const perPage = Math.min(visConfig.perPage || 10, nRow); + const perPage = Math.min(visParams.perPage || 0, nRow); const maxiPageIndex = Math.ceil(nRow / perPage) - 1; setPagination((p) => ({ pageIndex: p.pageIndex > maxiPageIndex ? maxiPageIndex : p.pageIndex, pageSize: perPage, })); - }, [nRow, visConfig.perPage]); + }, [nRow, visParams.perPage]); return useMemo( - () => ({ - ...pagination, - onChangeItemsPerPage, - onChangePage, - }), + () => + pagination.pageSize + ? { + ...pagination, + onChangeItemsPerPage, + onChangePage, + } + : undefined, [pagination, onChangeItemsPerPage, onChangePage] ); }; diff --git a/src/plugins/vis_type_timeline/server/series_functions/label.js b/src/plugins/vis_type_timeline/server/series_functions/label.js index c935d537081f..4649ee6cf53f 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/label.js +++ b/src/plugins/vis_type_timeline/server/series_functions/label.js @@ -62,10 +62,8 @@ export default new Chainable('label', { const config = args.byName; return alter(args, function (eachSeries) { if (config.regex) { - // not using a standard `import` so that if there's an issue with the re2 native module - // that it doesn't prevent OpenSearch Dashboards from starting up and we only have an issue using Timeline labels - const RE2 = require('re2'); - eachSeries.label = eachSeries.label.replace(new RE2(config.regex), config.label); + const regex = new RegExp(config.regex); + eachSeries.label = eachSeries.label.replace(regex, config.label); } else { eachSeries.label = config.label; } diff --git a/src/plugins/vis_type_timeline/server/series_functions/label.test.js b/src/plugins/vis_type_timeline/server/series_functions/label.test.js index c6dc832914a3..69268b385b07 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/label.test.js +++ b/src/plugins/vis_type_timeline/server/series_functions/label.test.js @@ -51,4 +51,24 @@ describe('label.js', () => { expect(r.output.list[0].label).to.equal('beerative'); }); }); + + it('can use a regex to capture groups to modify series label', () => { + return invoke(fn, [seriesList, 'beer$2', '(N)(egative)']).then((r) => { + expect(r.output.list[0].label).to.equal('beeregative'); + }); + }); + + it('can handle different regex patterns', () => { + const seriesListCopy1 = JSON.parse(JSON.stringify(seriesList)); + const seriesListCopy2 = JSON.parse(JSON.stringify(seriesList)); + + return Promise.all([ + invoke(fn, [seriesListCopy1, 'beer$1 - $2', '(N)(egative)']).then((r) => { + expect(r.output.list[0].label).to.equal('beerN - egative'); + }), + invoke(fn, [seriesListCopy2, 'beer$1_$2', '(N)(eg.*)']).then((r) => { + expect(r.output.list[0].label).to.equal('beerN_egative'); + }), + ]); + }); }); diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 91f017a437c3..d6df1573a661 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -33,10 +33,10 @@ import { resolve } from 'path'; import execa from 'execa'; import grunt from 'grunt'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; const JOBS_YAML = readFileSync(resolve(__dirname, '../.ci/jobs.yml'), 'utf8'); -const TEST_TAGS = safeLoad(JOBS_YAML) +const TEST_TAGS = load(JOBS_YAML) .JOB.filter((id) => id.startsWith('opensearch-dashboards-ciGroup')) .map((id) => id.replace(/^opensearch-dashboards-/, '')); diff --git a/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json index a0ff828390c6..5f308268566c 100644 --- a/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json @@ -12,7 +12,7 @@ "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@osd/plugin-helpers": "1.0.0", "react": "^16.14.0", "react-dom": "^16.12.0", diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index e733a4e36368..ce027815a57f 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -52,6 +52,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/doc_views_links'), require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), + require.resolve('./test_suites/dashboard_listing_plugin'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json b/test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json new file mode 100644 index 000000000000..454d9ea58471 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json @@ -0,0 +1,9 @@ +{ + "id": "dashboard_listing_test_plugin", + "version": "0.0.1", + "opensearchDashboardsVersion": "opensearchDashboards", + "configPath": ["dashboard_listing_test_plugin"], + "server": false, + "ui": true, + "requiredPlugins": ["dashboard"] +} diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json b/test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json new file mode 100644 index 000000000000..0b593604a2ad --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "dashboard_listing_test_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/dashboard_listing_test_plugin", + "opensearchDashboards": { + "version": "opensearchDashboards", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "osd": "../../../../scripts/use_node ../../../../scripts/osd.js", + "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts new file mode 100644 index 000000000000..80ddbf8a3382 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { PluginInitializer } from 'opensearch-dashboards/public'; +import { + DashboardListingTestPlugin, + DashboardListingTestPluginSetup, + DashboardListingTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + DashboardListingTestPluginSetup, + DashboardListingTestPluginStart +> = () => new DashboardListingTestPlugin(); diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx new file mode 100644 index 000000000000..76a407f7c0d2 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx @@ -0,0 +1,53 @@ +/* + * 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 * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Router, Switch, Route, Link } from 'react-router-dom'; +import { CoreSetup, Plugin } from 'opensearch-dashboards/public'; + +export class DashboardListingTestPlugin + implements Plugin { + public setup(core: CoreSetup, setupDeps: SetupDependencies) { + const ID = 'dashboard_listing_test_plugin'; + const BASE_URL = core.http.basePath.prepend(`/app/${ID}#`); + setupDeps.dashboard.registerDashboardProvider({ + appId: ID, + savedObjectsType: 'dashboardTest', + savedObjectsName: 'Dashboard Test', + editUrlPathFn: (obj: SavedObject) => `${BASE_URL}/${obj.id}/edit`, + viewUrlPathFn: (obj: SavedObject) => `${BASE_URL}/${obj.id}`, + createLinkText: 'Test Dashboard', + createSortText: 'Test Dashboard', + createUrl: `${BASE_URL}/create`, + }); + + core.application.register({ + id: ID, + title: 'Dashboard Listing Test Plugin', + appRoute: `app/${ID}`, + async mount(context, { element }) { + render( +

    Dashboard Listing Test Header

    , + element + ); + + return () => unmountComponentAtNode(element); + }, + }); + } + + public start() {} + public stop() {} +} + +export type DashboardListingTestPluginSetup = ReturnType; +export type DashboardListingTestPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json b/test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json new file mode 100644 index 000000000000..f77a5eaffc30 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/plugins/osd_sample_panel_action/package.json b/test/plugin_functional/plugins/osd_sample_panel_action/package.json index 5c5237717ac3..35f7e84ae0e2 100644 --- a/test/plugin_functional/plugins/osd_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/osd_sample_panel_action/package.json @@ -12,7 +12,7 @@ "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "react": "^16.14.0", "typescript": "4.0.2" } diff --git a/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json index 23be4ebf14d2..13f7b1695a0d 100644 --- a/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json @@ -12,7 +12,7 @@ "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@osd/plugin-helpers": "1.0.0", "react": "^16.14.0", "typescript": "4.0.2" diff --git a/test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts b/test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts new file mode 100644 index 000000000000..354cfac4fa87 --- /dev/null +++ b/test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts @@ -0,0 +1,71 @@ +/* + * 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 url from 'url'; +import expect from '@osd/expect'; + +const getPathWithHash = (absoluteUrl: string) => { + const parsed = url.parse(absoluteUrl); + return `${parsed.path}${parsed.hash ?? ''}`; +}; + +export default function ({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header']); + const browser = getService('browser'); + const listingTable = getService('listingTable'); + const find = getService('find'); + + describe('dashboard listing plugin', function describeIndexTests() { + const dashboardName = 'Dashboard Test'; + + before(async () => { + await PageObjects.dashboard.initTests({ + opensearchDashboardsIndex: '../functional/fixtures/opensearch_archiver/dashboard/legacy', + }); + await PageObjects.dashboard.clickCreateDashboardPrompt(); + await PageObjects.dashboard.saveDashboard('default'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('should be able to navigate to create a dashboard', async () => { + await testSubjects.click('createMenuDropdown'); + await testSubjects.click('contextMenuItem-dashboard'); + await PageObjects.dashboard.saveDashboard(dashboardName); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1); + }); + + it('should be able to navigate to view dashboard', async () => { + await listingTable.clickItemLink('dashboard', dashboardName); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + await PageObjects.dashboard.getIsInViewMode(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('should be able to navigate to edit dashboard', async () => { + await listingTable.searchForItemWithName(dashboardName); + const editBttn = await find.allByCssSelector('.euiToolTipAnchor'); + await editBttn[0].click(); + await PageObjects.dashboard.clickCancelOutOfEditMode(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('should be able to navigate to create a test dashboard', async () => { + await testSubjects.click('createMenuDropdown'); + await testSubjects.click('contextMenuItem-dashboard_listing_test_plugin'); + expect(getPathWithHash(await browser.getCurrentUrl())).to.eql( + '/app/dashboard_listing_test_plugin#/create' + ); + }); + }); +} diff --git a/test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts b/test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts new file mode 100644 index 000000000000..a84790824f64 --- /dev/null +++ b/test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts @@ -0,0 +1,35 @@ +/* + * 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 default function ({ getService, loadTestFile }) { + const browser = getService('browser'); + const opensearchArchiver = getService('opensearchArchiver'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await opensearchArchiver.loadIfNeeded( + '../functional/fixtures/opensearch_archiver/logstash_functional' + ); + } + + async function unloadLogstash() { + await opensearchArchiver.unload( + '../functional/fixtures/opensearch_archiver/logstash_functional' + ); + } + + describe('dashboard listing plugin', () => { + before(loadLogstash); + after(unloadLogstash); + + loadTestFile(require.resolve('./dashboard_listing_plugin')); + }); +} diff --git a/yarn.lock b/yarn.lock index 455b3475298f..b70c14a53a0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1273,10 +1273,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@npm:@opensearch-project/oui@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@opensearch-project/oui/-/oui-1.0.0.tgz#bf5e115c8d0f230415b07cc6acfb149ab081c5de" - integrity sha512-J709UQc7+il4y3aiqpHzeLOJAQhN6xEGLLHq4sUL3WHTsP37acONINXCpRNMa3FxZ+ChOd2ABmY+Ajs+fIgmug== +"@elastic/eui@npm:@opensearch-project/oui@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@opensearch-project/oui/-/oui-1.1.1.tgz#4a9318c2954659cdd8d83263ff2dc22a77cbd779" + integrity sha512-RBXbsZh6mjJKJqB/hSI2loenyM2rvdq9id29P/ZYlZGKKy0/tSreIOGcegSYMtNFmG029D20xVkhRmdn7cxK1A== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -2338,14 +2338,14 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@opensearch-project/opensearch@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.1.0.tgz#d79ab4ae643493512099673e117faffe40b4fe56" - integrity sha512-iM2u63j2IlUOuMSbcw1TZFpRqjK6qMwVhb3jLLa/x4aATxdKOiO1i17mgzfkeepqj85efNzXBZzN+jkq1/EXhQ== +"@opensearch-project/opensearch@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.2.1.tgz#a400203afa6512ef73945663163a404763a10f5a" + integrity sha512-8zfQX1acL9eWG+ohIc9nJVT9LSqXCdbVEJs0rCPRtji3XF6ahzsiKmGNTeWLxCPDxWCjAIWq9t95xP3Y5Egi6Q== dependencies: aws4 "^1.11.0" debug "^4.3.1" - hpagent "^0.1.1" + hpagent "^1.2.0" ms "^2.1.3" secure-json-parse "^2.4.0" @@ -3126,10 +3126,10 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== -"@types/js-yaml@^3.11.1": - version "3.12.7" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e" - integrity sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ== +"@types/js-yaml@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" @@ -4141,7 +4141,7 @@ agent-base@4: dependencies: es6-promisify "^5.0.0" -agent-base@6, agent-base@^6.0.2: +agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -4155,7 +4155,7 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +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== @@ -4369,11 +4369,6 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -"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" @@ -4413,14 +4408,6 @@ archy@^1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -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 "^3.6.0" - argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -5284,7 +5271,7 @@ cacache@^13.0.1: ssri "^7.0.0" unique-filename "^1.1.1" -cacache@^15.0.5, cacache@^15.2.0: +cacache@^15.0.5: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== @@ -5842,11 +5829,6 @@ 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" @@ -5973,11 +5955,6 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -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 sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - console-log-level@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" @@ -6655,7 +6632,7 @@ debug@3.X, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -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: +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.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -6886,11 +6863,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - delete-empty@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/delete-empty/-/delete-empty-2.0.0.tgz#dcf7c4f93a98445119acd57b137d13e7af78fa39" @@ -7337,13 +7309,6 @@ 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" @@ -7389,11 +7354,6 @@ entities@~2.1.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - envinfo@^7.7.3: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -7472,11 +7432,6 @@ 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" @@ -8795,20 +8750,6 @@ 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@^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 || ^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.2: version "3.2.0" resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.2.0.tgz#6b0a85e2aafbce209bca30e2d53af857707b1034" @@ -9389,11 +9330,6 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - has-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" @@ -9589,11 +9525,6 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoek@5.x.x: - version "5.0.4" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da" - integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w== - hoek@6.x.x: version "6.1.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" @@ -9630,10 +9561,10 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" -hpagent@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" - integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== +hpagent@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-1.2.0.tgz#0ae417895430eb3770c03443456b8d90ca464903" + integrity sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA== html-element-map@^1.2.0: version "1.3.1" @@ -9707,12 +9638,12 @@ htmlparser2@^7.0: domutils "^2.8.0" entities "^3.0.1" -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-aws-es@npm:@zhongnansu/http-aws-es@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@zhongnansu/http-aws-es/-/http-aws-es-6.0.1.tgz#1ab929eb7faa78ac5386c84069fb2e885fe1661c" + integrity sha512-uhe3FUtgT+sDyW2VA0hxQ44HUfeI8tf4nUo83mc3jtQPAIO57ZpRFKePmCoU+qaqHWfesD9hIQkTkXOJGlW26w== -http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: +http-cache-semantics@^4.0.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== @@ -9791,7 +9722,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.2: +iconv-lite@0.6: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -9979,11 +9910,6 @@ inquirer@^7.0.0, inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -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" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -10075,11 +10001,6 @@ 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" @@ -10320,11 +10241,6 @@ 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" @@ -11187,12 +11103,12 @@ jmespath@0.16.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -joi@^13.5.2: - version "13.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" - integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== +joi@^14.3.1: + version "14.3.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-14.3.1.tgz#164a262ec0b855466e0c35eea2a885ae8b6c703c" + integrity sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ== dependencies: - hoek "5.x.x" + hoek "6.x.x" isemail "3.x.x" topo "3.x.x" @@ -11234,14 +11150,14 @@ js-yaml-js-types@1.0.0: dependencies: esprima "^4.0.1" -js-yaml@4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" -js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@~3.14.0: +js-yaml@^3.13.1, js-yaml@~3.14.0: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -11993,28 +11909,6 @@ 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" @@ -12390,17 +12284,6 @@ 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" @@ -12408,20 +12291,13 @@ minipass-flush@^1.0.5: dependencies: minipass "^3.0.0" -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: +minipass-pipeline@^1.2.2: 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" @@ -12429,13 +12305,6 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" -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" @@ -12443,7 +12312,7 @@ minipass@^4.0.0: dependencies: yallist "^4.0.0" -minizlib@^2.0.0, minizlib@^2.1.1: +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== @@ -12661,11 +12530,6 @@ nan@^2.12.1, nan@^2.14.2: 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" @@ -12736,11 +12600,6 @@ 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" @@ -12824,22 +12683,6 @@ 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@^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.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -12913,13 +12756,6 @@ nopt@^2.2.0: dependencies: abbrev "1" -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -13001,16 +12837,6 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -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 "^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" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" @@ -13974,14 +13800,6 @@ 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" @@ -14224,15 +14042,6 @@ re-reselect@^3.4.0: resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd" integrity sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg== -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.3.0" - nan "^2.15.0" - node-gyp "^8.4.1" - react-ace@^7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-7.0.5.tgz#798299fd52ddf3a3dcc92afc5865538463544f01" @@ -15154,11 +14963,6 @@ 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.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" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -15604,11 +15408,6 @@ slide@~1.1.3: 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" @@ -15639,23 +15438,6 @@ 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" @@ -15900,7 +15682,7 @@ ssri@^7.0.0: figgy-pudding "^3.5.1" minipass "^3.1.1" -ssri@^8.0.0, ssri@^8.0.1: +ssri@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== @@ -16037,15 +15819,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.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" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -16063,6 +15836,15 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +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" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string.prototype.matchall@^4.0.6: version "4.0.7" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" @@ -16551,7 +16333,7 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: +tar@^6.0.2, tar@^6.1.11: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== @@ -18270,13 +18052,6 @@ which@^2.0.1, which@^2.0.2, which@~2.0.2: dependencies: isexe "^2.0.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== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -18478,15 +18253,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yaml@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec" - integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== +yaml@^1.10.0, yaml@^2.0.0, yaml@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" + integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== yargs-parser@20.2.4: version "20.2.4"