diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 48c74b29765b..e883a5a1c9e0 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -3,12 +3,16 @@ name: Build and test -# trigger on every commit push and PR for all branches except feature branches +# trigger on every commit push and PR for all branches except feature branches and pushes for backport branches on: push: - branches: [ '**', '!feature/**' ] + branches: [ '**', '!feature/**', '!backport/**' ] + paths-ignore: + - '**/*.md' pull_request: branches: [ '**', '!feature/**' ] + paths-ignore: + - '**/*.md' env: TEST_BROWSER_HEADLESS: 1 @@ -75,7 +79,7 @@ jobs: name: Run functional tests strategy: matrix: - group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ] + group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} @@ -182,7 +186,7 @@ jobs: working-directory: ./artifacts strategy: matrix: - version: [ osd-2.0.0 ] + version: [ osd-2.0.0, osd-2.1.0, osd-2.2.0 ] steps: - name: Checkout code uses: actions/checkout@v2 @@ -221,6 +225,10 @@ jobs: - name: Skipping tests if: steps.verify-opensearch-exists.outputs.version-exists != 'true' run: echo Tests were skipped because an OpenSearch release build does not exist for this version yet! + + - name: Setting environment variable to run tests for ${{ matrix.version }} + if: steps.verify-opensearch-exists.outputs.version-exists == 'true' + run: echo "BWC_VERSIONS=${{ matrix.version }}" >> $GITHUB_ENV - name: Download OpenSearch Dashboards uses: actions/download-artifact@v3 @@ -233,4 +241,13 @@ jobs: - name: Run tests if: steps.verify-opensearch-exists.outputs.version-exists == 'true' run: | - ./bwctest.sh -s false -o ${{ env.OPENSEARCH_URL }} -d ${{ steps.download.outputs.download-path }}/opensearch-dashboards-${{ env.VERSION }}-linux-x64.tar.gz \ No newline at end of file + ./bwctest.sh -s false -o ${{ env.OPENSEARCH_URL }} -d ${{ steps.download.outputs.download-path }}/opensearch-dashboards-${{ env.VERSION }}-linux-x64.tar.gz + + - uses: actions/upload-artifact@v3 + if: ${{ failure() && steps.verify-opensearch-exists.outputs.version-exists == 'true' }} + with: + name: ${{ matrix.version }}-test-failures + path: | + ./artifacts/bwc_tmp/test/cypress/videos/without-security/* + ./artifacts/bwc_tmp/test/cypress/screenshots/without-security/* + retention-days: 1 diff --git a/.github/workflows/create_doc_issue.yml b/.github/workflows/create_doc_issue.yml index 808af0029a10..299aa576b3bc 100644 --- a/.github/workflows/create_doc_issue.yml +++ b/.github/workflows/create_doc_issue.yml @@ -34,7 +34,7 @@ jobs: with: title: Add documentation related to new feature content-filepath: ./.github/doc_issue_template.md - labels: documentation + labels: dashboards repository: opensearch-project/documentation-website token: ${{ steps.github_app_token.outputs.token }} diff --git a/.i18nrc.json b/.i18nrc.json index 71c7970affa4..234aa8de0c25 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -22,6 +22,7 @@ "interpreter": "src/legacy/core_plugins/interpreter", "osd": "src/legacy/core_plugins/opensearch-dashboards", "osdDocViews": "src/legacy/core_plugins/osd_doc_views", + "osdDocViewsLinks": "src/legacy/core_plugins/osd_doc_views_links", "management": [ "src/legacy/core_plugins/management", "src/plugins/management" diff --git a/.node-version b/.node-version index 9ddeebac881c..a3eb5a03fa6a 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.19.1 +14.20.0 diff --git a/.nvmrc b/.nvmrc index 9ddeebac881c..a3eb5a03fa6a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.19.1 +14.20.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index bd61be433d04..a0ea08c6ac8a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,25 +1,4 @@ - -This code of conduct applies to all spaces provided by the OpenSource project including in code, documentation, issue trackers, mailing lists, chat channels, wikis, blogs, social media and any other communication channels used by the project. - - -**Our open source communities endeavor to:** - -* Be Inclusive: We are committed to being a community where everyone can join and contribute. This means using inclusive and welcoming language. -* Be Welcoming: We are committed to maintaining a safe space for everyone to be able to contribute. -* Be Respectful: We are committed to encouraging differing viewpoints, accepting constructive criticism and work collaboratively towards decisions that help the project grow. Disrespectful and unacceptable behavior will not be tolerated. -* Be Collaborative: We are committed to supporting what is best for our community and users. When we build anything for the benefit of the project, we should document the work we do and communicate to others on how this affects their work. - - -**Our Responsibility. As contributors, members, or bystanders we each individually have the responsibility to behave professionally and respectfully at all times. Disrespectful and unacceptable behaviors include, but are not limited to:** - -* The use of violent threats, abusive, discriminatory, or derogatory language; -* Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, race, political or religious affiliation; -* Posting of sexually explicit or violent content; -* The use of sexualized language and unwelcome sexual attention or advances; -* Public or private harassment of any kind; -* Publishing private information, such as physical or electronic address, without permission; -* Other conduct which could reasonably be considered inappropriate in a professional setting; -* Advocating for or encouraging any of the above behaviors. -* Enforcement and Reporting Code of Conduct Issues: - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported. [Contact us](mailto:opensource-codeofconduct@amazon.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. \ No newline at end of file +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. \ No newline at end of file diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 1c33af417390..4dc3e0d2583e 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -340,8 +340,8 @@ variable can’t be null has changed (potentially even due to changes in compelt assertion would now wrongly disable proper type checking for us. If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the -[`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) -linting rule for you plugin in the [`.eslintrc.js`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/.eslintrc.js) config. +[`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) +linting rule for you plugin in the [`.eslintrc.js`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/.eslintrc.js) config. ### Return/throw early from functions diff --git a/Dockerfile b/Dockerfile index f976d4875114..d5fddcd2c6cf 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_VERSION=14.19.1 +ARG NODE_VERSION=14.20.0 FROM node:${NODE_VERSION} AS base ENV HOME '.' diff --git a/Jenkinsfile b/Jenkinsfile index c221237aeb93..2b967bc59d01 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,6 +68,7 @@ def functionalDynamicParallelSteps(image){ "ciGroup10", "ciGroup11", "ciGroup12", + "ciGroup13", ] for (int i = 0; i < ciGroups.size(); i++) { def currentCiGroup = ciGroups[i]; diff --git a/TESTING.md b/TESTING.md index 15c2792d3307..d93475e4b317 100644 --- a/TESTING.md +++ b/TESTING.md @@ -24,7 +24,7 @@ In general, we recommend four tiers of tests: # Requirements * Install the latest NodeJS, [NPM](https://www.npmjs.com/get-npm) and [Yarn](https://classic.yarnpkg.com/en/docs/install/#mac-stable) - * `nvm install v14.19.1` + * `nvm install v14.20.0` * `npm install -g yarn` # Running tests @@ -50,7 +50,7 @@ To run specific functional tests, you can run by CI group: To debug functional tests: Say that you would want to debug a test in CI group 1, you can run the following command in your environment: -`node --debug-brk --inspect scripts/functional_tests.js --config test/functional/config.js --include ciGroup1 --debug` +`node --inspect-brk --inspect scripts/functional_tests.js --config test/functional/config.js --include ciGroup1 --debug` This will print off an address, to which you could open your chrome browser on your instance and navigate to `chrome://inspect/#devices` and inspect the functional test runner `scripts/functional_tests.js`. @@ -89,9 +89,24 @@ Automated testing is provided with Jenkins for Continuous Integration. Jenkins e Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable: `export TEST_BROWSER_HEADLESS=1` +Since local Selenium tests are run in a real browser, the dev environment should have a desktop environment and Google Chrome or Chromium installed to run the tests. + By default the version of OpenSearch Dashboards will pull the snapshot of the same version of OpenSearch if available while running a few integration tests and for running functional tests. However, if the version of OpenSearch Dashboards is not available, you can build OpenSearch locally and point the functional test runner to the executable with: `export TEST_OPENSEARCH_FROM=[local directory of OpenSearch executable]` +Selinium tests require a chromedriver and a corresponding version of chrome to run properly. Depending on the version of chromedriver used, you may need to use a version of Google Chrome that is not the latest version. You can do this by running: + +```sh +# Enter the version of chrome that you want to install +CHROME_VERSION=100.0.4896.127-1 + +# Download Chrome to a temp directory +curl -sSL "https://dl.google.com/linux/linux_signing_key.pub" | sudo apt-key add - && wget -O /tmp/chrome.deb "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb" + +# Install/Downgrade Chrome +sudo apt-get install -y --allow-downgrades /tmp/chrome.deb +``` + # Misc Although Jest is the standard for this project, there are a few Mocha tests that still exist. You can run these tests by running: `yarn test:mocha` diff --git a/bwctest.sh b/bwctest.sh index def139795a4b..61b8ca244e06 100755 --- a/bwctest.sh +++ b/bwctest.sh @@ -13,7 +13,7 @@ set -e -DEFAULT_VERSIONS="osd-2.0.0," +DEFAULT_VERSIONS="osd-2.0.0,osd-2.1.0,osd-2.2.0" function usage() { echo "" diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 162545b2a1a5..69bd26c18d20 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -40,7 +40,10 @@ # This node attribute should assign all nodes of the same cluster an integer value that increments with each new cluster that is spun up # e.g. in opensearch.yml file you would set the value to a setting using node.attr.cluster_id: # Should only be enabled if there is a corresponding node attribute created in your OpenSearch config that matches the value here -#opensearch.optimizedHealthcheckId: "cluster_id" +#opensearch.optimizedHealthcheck.id: "cluster_id" +#opensearch.optimizedHealthcheck.filters: { +# attribute_key: "attribute_value", +#} # If your OpenSearch is protected with basic authentication, these settings provide # the username and password that the OpenSearch Dashboards server uses to perform maintenance on the OpenSearch Dashboards @@ -131,8 +134,9 @@ # Set the allowlist to check input graphite Url. Allowlist is the default check list. #vis_type_timeline.graphiteAllowedUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'] -# Set the blocklist to check input graphite Url. Blocklist is an IP list. +# Set the denylist to check input graphite Url. Denylist is an IP list. # Below is an example for reference + # vis_type_timeline.graphiteBlockedIPs: [ # //Loopback # '127.0.0.0/8', @@ -161,7 +165,37 @@ # '2001:db8::/32', # 'ff00::/8', # ] -#vis_type_timeline.graphiteBlockedIPs: [] + +# vis_type_timeline.graphiteDeniedIPs: [] + +# vis_type_timeline.graphiteDeniedIPs: [ +# //Loopback +# '127.0.0.0/8', +# '::1/128', +# //Link-local Address for IPv6 +# 'fe80::/10', +# //Private IP address for IPv4 +# '10.0.0.0/8', +# '172.16.0.0/12', +# '192.168.0.0/16', +# //Unique local address (ULA) +# 'fc00::/7', +# //Reserved IP address +# '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', +# ] # opensearchDashboards.branding: # logo: @@ -175,11 +209,17 @@ # darkModeUrl: "" # faviconUrl: "" # applicationTitle: "" +# useExpandedHeader: false -# Set the value of this setting to true to capture region blocked warnings and errors +# Set the value of this setting to true to capture region denied warnings and errors # for your map rendering services. -# map.showRegionBlockedWarning: false + +# map.showRegionDeniedWarning: false # Set the value of this setting to false to suppress search usage telemetry # for reducing the load of OpenSearch cluster. # data.search.usageTelemetry.enabled: false + +# Set the value of this setting to true to start exploring wizard +# functionality in Visualization. +# wizard.enabled: false diff --git a/cypress/integration/with-security/check_filter_and_query.js b/cypress/integration/with-security/check_filter_and_query.js index 0055e5c078ed..e1a486dcfce5 100644 --- a/cypress/integration/with-security/check_filter_and_query.js +++ b/cypress/integration/with-security/check_filter_and_query.js @@ -12,6 +12,8 @@ import { const miscUtils = new MiscUtils(cy); const commonUI = new CommonUI(cy); const loginPage = new LoginPage(cy); +const startDate = 'Nov 1, 2016 @ 00:00:00.000'; +const endDate = `Dec 31, ${new Date().getFullYear()} @ 00:00:00.000`; describe('check dashboards filter and query', () => { beforeEach(() => { @@ -66,7 +68,7 @@ describe('check dashboards filter and query', () => { .find('[class="osdSavedQueryListItem__labelText"]') .should('have.text', 'test-query') .click(); - commonUI.setDateRange('Dec 1, 2021 @ 00:00:00.000', 'Jan 1, 2021 @ 00:00:00.000'); + commonUI.setDateRange(endDate, startDate); //[Logs] vistor chart should show osx 100% cy.get('[data-title="[Logs] Visitors by OS"]') diff --git a/cypress/integration/without-security/check_filter_and_query.js b/cypress/integration/without-security/check_filter_and_query.js index 30911d05ba7e..65b9aaf4a98b 100644 --- a/cypress/integration/without-security/check_filter_and_query.js +++ b/cypress/integration/without-security/check_filter_and_query.js @@ -10,6 +10,8 @@ import { const miscUtils = new MiscUtils(cy); const commonUI = new CommonUI(cy); +const startDate = 'Nov 1, 2016 @ 00:00:00.000'; +const endDate = `Dec 31, ${new Date().getFullYear()} @ 00:00:00.000`; describe('check dashboards filter and query', () => { beforeEach(() => { @@ -56,7 +58,7 @@ describe('check dashboards filter and query', () => { .find('[class="osdSavedQueryListItem__labelText"]') .should('have.text', 'test-query') .click(); - commonUI.setDateRange('Dec 1, 2021 @ 00:00:00.000', 'Jan 1, 2021 @ 00:00:00.000'); + commonUI.setDateRange(endDate, startDate); //[Logs] vistor chart should show osx 100% cy.get('[data-title="[Logs] Visitors by OS"]') diff --git a/cypress/test-data/with-security/osd-2.1.0.tar.gz b/cypress/test-data/with-security/osd-2.1.0.tar.gz new file mode 100644 index 000000000000..c1028e5705a0 Binary files /dev/null and b/cypress/test-data/with-security/osd-2.1.0.tar.gz differ diff --git a/cypress/test-data/with-security/osd-2.2.0.tar.gz b/cypress/test-data/with-security/osd-2.2.0.tar.gz new file mode 100644 index 000000000000..2f2c98f1e3da Binary files /dev/null and b/cypress/test-data/with-security/osd-2.2.0.tar.gz differ diff --git a/cypress/test-data/without-security/osd-2.1.0.tar.gz b/cypress/test-data/without-security/osd-2.1.0.tar.gz new file mode 100644 index 000000000000..642bca840e60 Binary files /dev/null and b/cypress/test-data/without-security/osd-2.1.0.tar.gz differ diff --git a/cypress/test-data/without-security/osd-2.2.0.tar.gz b/cypress/test-data/without-security/osd-2.2.0.tar.gz new file mode 100644 index 000000000000..d229d6b7ed26 Binary files /dev/null and b/cypress/test-data/without-security/osd-2.2.0.tar.gz differ diff --git a/examples/bfetch_explorer/public/plugin.tsx b/examples/bfetch_explorer/public/plugin.tsx index 140369e08da0..b3645eb90379 100644 --- a/examples/bfetch_explorer/public/plugin.tsx +++ b/examples/bfetch_explorer/public/plugin.tsx @@ -75,7 +75,7 @@ export class BfetchExplorerPlugin implements Plugin { { label: 'README', href: - 'https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/src/plugins/bfetch/README.md', + 'https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/bfetch/README.md', iconType: 'logoGithub', size: 's', target: '_blank', diff --git a/examples/developer_examples/README.md b/examples/developer_examples/README.md index e33d7cc8f64f..0453b90162b2 100644 --- a/examples/developer_examples/README.md +++ b/examples/developer_examples/README.md @@ -14,7 +14,7 @@ services. Add your a link to your example using the developerExamples `register` links: [ { label: 'README', - href: 'https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/src/plugins/foo/README.md', + href: 'https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/foo/README.md', iconType: 'logoGithub', target: '_blank', size: 's', diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 18b694c8480b..f36a556ea93a 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -86,7 +86,7 @@ export class EmbeddableExplorerPlugin implements Plugin ***NOTE:*** These commands should be run from the OpenSearch Dashboards repo, and `upstream` is our convention for the git remote that references https://github.com/opensearch-project/OpenSearch-Dashboards.git, unless you added this remote you might need to use `origin`. ```sh -git pull upstream master +git pull upstream main yarn osd bootstrap ``` diff --git a/packages/osd-plugin-generator/template/README.md.ejs b/packages/osd-plugin-generator/template/README.md.ejs index ee1184e7e238..b28e29d36144 100755 --- a/packages/osd-plugin-generator/template/README.md.ejs +++ b/packages/osd-plugin-generator/template/README.md.ejs @@ -7,7 +7,7 @@ ## Development See the [OpenSearch Dashboards contributing - guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/CONTRIBUTING.md) for instructions + guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. <% if (thirdPartyPlugin) { %> diff --git a/packages/osd-pm/dist/index.js b/packages/osd-pm/dist/index.js index 0cefa9767f41..336a6c56b299 100644 --- a/packages/osd-pm/dist/index.js +++ b/packages/osd-pm/dist/index.js @@ -86225,7 +86225,7 @@ module.exports = ProgressEmitter; "use strict"; -const blacklist = [ +const denylist = [ // # All '^npm-debug\\.log$', // Error log for npm '^\\..*\\.swp$', // Swap file for vim state @@ -86254,7 +86254,7 @@ exports.re = () => { throw new Error('`junk.re` was renamed to `junk.regex`'); }; -exports.regex = new RegExp(blacklist.join('|')); +exports.regex = new RegExp(denylist.join('|')); exports.is = filename => exports.regex.test(filename); diff --git a/packages/osd-spec-to-console/README.md b/packages/osd-spec-to-console/README.md index 11c6570c96ce..52bf6681c7a4 100644 --- a/packages/osd-spec-to-console/README.md +++ b/packages/osd-spec-to-console/README.md @@ -1,4 +1,4 @@ -\ mini utility to convert [OpenSearch's REST spec](https://github.com/opensearch-project/OpenSearch/blob/master/rest-api-spec) to Console's (OpenSearch Dashboards) autocomplete format. +\ mini utility to convert [OpenSearch's REST spec](https://github.com/opensearch-project/OpenSearch/blob/main/rest-api-spec) to Console's (OpenSearch Dashboards) autocomplete format. It is used to semi-manually update Console's autocompletion rules. @@ -13,7 +13,7 @@ git init git remote add origin https://github.com/opensearch-project/OpenSearch git config core.sparsecheckout true echo "rest-api-spec/src/main/resources/rest-api-spec/api/*\nx-pack/plugin/src/test/resources/rest-api-spec/api/*" > .git/info/sparse-checkout -git pull --depth=1 origin master +git pull --depth=1 origin main ``` ### Usage diff --git a/packages/osd-spec-to-console/test/fixtures/cluster_health_autocomplete.json b/packages/osd-spec-to-console/test/fixtures/cluster_health_autocomplete.json index a7334198cc7f..4a49ad089e2c 100644 --- a/packages/osd-spec-to-console/test/fixtures/cluster_health_autocomplete.json +++ b/packages/osd-spec-to-console/test/fixtures/cluster_health_autocomplete.json @@ -14,6 +14,7 @@ ], "local": "__flag__", "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "", "wait_for_active_shards": "", "wait_for_nodes": "", diff --git a/packages/osd-spec-to-console/test/fixtures/cluster_health_spec.json b/packages/osd-spec-to-console/test/fixtures/cluster_health_spec.json index 31e504f2a668..3975d505c2f9 100644 --- a/packages/osd-spec-to-console/test/fixtures/cluster_health_spec.json +++ b/packages/osd-spec-to-console/test/fixtures/cluster_health_spec.json @@ -57,6 +57,10 @@ "type":"time", "description":"Explicit operation timeout for connection to master node" }, + "cluster_manager_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to cluster manager node" + }, "timeout":{ "type":"time", "description":"Explicit operation timeout" diff --git a/packages/osd-std/package.json b/packages/osd-std/package.json index 731eee02a448..93bf1abb81d9 100644 --- a/packages/osd-std/package.json +++ b/packages/osd-std/package.json @@ -15,6 +15,6 @@ "devDependencies": { "@osd/utility-types": "1.0.0", "typescript": "4.0.2", - "tsd": "^0.16.0" + "tsd": "^0.21.0" } } diff --git a/packages/osd-std/src/__snapshots__/validate_object.test.ts.snap b/packages/osd-std/src/__snapshots__/validate_object.test.ts.snap new file mode 100644 index 000000000000..937e040c771e --- /dev/null +++ b/packages/osd-std/src/__snapshots__/validate_object.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`can't submit {"__proto__":null} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"constructor":{"prototype":null}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"__proto__":true}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"__proto__":{}}}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"constructor":{"prototype":null}}}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"constructor":{"prototype":null}}} 1`] = `"'constructor.prototype' is an invalid key"`; diff --git a/packages/osd-std/src/index.ts b/packages/osd-std/src/index.ts index 3f2db99efd5d..49902a2c2479 100644 --- a/packages/osd-std/src/index.ts +++ b/packages/osd-std/src/index.ts @@ -38,4 +38,5 @@ export { withTimeout } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; +export { validateObject } from './validate_object'; export * from './rxjs_7'; diff --git a/packages/osd-std/src/validate_object.test.ts b/packages/osd-std/src/validate_object.test.ts new file mode 100644 index 000000000000..4bf3d04302e4 --- /dev/null +++ b/packages/osd-std/src/validate_object.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { validateObject } from './validate_object'; + +test(`fails on circular references`, () => { + const foo: Record = {}; + foo.myself = foo; + + expect(() => + validateObject({ + payload: foo, + }) + ).toThrowErrorMatchingInlineSnapshot(`"circular reference detected"`); +}); + +[ + { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + { + foo: { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + }, + { constructor: { foo: { prototype: null } } }, + { prototype: { foo: { constructor: null } } }, +].forEach((value) => { + ['headers', 'payload', 'query', 'params'].forEach((property) => { + const obj = { + [property]: value, + }; + test(`can submit ${JSON.stringify(obj)}`, () => { + expect(() => validateObject(obj)).not.toThrowError(); + }); + }); +}); + +// if we use the object literal syntax to create the following values, we end up +// actually reassigning the __proto__ which makes it be a non-enumerable not-own property +// which isn't what we want to test here +[ + JSON.parse(`{ "__proto__": null }`), + JSON.parse(`{ "foo": { "__proto__": true } }`), + JSON.parse(`{ "foo": { "bar": { "__proto__": {} } } }`), + JSON.parse(`{ "constructor": { "prototype" : null } }`), + JSON.parse(`{ "foo": { "constructor": { "prototype" : null } } }`), + JSON.parse(`{ "foo": { "bar": { "constructor": { "prototype" : null } } } }`), +].forEach((value) => { + test(`can't submit ${JSON.stringify(value)}`, () => { + expect(() => validateObject(value)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/packages/osd-std/src/validate_object.ts b/packages/osd-std/src/validate_object.ts new file mode 100644 index 000000000000..96b42050a361 --- /dev/null +++ b/packages/osd-std/src/validate_object.ts @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +interface StackItem { + value: any; + previousKey: string | null; +} + +// we have to do Object.prototype.hasOwnProperty because when you create an object using +// Object.create(null), and I assume other methods, you get an object without a prototype, +// so you can't use current.hasOwnProperty +const hasOwnProperty = (obj: any, property: string) => + Object.prototype.hasOwnProperty.call(obj, property); + +const isObject = (obj: any) => typeof obj === 'object' && obj !== null; + +// we're using a stack instead of recursion so we aren't limited by the call stack +export function validateObject(obj: any) { + if (!isObject(obj)) { + return; + } + + const stack: StackItem[] = [ + { + value: obj, + previousKey: null, + }, + ]; + const seen = new WeakSet([obj]); + + while (stack.length > 0) { + const { value, previousKey } = stack.pop()!; + + if (!isObject(value)) { + continue; + } + + if (hasOwnProperty(value, '__proto__')) { + throw new Error(`'__proto__' is an invalid key`); + } + + if (hasOwnProperty(value, 'prototype') && previousKey === 'constructor') { + throw new Error(`'constructor.prototype' is an invalid key`); + } + + // iterating backwards through an array is reportedly more performant + const entries = Object.entries(value); + for (let i = entries.length - 1; i >= 0; --i) { + const [key, childValue] = entries[i]; + if (isObject(childValue)) { + if (seen.has(childValue)) { + throw new Error('circular reference detected'); + } + + seen.add(childValue); + } + + stack.push({ + value: childValue, + previousKey: key, + }); + } + } +} diff --git a/packages/osd-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/osd-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 86fff0dec924..74604dc51690 100644 --- a/packages/osd-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/osd-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -65,9 +65,9 @@ export function runFailedTestsReporterCli() { } const isPr = !!process.env.ghprbPullId; - const isMasterOrVersion = branch === 'master' || branch.match(/^\d+\.(x|\d+)$/); - if (!isMasterOrVersion || isPr) { - log.info('Failure issues only created on master/version branch jobs'); + const isMainOrVersion = branch === 'main' || branch.match(/^\d+\.(x|\d+)$/); + if (!isMainOrVersion || isPr) { + log.info('Failure issues only created on main/version branch jobs'); updateGithub = false; } } diff --git a/packages/osd-ui-framework/Gruntfile.js b/packages/osd-ui-framework/Gruntfile.js index 9ad26b578e08..b52e1430f56e 100644 --- a/packages/osd-ui-framework/Gruntfile.js +++ b/packages/osd-ui-framework/Gruntfile.js @@ -28,7 +28,7 @@ * under the License. */ -const sass = require('node-sass'); +const sass = require('sass'); const postcss = require('postcss'); const postcssConfig = require('@osd/optimizer/postcss.config.js'); diff --git a/packages/osd-ui-framework/package.json b/packages/osd-ui-framework/package.json index 621698bce559..a89cc82a9d42 100644 --- a/packages/osd-ui-framework/package.json +++ b/packages/osd-ui-framework/package.json @@ -23,14 +23,14 @@ "enzyme-adapter-react-16": "^1.9.1" }, "devDependencies": { - "@elastic/eui": "34.6.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", "@osd/babel-preset": "1.0.0", "@osd/optimizer": "1.0.0", "grunt": "^1.5.2", "grunt-babel": "^8.0.0", "grunt-contrib-clean": "^2.0.0", "grunt-contrib-copy": "^1.0.0", - "node-sass": "^6.0.1", + "sass": "~1.26.11", "postcss": "^8.4.5", "sinon": "^7.4.2" } diff --git a/packages/osd-ui-shared-deps/package.json b/packages/osd-ui-shared-deps/package.json index 62776c80a3c9..8536d2fa813c 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": "34.6.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", "@elastic/numeral": "^2.5.0", "@osd/i18n": "1.0.0", "@osd/monaco": "1.0.0", diff --git a/packages/osd-utility-types/package.json b/packages/osd-utility-types/package.json index 63c7dbc02753..aa5beca212ef 100644 --- a/packages/osd-utility-types/package.json +++ b/packages/osd-utility-types/package.json @@ -20,6 +20,6 @@ }, "devDependencies": { "del-cli": "^3.0.1", - "tsd": "^0.16.0" + "tsd": "^0.21.0" } } diff --git a/release-notes/opensearch-dashboards.release-notes-1.1.0.md b/release-notes/opensearch-dashboards.release-notes-1.1.0.md index 73238dbc186e..88dd91f0be81 100644 --- a/release-notes/opensearch-dashboards.release-notes-1.1.0.md +++ b/release-notes/opensearch-dashboards.release-notes-1.1.0.md @@ -198,7 +198,7 @@ Bumps [jszip](https://github.com/Stuk/jszip) from 3.3.0 to 3.7.1. - [Release notes](https://github.com/Stuk/jszip/releases) - - [Changelog](https://github.com/Stuk/jszip/blob/master/CHANGES.md) + - [Changelog](https://github.com/Stuk/jszip/blob/main/CHANGES.md) - [Commits](https://github.com/Stuk/jszip/compare/v3.3.0...v3.7.1) updated-dependencies: diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.5.md b/release-notes/opensearch-dashboards.release-notes-1.3.5.md new file mode 100644 index 000000000000..5d86383302da --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.5.md @@ -0,0 +1,14 @@ +## Version 1.3.5 Release Notes + +### 🛡 Security +* Bump moment from 2.29.2 to 2.29.4 ([#1931](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1931)) +* [CVE] Add resolution for url-parse ([#2181](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2181)) + +### 🐛 Bug Fixes +Fix WMS can't load when unable access maps services([#1550](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1550)) + +### 🔩 Tests +* Bump chrome driver version in PR check workflow ([#2186](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2186)) + +### 🛠 Maintenance +* Version increment to 1.3.5 ([#1901](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1901)) \ No newline at end of file diff --git a/release-notes/opensearch-dashboards.release-notes-2.1.0.md b/release-notes/opensearch-dashboards.release-notes-2.1.0.md new file mode 100644 index 000000000000..ab50daa9189b --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.1.0.md @@ -0,0 +1,38 @@ +## Version 2.1.0 Release Notes + +#### Deprecations +* Changes config name in yml file to new non-deprecated name ([#1485](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1485)) +* Deprecate isDevClusterMaster in favor of isDevClusterManager ([#1719](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1719)) +* Deprecate setupMaster in favor of setupClusterManager ([#1752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1752)) +* Deprecate master nodes and replace with cluster_manager nodes ([#1761](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1761)) +* Replace master in comments ([#1778](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1778)) +* Replace references to master branch with main ([#1780](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1780)) +* Deprecate master_timeout in favor of cluster_manager_timeout ([#1788](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1788)) +* Deprecate the apiVersion: master value and replace with main ([#1799](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1799)) +* Deprecate cat master API in favor of cat cluster_manager ([#1800](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1800)) + +### 🛡 Security +* Adding noreferrer on doc links ([#1709](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1709)) +* [CVE-2022-25851] Resolve jpeg-js to 0.4.4 ([#1753](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1753)) +* [CVE-2022-33987] Bump tsd from 0.16.0 to 0.21.0 ([#1770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1770)) + +### 📈 Enhancements +* Logic to enable extensibility for the maps plugin ([#1632](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1632)) +* [Consolidated global header] Use `opensearchDashboards.branding.useExpandedHeader: false` to use the consolidated menu and global header bar. ([#1586](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1586)) ([#1802](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1802)) + +### 🚞 Infrastructure +* Allow Node patch versions to be higher on runtime if bundled Node is not available ([#1189](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1189)) + +### 📝 Documentation +* [Admin] Adds Josh Romero as co-maintainer ([#1682](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1682)) +* Fixes formatting and typos in documentation ([#1697](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1697)) + +### 🛠 Maintenance +* [Version] Increment to 2.1 ([#1503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1503)) + +### 🔩 Tests +* Migrate mocha tests to jest ([#1553](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1553)) +* Add backwards compatibility tests to github actions ([#1624](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1624)) +* Date range for tests incorrect params related to backwards compatibility tests ([#1772](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1772)) +* Update tests to reflect max zoom level for maps ([#1823](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1823)) + * Maps zoom levels updated from current zoom level 10 to zoom level 14 on coordinate and region maps. This feature helps you visualize more granular geo data diff --git a/release-notes/opensearch-dashboards.release-notes-2.2.0.md b/release-notes/opensearch-dashboards.release-notes-2.2.0.md new file mode 100644 index 000000000000..90e383646b68 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.2.0.md @@ -0,0 +1,46 @@ +## Version 2.2.0 Release Notes + +#### Notable changes +* Bump node version to 14.20.0 ([#2101](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2101)) +* OpenSearch Dashboards uses [OUI](https://github.com/opensearch-project/oui) and its alias onto EUI ([#2080](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2080)) +* New experimental feature: adds the Drag and Drop editor to Visualize. Note this is disabled by default. Please enable by setting `wizard.enabled: true` in `opensearch_dashboards.yml` ([#1966](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1966)) + +#### Deprecations +* Deprecate the Blacklist / Whitelist nomenclature ([#1808](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1808)) + +### 📈 Features/Enhancements +* Add DocView links pluggable injection capability ([#1200](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1200)) +* Enable users to select custom vector map for visualization ([#1718](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1718)) +* [UX] Restyle global breadcrumbs ([#1954](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1954)) +* [Feature] Adds the Drag and Drop editor to Visualize ([#1966](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1966)) +* Alias OUI onto EUI ([#2080](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2080)) + +### 🛡 Security +* Bump terser from 4.8.0 to 4.8.1 ([#1930](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1930)) +* Bump moment from 2.29.2 to 2.29.4 ([#1931](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1931)) +* [CVE] Handle invalid query, index and date in vega charts filter handlers ([#1946](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1946)) +* Bump node version to 14.20.0 ([#2101](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2101)) + +### 📝 Documentation +* [Docs] Add developer documentation for using/modifying the chrome service ([#1875](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1875)) +* [Docs] Updates Code of Conduct ([#1964](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1964)) + +### 🐛 Bug Fixes +* [Bug] Fix new issue link ([#1837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1837)) +* Remove banner when editing maps visualization ([#1848](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1848)) +* Fixes issue on saving custom vector map options as part of visualization ([#1896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1896)) +* [BUG] Fixing some of the broken links in core plugin API documentations ([#1946](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1946)) +* [BUG] show region blocked warning config not respected ([#2042](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2042)) +* [BUG] Telemetry plugin cluster info rename error ([#2043](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2043)) +* [Bug] fix TSVB y-axis ([#2079](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2079)) +* [Bug] Fix Global Breadcrumb Styling in dark mode ([#2085](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2085)) + +### 🚞 Refactor +* changes js code to ts in region_map ([#2084](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2084)) + +### 🛠 Maintenance +* [Version] Increment to 2.2 ([#1860](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1860)) + +### 🔩 Tests +* [CI][Tests] add BWC tests for 2.2.0 ([#1861](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1861)) +* [CI] Clean up for BWC tests & run only on PRs for backports ([#1948](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1948)) \ No newline at end of file diff --git a/release-notes/opensearch-dashboards.release-notes-2.2.1.md b/release-notes/opensearch-dashboards.release-notes-2.2.1.md new file mode 100644 index 000000000000..f8870298e81d --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.2.1.md @@ -0,0 +1,4 @@ +## Version 2.2.1 Release Notes + +### 🛠 Maintenance +* Bump 2.2 branch version to be 2.2.1 ([#2129](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2129)) \ No newline at end of file diff --git a/release-notes/opensearch-dashboards.release-notes-2.3.0.md b/release-notes/opensearch-dashboards.release-notes-2.3.0.md new file mode 100644 index 000000000000..f952e37e40a2 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.3.0.md @@ -0,0 +1,31 @@ +## Version 2.3.0 Release Notes + +### 📈 Features/Enhancements + +- [D&D] Add new states in metadata slice ([#2193](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2193)) +- [D&D] Add visualization type switcher for Wizard ([#2217](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2217)) +- [D&D] Save index pattern using proper saved object structure ([#2218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2218)) +- [D&D] Persist index field on agg type change if possible ([#2227](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2227)) +- [D&D] Add count field to field picker ([#2231](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2231)) +- [D&D] Add Bar line and Area charts to Wizard ([#2266](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2266)) + +### 🐛 Bug Fixes + +- Fix maps wms zoom limitation ([#1915](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1915)) +- [Visualizations] Add visConfig.title and uiState to build pipeline function (([#2192](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2192)) +- Custom healthcheck with filters ([#2232](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2232)) +- [BUG] Fix healthcheck logic to expect object and return ids ([#2277](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2277)) + +### 🚞 Refactor + +- [DeAngular][visualization][vislib] Remove angular from vislib ([#2138](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2138)) +- Change timeline icon ([#2162](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2162)) + +### 🛠 Maintenance + +- Increment from 2.2 to 2.3. ([#2096](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2096)) + +### 🔩 Tests + +- [CI][tests] Add backwards compatibility (BWC) tests for 2.3.0 ([#2281](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2281)) +- Fix 2.x backwards compatibility (BWC) tests by restoring 1.3.2 ([#2284](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2284)) diff --git a/src/core/README.md b/src/core/README.md index f38b9b49db86..3577813e17a0 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -4,11 +4,11 @@ Core is a set of systems (frontend, backend etc.) that OpenSearch Dashboards and ## Plugin development Core Plugin API Documentation: - - [Core Public API](/docs/development/core/public/opensearch-dashboards-plugin-core-public.md) - - [Core Server API](/docs/development/core/server/opensearch-dashboards-plugin-core-server.md) + - [Core Public API](../core/public/public.api.md) + - [Core Server API](../core/server/server.api.md) - [Conventions for Plugins](./CONVENTIONS.md) - [Testing OpenSearch Dashboards Plugins](./TESTING.md) - - [OpenSearch Dashboards Platform Plugin API](./docs/developer/architecture/kibana-platform-plugin-api.asciidoc ) + - [OpenSearch Dashboards Platform Plugin API](./PRINCIPLES.md) Internal Documentation: - [Saved Objects Migrations](./server/saved_objects/migrations/README.md) diff --git a/src/core/public/_variables.scss b/src/core/public/_variables.scss index bb8baa3462f1..f77e2b4d2755 100644 --- a/src/core/public/_variables.scss +++ b/src/core/public/_variables.scss @@ -1,3 +1,10 @@ @import "@elastic/eui/src/global_styling/variables/header"; -$osdHeaderOffset: $euiHeaderHeightCompensation * 2; +$osdHeaderOffset: $euiHeaderHeightCompensation; +$osdHeaderBreadcrumbBlueBackground: #b9d9eb; +$osdHeaderBreadcrumbGrayBackground: #d9e1e2; +$osdHeaderBreadcrumbCollapsedLink: #002a3a; +$osdHeaderBreadcrumbPacificSkyDarkestBackground: #163f66; +$osdHeaderBreadcrumbMidnightSkyMediumBackground: #4c636f; +$osdHeaderBreadcrumbMidnightSkyMediumLightColor: #96a0a5; +$osdHeaderBreadcrumbMidnightSkyMediumLightHoverColor: #b0b8bb; diff --git a/src/core/public/chrome/README.md b/src/core/public/chrome/README.md new file mode 100644 index 000000000000..6ec765a3bb0b --- /dev/null +++ b/src/core/public/chrome/README.md @@ -0,0 +1,130 @@ + +## Chrome Service + +- [About!](#about-) +- [Nav Controls Service](#navcontrolsservice-) +- [Nav Links Service](#navlinksservice-) +- [Recently Accessed Service](#recentlyaccessedservice-) +- [Doc Title Service](#doctitleservice-) +- [UI](#ui-) + +## About : +- **Signature** - `export interface ChromeStart` +The chrome service is a high level UI service that is part of CoreStart (Core services exposed to the Plugin start lifecycle) and offers other plugins a way to add navigation controls to the UI, edit the document title, manipuate navlinks on global header as well as edit the recent accessed tab. It consists of these sub-services/components. + +- NavControlsService : for registering new controls to be displayed in the navigation bar. +- NavLinksService : for manipulating nav links. +- RecentlyAccessedService : for recently accessed history. +- DocTitleService: for accessing and updating the document title +- UI : All the UI components,icons e.g. header, loaders. + + +- How to access ? add in interface `chrome: ChromeStart && chrome.servicesName => e.g chrome.docTitle.method` +- Where it is getting Registered/Executed ? [Staring Point](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/2.1/src/core/public/rendering/rendering_service.tsx) +- How header component is getting rendered ? `const chromeUi = chrome.getHeaderComponent(); ` +- Chrome Methods [See chrome interface/methods](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/2.1/src/core/public/chrome/chrome_service.tsx) + + +### NavControlsService : +- Interface : ChromeNavControls +- **Signature** - `navControls: ChromeNavControls` +- Registering new controller + +Example : +Register a left-side nav control rendered with React. + +```jsx +chrome.navControls.registerLeft({ + mount(targetDomElement) { + ReactDOM.mount(, targetDomElement); + return () => ReactDOM.unmountComponentAtNode(targetDomElement); + } +}) +``` +### NavLinksService : +- Interface : ChromeNavLinks +- **Signature** - `navLinks: ChromeNavLinks` +- e.g. Method : + +Get an observable for a sorted list of navlinks :- + +`getNavLinks$(): Observable>>` + +Get the state of a navlink at this point in time :- + +`get(id: string): ChromeNavLink | undefined` + +Get the current state of all navlinks :- + +`getAll(): Array>` + +Check whether or not a navlink exists :- + +`has(id: string): boolean` + +Remove all navlinks except the one matching the given id :- +`showOnly(id: string): void` + +- How to access + ###### Get the current state of all navlinks: + `core.chrome.navLinks.getAll()` + + ###### Get the state of a navlink at this point in time: + `core.chrome.navLinks.get()` + +### RecentlyAccessedService : + +- Interface : ChromeRecentlyAccessed +- Signature : ```recentlyAccessed: ChromeRecentlyAccessed``` +- The Recently viewed items are stored in the browser's local storage. +- You can go back to the recent search/visualization/dashboard , each item has (link,label,id) +- Methods : + +Adds a new item to the recently accessed history :- + +`add(link: string, label: string, id: string): void` + +Gets an Array of the current recently accessed history :- + +`get(): ChromeRecentlyAccessedHistoryItem[]` + +Gets an Observable of the array of recently accessed history :- + +`get$(): Observable` + +- How to access + ###### Adds a new item to the recently accessed history : + ` + chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234'); + ` + ###### Gets an Array of the current recently accessed history : + ` + chrome.recentlyAccessed.get().forEach(console.log);; + ` + +### DocTitleService : +- Interface : ChromeDocTitle +- **Signature** - `docTitle: ChromeDocTitle` + ###### - How to change the title of the document + + + ```ts + chrome.docTitle.change('My application title') + chrome.docTitle.change(['My application', 'My section']) + ``` +### UI : +###### consists of tsx/scss files && renders UI components from css Library e.g `````` + +###### Adding/overriding existing css : +- Create scss file and define class e.g .osdCustomClass{} +- pass className prop to UI component. + e.g ` ` + +###### UI Components : + - HeaderBreadcrumbs : Props + + - responsive:boolean Hides extra (above the max) breadcrumbs under a collapsed item as the window gets smaller. + - max: Collapses the inner items past the maximum set here into a single ellipses item. + - breadcrumbs : The array of individual Breadcrumb items + + \ No newline at end of file diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index ebfa010b9431..da5cbda1c92a 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -257,6 +257,8 @@ export class ChromeService { navControlsLeft$={navControls.getLeft$()} navControlsCenter$={navControls.getCenter$()} navControlsRight$={navControls.getRight$()} + navControlsExpandedCenter$={navControls.getExpandedCenter$()} + navControlsExpandedRight$={navControls.getExpandedRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} branding={injectedMetadata.getBranding()} diff --git a/src/core/public/chrome/nav_controls/nav_controls_service.test.ts b/src/core/public/chrome/nav_controls/nav_controls_service.test.ts index 15c94805dc6f..6e2a71537e17 100644 --- a/src/core/public/chrome/nav_controls/nav_controls_service.test.ts +++ b/src/core/public/chrome/nav_controls/nav_controls_service.test.ts @@ -36,7 +36,7 @@ describe('RecentlyAccessed#start()', () => { return new NavControlsService().start(); }; - describe('left side', () => { + describe('left contorols', () => { it('allows registration', async () => { const navControls = getStart(); const nc = { mount: jest.fn() }; @@ -56,7 +56,7 @@ describe('RecentlyAccessed#start()', () => { }); }); - describe('right side', () => { + describe('right controls', () => { it('allows registration', async () => { const navControls = getStart(); const nc = { mount: jest.fn() }; @@ -75,4 +75,72 @@ describe('RecentlyAccessed#start()', () => { expect(await navControls.getRight$().pipe(take(1)).toPromise()).toEqual([nc2, nc1, nc3]); }); }); + + describe('center controls', () => { + it('allows registration', async () => { + const navControls = getStart(); + const nc = { mount: jest.fn() }; + navControls.registerCenter(nc); + expect(await navControls.getCenter$().pipe(take(1)).toPromise()).toEqual([nc]); + }); + + it('sorts controls by order property', async () => { + const navControls = getStart(); + const nc1 = { mount: jest.fn(), order: 10 }; + const nc2 = { mount: jest.fn(), order: 0 }; + const nc3 = { mount: jest.fn(), order: 20 }; + navControls.registerCenter(nc1); + navControls.registerCenter(nc2); + navControls.registerCenter(nc3); + expect(await navControls.getCenter$().pipe(take(1)).toPromise()).toEqual([nc2, nc1, nc3]); + }); + }); + + describe('expanded right controls', () => { + it('allows registration', async () => { + const navControls = getStart(); + const nc = { mount: jest.fn() }; + navControls.registerExpandedRight(nc); + expect(await navControls.getExpandedRight$().pipe(take(1)).toPromise()).toEqual([nc]); + }); + + it('sorts controls by order property', async () => { + const navControls = getStart(); + const nc1 = { mount: jest.fn(), order: 10 }; + const nc2 = { mount: jest.fn(), order: 0 }; + const nc3 = { mount: jest.fn(), order: 20 }; + navControls.registerExpandedRight(nc1); + navControls.registerExpandedRight(nc2); + navControls.registerExpandedRight(nc3); + expect(await navControls.getExpandedRight$().pipe(take(1)).toPromise()).toEqual([ + nc2, + nc1, + nc3, + ]); + }); + }); + + describe('expanded center controls', () => { + it('allows registration', async () => { + const navControls = getStart(); + const nc = { mount: jest.fn() }; + navControls.registerExpandedCenter(nc); + expect(await navControls.getExpandedCenter$().pipe(take(1)).toPromise()).toEqual([nc]); + }); + + it('sorts controls by order property', async () => { + const navControls = getStart(); + const nc1 = { mount: jest.fn(), order: 10 }; + const nc2 = { mount: jest.fn(), order: 0 }; + const nc3 = { mount: jest.fn(), order: 20 }; + navControls.registerExpandedCenter(nc1); + navControls.registerExpandedCenter(nc2); + navControls.registerExpandedCenter(nc3); + expect(await navControls.getExpandedCenter$().pipe(take(1)).toPromise()).toEqual([ + nc2, + nc1, + nc3, + ]); + }); + }); }); diff --git a/src/core/public/chrome/nav_controls/nav_controls_service.ts b/src/core/public/chrome/nav_controls/nav_controls_service.ts index 27ac136c18d5..57298dac39ff 100644 --- a/src/core/public/chrome/nav_controls/nav_controls_service.ts +++ b/src/core/public/chrome/nav_controls/nav_controls_service.ts @@ -78,6 +78,10 @@ export class NavControlsService { const navControlsLeft$ = new BehaviorSubject>(new Set()); const navControlsRight$ = new BehaviorSubject>(new Set()); const navControlsCenter$ = new BehaviorSubject>(new Set()); + const navControlsExpandedRight$ = new BehaviorSubject>(new Set()); + const navControlsExpandedCenter$ = new BehaviorSubject>( + new Set() + ); return { // In the future, registration should be moved to the setup phase. This @@ -91,6 +95,16 @@ export class NavControlsService { registerCenter: (navControl: ChromeNavControl) => navControlsCenter$.next(new Set([...navControlsCenter$.value.values(), navControl])), + registerExpandedRight: (navControl: ChromeNavControl) => + navControlsExpandedRight$.next( + new Set([...navControlsExpandedRight$.value.values(), navControl]) + ), + + registerExpandedCenter: (navControl: ChromeNavControl) => + navControlsExpandedCenter$.next( + new Set([...navControlsExpandedCenter$.value.values(), navControl]) + ), + getLeft$: () => navControlsLeft$.pipe( map((controls) => sortBy([...controls.values()], 'order')), @@ -106,6 +120,16 @@ export class NavControlsService { map((controls) => sortBy([...controls.values()], 'order')), takeUntil(this.stop$) ), + getExpandedRight$: () => + navControlsExpandedRight$.pipe( + map((controls) => sortBy([...controls.values()], 'order')), + takeUntil(this.stop$) + ), + getExpandedCenter$: () => + navControlsExpandedCenter$.pipe( + map((controls) => sortBy([...controls.values()], 'order')), + takeUntil(this.stop$) + ), }; } diff --git a/src/core/public/chrome/public/assets/round_filter.svg b/src/core/public/chrome/public/assets/round_filter.svg new file mode 100644 index 000000000000..84cf36058553 --- /dev/null +++ b/src/core/public/chrome/public/assets/round_filter.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index f911b04fd233..9bdbec781c5e 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -507,116 +507,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiHorizontalRule euiHorizontalRule--full" /> - -
- -
-
- - - -
-
-
-
-
- -
- -
-
- - - -
-
-
-
-
- -
+ + +

+ Recently viewed +

+
+
+ } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + onToggle={[Function]} + paddingSize="none" > -
-
- - - -
-
-
-
-
- - - - -

- Recently viewed -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
-
, + } } - } - href="/" - navLinks$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "", - "href": "", - "id": "opensearchDashboards", - "title": "opensearchDashboards", + className="euiHeaderSectionItemButton" + color="text" + data-test-subj="toggleNavButton" + onClick={[Function]} + > + + + +
+ + +
+ +
+
+ + - - -
- - - - - -
-
-
- - -
- -
- - - -
-
-
-
- -
- -
- -
-
- -
- - + + + + + +
+ +
+
+
+
+
+
+ , + } + } + className="euiHeaderSectionItemButton header__homeLoaderNavButton" + color="text" + data-test-subj="homeLoader" + href="/" + onClick={[Function]} + title="Go to home page" + > + + + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + +
+ +
+
+ +
+ +
+
+ +
+ + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > +
+
+ + + + + + + + + + , + } + } + className="euiHeaderSectionItemButton" + color="text" + onClick={[Function]} + > + + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + +`; + +exports[`Header renders condensed header 1`] = ` +
- - - - - - - -
- -
- -
- - - - - - - - - - , - } - } - className="euiHeaderSectionItemButton" - color="text" - data-test-subj="toggleNavButton" - onClick={[Function]} - > - - - -
-
- -
-
- - - - - - - - - -
- -
- -
- -
- -
- -
- -
- +
+
+ +
+ +
+ +
+ + + + + + + + + + , + } + } + className="euiHeaderSectionItemButton" + color="text" + data-test-subj="toggleNavButton" + onClick={[Function]} + > + + + +
+
+ +
+ +
+
+ +
+ + + + + + +
+ +
+
+
+
+
+
+ , + } + } + className="euiHeaderSectionItemButton header__homeLoaderNavButton" + color="text" + data-test-subj="homeLoader" + href="/" + onClick={[Function]} + title="Go to home page" + > + + + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ - - - - - + + +
+
+ + + + + + + + + + + +
diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap index 5080b23e99c2..2308839a19a9 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -3,7 +3,7 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = ` @@ -15,7 +15,7 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 2`] = Array [ @@ -23,7 +23,7 @@ Array [ , diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap new file mode 100644 index 000000000000..10212cd27561 --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap @@ -0,0 +1,1868 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header logo in dark mode uses custom logo dark mode URL 1`] = ` + + + custom title logo + + +`; + +exports[`Header logo in dark mode uses custom logo default mode URL if no dark mode logo provided 1`] = ` + + + custom title logo + + +`; + +exports[`Header logo in dark mode uses opensearch logo if custom mark provided without logo 1`] = ` + + + custom title logo + + +`; + +exports[`Header logo in dark mode uses opensearch logo if no logo provided 1`] = ` + + + custom title logo + + +`; + +exports[`Header logo in default mode uses custom logo default mode URL 1`] = ` + + + custom title logo + + +`; + +exports[`Header logo in default mode uses opensearch logo if custom mark provided without logo 1`] = ` + + + custom title logo + + +`; + +exports[`Header logo in default mode uses opensearch logo if no branding provided 1`] = ` + + + opensearch dashboards logo + + +`; + +exports[`Header logo in default mode uses opensearch logo if no logo provided 1`] = ` + + + custom title logo + + +`; diff --git a/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap new file mode 100644 index 000000000000..63e83acd78e3 --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap @@ -0,0 +1,1995 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Home button icon in condensed dark mode uses custom mark dark mode URL 1`] = ` + + + + + +`; + +exports[`Home button icon in condensed dark mode uses custom mark default mode URL if no dark mode mark provided 1`] = ` + + + + + +`; + +exports[`Home button icon in condensed dark mode uses opensearch mark if custom logo provided without mark 1`] = ` + + + + + +`; + +exports[`Home button icon in condensed dark mode uses opensearch mark if no mark provided 1`] = ` + + + + + +`; + +exports[`Home button icon in condensed light mode uses custom mark default mode URL 1`] = ` + + + + + +`; + +exports[`Home button icon in condensed light mode uses opensearch mark if custom logo provided without mark 1`] = ` + + + + + +`; + +exports[`Home button icon in condensed light mode uses opensearch mark if no mark provided 1`] = ` + + + + + +`; + +exports[`Home button icon in dark mode uses custom mark dark mode URL 1`] = ` + + + + + +`; + +exports[`Home button icon in dark mode uses custom mark default mode URL if no dark mode mark provided 1`] = ` + + + + + +`; + +exports[`Home button icon in dark mode uses home icon if custom logo provided without mark 1`] = ` + + + + + +`; + +exports[`Home button icon in dark mode uses home icon if no mark provided 1`] = ` + + + + + +`; + +exports[`Home button icon in light mode uses custom mark default mode URL 1`] = ` + + + + + +`; + +exports[`Home button icon in light mode uses home icon if custom logo provided without mark 1`] = ` + + + + + +`; + +exports[`Home button icon in light mode uses home icon if no branding provided 1`] = ` + + + + + +`; + +exports[`Home button icon in light mode uses home icon if no mark provided 1`] = ` + + + + + +`; diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 44cd86427832..b5e93e61c297 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,4 +1,14 @@ -@include euiHeaderAffordForFixed; +:not(.headerIsExpanded) { + @include euiHeaderAffordForFixed($osdHeaderOffset); +} + +.headerIsExpanded { + @include euiHeaderAffordForFixed($osdHeaderOffset * 2); +} + +.headerGlobalNav .euiHeaderSectionItem:empty { + min-width: 0; +} .chrHeaderHelpMenu__version { text-transform: none; diff --git a/src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap b/src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap deleted file mode 100644 index 80b66e95bd41..000000000000 --- a/src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap +++ /dev/null @@ -1,1013 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header logo in dark mode rendered using logo dark mode URL 1`] = ` - -
- custom title logo -
-
-`; - -exports[`Header logo in dark mode rendered using logo default mode URL 1`] = ` - -
- custom title logo -
-
-`; - -exports[`Header logo in dark mode rendered using mark dark mode URL 1`] = ` - -
- custom title logo -
-
-`; - -exports[`Header logo in dark mode rendered using mark default mode URL 1`] = ` - -
- custom title logo -
-
-`; - -exports[`Header logo in dark mode rendered using original opensearch logo 1`] = ` - - custom title logo - -`; - -exports[`Header logo in default mode rendered using logo default mode URL 1`] = ` - -
- custom title logo -
-
-`; - -exports[`Header logo in default mode rendered using mark default mode URL 1`] = ` - -
- custom title logo -
-
-`; - -exports[`Header logo in default mode rendered using the original opensearch logo 1`] = ` - - custom title logo - -`; diff --git a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx b/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx deleted file mode 100644 index f04679aea6c6..000000000000 --- a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { CustomLogo } from './opensearch_dashboards_custom_logo'; - -describe('Header logo ', () => { - describe('in default mode ', () => { - it('rendered using logo default mode URL', () => { - const branding = { - darkMode: false, - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('rendered using mark default mode URL', () => { - const branding = { - darkMode: false, - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('rendered using the original opensearch logo', () => { - const branding = { - darkMode: false, - logo: {}, - mark: {}, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - }); - - describe('in dark mode ', () => { - it('rendered using logo dark mode URL', () => { - const branding = { - darkMode: true, - logo: { defaultUrl: '/defaultModeLogo', darkModeUrl: '/darkModeLogo' }, - mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('rendered using logo default mode URL', () => { - const branding = { - darkMode: true, - logo: { defaultUrl: '/defaultModeLogo' }, - mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('rendered using mark dark mode URL', () => { - const branding = { - darkMode: true, - logo: {}, - mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('rendered using mark default mode URL', () => { - const branding = { - darkMode: true, - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('rendered using original opensearch logo', () => { - const branding = { - darkMode: true, - logo: {}, - mark: {}, - applicationTitle: 'custom title', - }; - const component = mountWithIntl(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx b/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx deleted file mode 100644 index 734b2dd3a1cc..000000000000 --- a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import '../header_logo.scss'; -import { ChromeBranding } from '../../../chrome_service'; - -/** - * Use branding configurations to render the header logo on the nav bar. - * - * @param {ChromeBranding} - branding object consist of logo, mark and title - * @returns Custom branding logo component which is going to be rendered on the main page header bar. - * If logo default is valid, the full logo by logo default config will be rendered; - * if not, the logo icon by mark default config will be rendered; if both are not found, - * the default OpenSearch Dashboards logo will be rendered. - */ -export const CustomLogo = ({ ...branding }: ChromeBranding) => { - const darkMode = branding.darkMode; - const assetFolderUrl = branding.assetFolderUrl; - const logoDefault = branding.logo?.defaultUrl; - const logoDarkMode = branding.logo?.darkModeUrl; - const markDefault = branding.mark?.defaultUrl; - const markDarkMode = branding.mark?.darkModeUrl; - const applicationTitle = branding.applicationTitle; - - /** - * Use branding configurations to check which URL to use for rendering - * header logo in nav bar in default mode - * - * @returns a valid custom URL or undefined if no valid URL is provided - */ - const customHeaderLogoDefaultMode = () => { - return logoDefault ?? markDefault ?? undefined; - }; - - /** - * Use branding configurations to check which URL to use for rendering - * header logo in nav bar in dark mode - * - * @returns a valid custom URL or undefined if no valid URL is provided - */ - const customHeaderLogoDarkMode = () => { - return logoDarkMode ?? logoDefault ?? markDarkMode ?? markDefault ?? undefined; - }; - - /** - * Render custom header logo for both default mode and dark mode - * - * @returns a valid custom header logo URL, or undefined - */ - const customHeaderLogo = () => { - return darkMode ? customHeaderLogoDarkMode() : customHeaderLogoDefaultMode(); - }; - - return customHeaderLogo() ? ( -
- {applicationTitle -
- ) : ( - {applicationTitle - ); -}; diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index f5f879848ed6..51d43d96f7fd 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -217,41 +217,6 @@ export function CollapsibleNav({ )} - {/* Pinned items */} - - - { - if (isModifiedOrPrevented(event)) { - return; - } - - event.preventDefault(); - closeNav(); - navigateToApp('home'); - }, - }, - ]} - maxWidth="none" - color="text" - gutterSize="none" - size="s" - /> - - - {/* Recently viewed */} { }); }); - it('renders', () => { + it('handles visibility and lock changes', () => { const isVisible$ = new BehaviorSubject(false); const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); const isLocked$ = new BehaviorSubject(false); @@ -111,10 +113,21 @@ describe('Header', () => { /> ); expect(component.find('EuiHeader').exists()).toBeFalsy(); + expect(component.find('EuiProgress').exists()).toBeTruthy(); act(() => isVisible$.next(true)); component.update(); - expect(component.find('EuiHeader').exists()).toBeTruthy(); + expect(component.find('EuiHeader.primaryHeader').exists()).toBeTruthy(); + expect(component.find('EuiHeader.expandedHeader').exists()).toBeTruthy(); + expect(component.find('HeaderLogo').exists()).toBeTruthy(); + expect(component.find('HeaderNavControls')).toHaveLength(5); + expect(component.find('[data-test-subj="toggleNavButton"]').exists()).toBeTruthy(); + expect(component.find('HomeLoader').exists()).toBeTruthy(); + expect(component.find('HeaderBreadcrumbs').exists()).toBeTruthy(); + expect(component.find('HeaderBadge').exists()).toBeTruthy(); + expect(component.find('HeaderActionMenu').exists()).toBeTruthy(); + expect(component.find('HeaderHelpMenuUI').exists()).toBeTruthy(); + expect(component.find('EuiFlyout[aria-label="Primary"]').exists()).toBeFalsy(); act(() => isLocked$.next(true)); @@ -122,4 +135,32 @@ describe('Header', () => { expect(component.find('EuiFlyout[aria-label="Primary"]').exists()).toBeTruthy(); expect(component).toMatchSnapshot(); }); + + it('renders condensed header', () => { + const branding = { + darkMode: false, + logo: { defaultUrl: '/foo' }, + mark: { defaultUrl: '/foo' }, + applicationTitle: 'Foobar Dashboards', + useExpandedHeader: false, + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(
); + + expect(component.find('EuiHeader.primaryHeader').exists()).toBeTruthy(); + expect(component.find('EuiHeader.expandedHeader').exists()).toBeFalsy(); + expect(component.find('HeaderLogo').exists()).toBeFalsy(); + expect(component.find('HeaderNavControls')).toHaveLength(3); + expect(component.find('[data-test-subj="toggleNavButton"]').exists()).toBeTruthy(); + expect(component.find('HomeLoader').exists()).toBeTruthy(); + expect(component.find('HeaderBreadcrumbs').exists()).toBeTruthy(); + expect(component.find('HeaderBadge').exists()).toBeTruthy(); + expect(component.find('HeaderActionMenu').exists()).toBeTruthy(); + expect(component.find('HeaderHelpMenuUI').exists()).toBeTruthy(); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 15a78c7350c8..d26105914fde 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -59,9 +59,10 @@ import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; -import { HeaderLogo } from './header_logo'; +import { HomeLoader } from './home_loader'; import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; +import { HeaderLogo } from './header_logo'; export interface HeaderProps { opensearchDashboardsVersion: string; @@ -81,6 +82,8 @@ export interface HeaderProps { navControlsLeft$: Observable; navControlsCenter$: Observable; navControlsRight$: Observable; + navControlsExpandedCenter$: Observable; + navControlsExpandedRight$: Observable; basePath: HttpStart['basePath']; isLocked$: Observable; loadingCount$: ReturnType; @@ -109,57 +112,52 @@ export function Header({ const toggleCollapsibleNavRef = createRef void }>(); const navId = htmlIdGenerator()(); const className = classnames('hide-for-sharing', 'headerGlobalNav'); + const { useExpandedHeader = true, darkMode } = branding; return ( <>
- , - , - ], - borders: 'none', - }, - { - ...(observables.navControlsCenter$ && { + {useExpandedHeader && ( + , + ], + borders: 'none', + }, + { items: [ - + , ], - }), - borders: 'none', - }, - { - items: [ - - - , - , - , - ], - borders: 'none', - }, - ]} - /> - - + borders: 'none', + }, + { + items: [ + + + , + , + ], + borders: 'none', + }, + ]} + /> + )} + + - + + + + + + + - + + + + + + + + + + + + + + +
diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.scss b/src/core/public/chrome/ui/header/header_breadcrumbs.scss new file mode 100644 index 000000000000..5ac333e1615a --- /dev/null +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.scss @@ -0,0 +1,107 @@ +@import "../../../../public/variables"; + +$firstBreadcrumbPolygon: polygon( + 0% 0%, + 100% 0%, + calc(100% - #{$euiSizeS}) 100%, + 0% 100% +); +$breadcrumbPolygon: polygon( + $euiSizeS 0%, + 100% 0%, + calc(100% - #{$euiSizeS}) 100%, + 0% 100% +); + +/* remove background color on autofocus for euiBreadcrumbs in Popover */ +.euiBreadcrumbs__inPopover { + .euiLink.euiLink--text:focus { + background: none; + } +} + +.osdHeaderBreadcrumbs { + /* common for both light & dark theme */ + .osdBreadcrumbs, + &--dark .osdBreadcrumbs { + clip-path: $breadcrumbPolygon; + padding: $euiSizeXS - 2.5 $euiSizeL - $euiSizeXS; + + &:first-child { + clip-path: $firstBreadcrumbPolygon; + } + } + + .osdBreadcrumbs { + background-color: $osdHeaderBreadcrumbGrayBackground; + + &:last-child { + background-color: $osdHeaderBreadcrumbBlueBackground; + } + } + + /* only light mode */ + .euiBreadcrumb__collapsedLink { + color: $osdHeaderBreadcrumbCollapsedLink; + background: $euiColorEmptyShade; + } + + .euiLink.euiLink--subdued:focus { + background: $euiColorEmptyShade; + outline: $euiSizeXS - 1 solid $osdHeaderBreadcrumbBlueBackground; + } + + /* common for dark & light mode */ + &, + &--dark { + /* + filter defines a custom filter effect by grouping atomic filter primitives! + here we are using Gaussian filter with stdDeviation for applying + border-radius on clipped background. + */ + filter: url("../../public/assets/round_filter.svg#round"); + + button { + line-height: inherit; + } + + .euiBreadcrumbSeparator { + display: none; + } + + .euiPopover__anchor { + padding: 0 $euiSizeS; + } + + .euiBreadcrumb:not(.euiBreadcrumb:last-child) { + margin-right: 0; + } + } + + /* only dark mode */ + &--dark { + .osdBreadcrumbs { + background-color: $osdHeaderBreadcrumbMidnightSkyMediumBackground; + color: $osdHeaderBreadcrumbMidnightSkyMediumLightColor; + + &:hover { + color: $osdHeaderBreadcrumbMidnightSkyMediumLightHoverColor; + } + + &:last-child { + background-color: $osdHeaderBreadcrumbPacificSkyDarkestBackground; + color: $euiColorFullShade; + } + } + + .euiBreadcrumb__collapsedLink { + color: $euiColorGhost; + background: $euiColorEmptyShade; + } + + .euiLink.euiLink--subdued:focus { + background: $euiColorEmptyShade; + outline: $euiSizeXS - 1 solid $osdHeaderBreadcrumbPacificSkyDarkestBackground; + } + } +} diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 2008a3f6c493..d95e7881cf5a 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -38,7 +38,11 @@ describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); const wrapper = mount( - + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index ca50b15d5af6..ba81b61e8bcd 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -35,14 +35,18 @@ import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { ChromeBreadcrumb } from '../../chrome_service'; +import './header_breadcrumbs.scss'; + interface Props { appTitle$: Observable; breadcrumbs$: Observable; + isDarkMode?: boolean; } -export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$, isDarkMode }: Props) { const appTitle = useObservable(appTitle$, 'OpenSearch Dashboards'); const breadcrumbs = useObservable(breadcrumbs$, []); + const className = isDarkMode ? 'osdHeaderBreadcrumbs--dark' : 'osdHeaderBreadcrumbs'; let crumbs = breadcrumbs; if (breadcrumbs.length === 0 && appTitle) { @@ -57,7 +61,15 @@ export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { i === 0 && 'first', i === breadcrumbs.length - 1 && 'last' ), + className: classNames('osdBreadcrumbs'), })); - return ; + return ( + + ); } diff --git a/src/core/public/chrome/ui/header/header_logo.test.tsx b/src/core/public/chrome/ui/header/header_logo.test.tsx new file mode 100644 index 000000000000..0ac5a03b3589 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_logo.test.tsx @@ -0,0 +1,169 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { BehaviorSubject } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { HeaderLogo, DEFAULT_DARK_LOGO, DEFAULT_LOGO } from './header_logo'; + +const mockProps = () => ({ + href: '/', + navLinks$: new BehaviorSubject([]), + forceNavigation$: new BehaviorSubject(false), + navigateToApp: jest.fn(), + branding: {}, +}); + +describe('Header logo ', () => { + describe('in default mode ', () => { + it('uses opensearch logo if no branding provided', () => { + const branding = {}; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(`/${DEFAULT_LOGO}`); + expect(img.prop('alt')).toEqual(`opensearch dashboards logo`); + expect(component).toMatchSnapshot(); + }); + + it('uses opensearch logo if no logo provided', () => { + const branding = { + darkMode: false, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_LOGO}`); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + + it('uses opensearch logo if custom mark provided without logo', () => { + const branding = { + darkMode: false, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_LOGO}`); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom logo default mode URL', () => { + const branding = { + darkMode: false, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(branding.logo.defaultUrl); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + }); + + describe('in dark mode ', () => { + it('uses opensearch logo if no logo provided', () => { + const branding = { + darkMode: true, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_DARK_LOGO}`); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + + it('uses opensearch logo if custom mark provided without logo', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_DARK_LOGO}`); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom logo default mode URL if no dark mode logo provided', () => { + const branding = { + darkMode: true, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(branding.logo.defaultUrl); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom logo dark mode URL', () => { + const branding = { + darkMode: true, + logo: { defaultUrl: '/defaultModeLogo', darkModeUrl: '/darkModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const props = { + ...mockProps(), + branding, + }; + const component = mountWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(branding.logo.darkModeUrl); + expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index bbbd173c7bbd..abc32daabdf9 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -28,15 +28,14 @@ * under the License. */ +import './header_logo.scss'; import { i18n } from '@osd/i18n'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; -import { CustomLogo } from './branding/opensearch_dashboards_custom_logo'; import { ChromeBranding } from '../../chrome_service'; -import './header_logo.scss'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -98,6 +97,8 @@ function onClick( } } +export const DEFAULT_DARK_LOGO = 'opensearch_logo_dark_mode.svg'; +export const DEFAULT_LOGO = 'opensearch_logo_default_mode.svg'; interface Props { href: string; navLinks$: Observable; @@ -109,6 +110,20 @@ interface Props { export function HeaderLogo({ href, navigateToApp, branding, ...observables }: Props) { const forceNavigation = useObservable(observables.forceNavigation$, false); const navLinks = useObservable(observables.navLinks$, []); + const { + darkMode, + assetFolderUrl = '', + logo = {}, + applicationTitle = 'opensearch dashboards', + } = branding; + const { defaultUrl: logoUrl, darkModeUrl: darkLogoUrl } = logo; + + const customLogo = darkMode ? darkLogoUrl ?? logoUrl : logoUrl; + const defaultLogo = darkMode ? DEFAULT_DARK_LOGO : DEFAULT_LOGO; + + const logoSrc = customLogo ? customLogo : `${assetFolderUrl}/${defaultLogo}`; + const testSubj = customLogo ? 'customLogo' : 'defaultLogo'; + const alt = `${applicationTitle} logo`; return ( - + {alt} ); } diff --git a/src/core/public/chrome/ui/header/home_icon.test.tsx b/src/core/public/chrome/ui/header/home_icon.test.tsx new file mode 100644 index 000000000000..d1ede9e2c5a3 --- /dev/null +++ b/src/core/public/chrome/ui/header/home_icon.test.tsx @@ -0,0 +1,255 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DEFAULT_MARK, DEFAULT_DARK_MARK, HomeIcon } from './home_icon'; + +// TODO: many of these tests cover the conditional logic of which mark to load depending on raw branding configurations +// Dark mode and custom/default fallbacks should instead be consolidated and centralized elsewhere: +// https://github.com/opensearch-project/OpenSearch-Dashboards/issues/895#issuecomment-1164995007 +describe('Home button icon ', () => { + describe('in condensed light mode ', () => { + it('uses opensearch mark if no mark provided', () => { + const branding = { + darkMode: false, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_MARK}`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses opensearch mark if custom logo provided without mark', () => { + const branding = { + darkMode: false, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_MARK}`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom mark default mode URL', () => { + const branding = { + darkMode: false, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + }); + + describe('in condensed dark mode ', () => { + it('uses opensearch mark if no mark provided', () => { + const branding = { + darkMode: true, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_DARK_MARK}`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses opensearch mark if custom logo provided without mark', () => { + const branding = { + darkMode: true, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_DARK_MARK}`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom mark default mode URL if no dark mode mark provided', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom mark dark mode URL', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + useExpandedHeader: false, + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(branding.mark.darkModeUrl); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + }); + + describe('in light mode ', () => { + it('uses home icon if no branding provided', () => { + const branding = {}; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual(`m`); + expect(icon.prop('title')).toEqual(`opensearch dashboards home`); + expect(component).toMatchSnapshot(); + }); + + it('uses home icon if no mark provided', () => { + const branding = { + darkMode: false, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual(`m`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses home icon if custom logo provided without mark', () => { + const branding = { + darkMode: false, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual(`m`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom mark default mode URL', () => { + const branding = { + darkMode: false, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); + expect(icon.prop('size')).toEqual(`l`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + }); + + describe('in dark mode ', () => { + it('uses home icon if no mark provided', () => { + const branding = { + darkMode: true, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual(`m`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses home icon if custom logo provided without mark', () => { + const branding = { + darkMode: true, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual(`m`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom mark default mode URL if no dark mode mark provided', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); + expect(icon.prop('size')).toEqual(`l`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + + it('uses custom mark dark mode URL', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, + applicationTitle: 'custom title', + assetFolderUrl: 'base/ui/default_branding', + }; + const component = mountWithIntl(); + const icon = component.find('EuiIcon'); + expect(icon.prop('type')).toEqual(branding.mark.darkModeUrl); + expect(icon.prop('size')).toEqual(`l`); + expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/public/chrome/ui/header/home_icon.tsx b/src/core/public/chrome/ui/header/home_icon.tsx new file mode 100644 index 000000000000..9260fc19ccae --- /dev/null +++ b/src/core/public/chrome/ui/header/home_icon.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiIcon } from '@elastic/eui'; +import { ChromeBranding } from '../../chrome_service'; + +export const DEFAULT_MARK = 'opensearch_mark_default_mode.svg'; +export const DEFAULT_DARK_MARK = 'opensearch_mark_dark_mode.svg'; + +/** + * Use branding configurations to render the header mark on the nav bar. + * + * @param {ChromeBranding} - branding object consist of mark, darkmode selection, asset path and title + * @returns Mark component which is going to be rendered on the main page header bar. + */ +export const HomeIcon = ({ + darkMode, + assetFolderUrl = '', + mark, + applicationTitle = 'opensearch dashboards', + useExpandedHeader = true, +}: ChromeBranding) => { + const { defaultUrl: markUrl, darkModeUrl: darkMarkUrl } = mark ?? {}; + + const customMark = darkMode ? darkMarkUrl ?? markUrl : markUrl; + const defaultMark = darkMode ? DEFAULT_DARK_MARK : DEFAULT_MARK; + + const getIconProps = () => { + const iconType = customMark + ? customMark + : useExpandedHeader + ? 'home' + : `${assetFolderUrl}/${defaultMark}`; + const testSubj = customMark ? 'customMark' : useExpandedHeader ? 'homeIcon' : 'defaultMark'; + const title = `${applicationTitle} home`; + // marks look better at the large size, but the home icon should be medium to fit in with other icons + const size = iconType === 'home' ? ('m' as const) : ('l' as const); + + return { + 'data-test-subj': testSubj, + 'data-test-image-url': iconType, + type: iconType, + title, + size, + }; + }; + + const props = getIconProps(); + + return ; +}; diff --git a/src/core/public/chrome/ui/header/home_loader.scss b/src/core/public/chrome/ui/header/home_loader.scss new file mode 100644 index 000000000000..78d5345a88f2 --- /dev/null +++ b/src/core/public/chrome/ui/header/home_loader.scss @@ -0,0 +1,14 @@ +.header__homeLoaderNavButton .euiHeaderSectionItemButton__content { + // avoid layout shift on smallest breakpoint if logo and loading indicator are different sizes + min-width: 24px; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + + & .loaderContainer, + & .homeIconContainer { + grid-area: 1 / 1; + align-self: center; + justify-self: center; + } +} diff --git a/src/core/public/chrome/ui/header/home_loader.tsx b/src/core/public/chrome/ui/header/home_loader.tsx new file mode 100644 index 000000000000..4cd231bdf83e --- /dev/null +++ b/src/core/public/chrome/ui/header/home_loader.tsx @@ -0,0 +1,142 @@ +/* + * 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 './home_loader.scss'; + +import { i18n } from '@osd/i18n'; +import React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { Observable } from 'rxjs'; +import Url from 'url'; +import { EuiHeaderSectionItemButton } from '@elastic/eui'; +import { ChromeNavLink } from '../..'; +import { ChromeBranding } from '../../chrome_service'; +import { LoadingIndicator } from '../loading_indicator'; +import { HomeIcon } from './home_icon'; + +function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { + let current = element; + while (current) { + if (current.tagName === 'A') { + return current as HTMLAnchorElement; + } + + if (!current.parentElement || current.parentElement === document.body) { + return undefined; + } + + current = current.parentElement; + } +} + +function onClick( + event: React.MouseEvent, + forceNavigation: boolean, + navLinks: ChromeNavLink[], + navigateToApp: (appId: string) => void +) { + const anchor = findClosestAnchor((event as any).nativeEvent.target); + if (!anchor) { + return; + } + + const navLink = navLinks.find((item) => item.href === anchor.href); + if (navLink && navLink.disabled) { + event.preventDefault(); + return; + } + + if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) { + return; + } + + if (forceNavigation) { + const toParsed = Url.parse(anchor.href); + const fromParsed = Url.parse(document.location.href); + const sameProto = toParsed.protocol === fromParsed.protocol; + const sameHost = toParsed.host === fromParsed.host; + const samePath = toParsed.path === fromParsed.path; + + if (sameProto && sameHost && samePath) { + if (toParsed.hash) { + document.location.reload(); + } + + // event.preventDefault() keeps the browser from seeing the new url as an update + // and even setting window.location does not mimic that behavior, so instead + // we use stopPropagation() to prevent angular from seeing the click and + // starting a digest cycle/attempting to handle it in the router. + event.stopPropagation(); + } + } else { + navigateToApp('home'); + event.preventDefault(); + } +} + +interface Props { + href: string; + navLinks$: Observable; + forceNavigation$: Observable; + loadingCount$: Observable; + navigateToApp: (appId: string) => void; + branding: ChromeBranding; +} + +export function HomeLoader({ href, navigateToApp, branding, ...observables }: Props) { + const forceNavigation = useObservable(observables.forceNavigation$, false); + const navLinks = useObservable(observables.navLinks$, []); + const loadingCount = useObservable(observables.loadingCount$, 0); + const label = i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { + defaultMessage: 'Go to home page', + }); + + return ( + ) => + onClick(e, forceNavigation, navLinks, navigateToApp) + } + href={href} + aria-label={label} + title={label} + > + {!(loadingCount > 0) && ( +
+ +
+ )} +
+ +
+
+ ); +} diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index fe6162f30e52..8fe5a36ebb55 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -183,7 +183,7 @@ export class CoreSystem { return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { - if (this.fatalErrorsSetup) { + if (this.fatalErrorsSetup && (typeof error === 'string' || error instanceof Error)) { this.fatalErrorsSetup.add(error); } else { // If the FatalErrorsService has not yet been setup, log error to console @@ -260,9 +260,14 @@ export class CoreSystem { await this.plugins.start(core); + const { useExpandedHeader = true } = injectedMetadata.getBranding() ?? {}; + // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; this.rootDomElement.classList.add('coreSystemRootDomElement'); + if (useExpandedHeader) { + this.rootDomElement.classList.add('headerIsExpanded'); + } this.rootDomElement.appendChild(coreUiTargetDomElement); this.rootDomElement.appendChild(notificationsTargetDomElement); this.rootDomElement.appendChild(overlayTargetDomElement); @@ -278,7 +283,7 @@ export class CoreSystem { application, }; } catch (error) { - if (this.fatalErrorsSetup) { + if (this.fatalErrorsSetup && (typeof error === 'string' || error instanceof Error)) { this.fatalErrorsSetup.add(error); } else { // If the FatalErrorsService has not yet been setup, log error to console diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 1ac489c8f129..e59008f08259 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,5 +1,3 @@ -@include euiHeaderAffordForFixed($osdHeaderOffset); - /** * stretch the root element of the OpenSearch Dashboards application to set the base-size that * flexed children should keep. Only works when paired with root styles applied @@ -17,6 +15,10 @@ margin: 0 auto; min-height: calc(100vh - #{$osdHeaderOffset}); + .headerIsExpanded & { + min-height: calc(100vh - #{$osdHeaderOffset * 2}); + } + &.hidden-chrome { min-height: 100vh; } diff --git a/src/core/server/core_app/assets/default_branding/opensearch_logo.svg b/src/core/server/core_app/assets/default_branding/opensearch_logo_dark_mode.svg similarity index 100% rename from src/core/server/core_app/assets/default_branding/opensearch_logo.svg rename to src/core/server/core_app/assets/default_branding/opensearch_logo_dark_mode.svg diff --git a/src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg b/src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg new file mode 100644 index 000000000000..9ee816341523 --- /dev/null +++ b/src/core/server/core_app/assets/default_branding/opensearch_logo_default_mode.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/core/server/core_app/assets/legacy_dark_theme.css b/src/core/server/core_app/assets/legacy_dark_theme.css index b79f3835acf1..4ef4c726e414 100644 --- a/src/core/server/core_app/assets/legacy_dark_theme.css +++ b/src/core/server/core_app/assets/legacy_dark_theme.css @@ -12,7 +12,7 @@ /*! * Bootstrap v3.3.6 (http://getbootstrap.com) * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ /* @notice * This product bundles bootstrap@3.3.6 which is available under a diff --git a/src/core/server/core_app/assets/legacy_light_theme.css b/src/core/server/core_app/assets/legacy_light_theme.css index 27252f6548fe..9f9a0dc118d1 100644 --- a/src/core/server/core_app/assets/legacy_light_theme.css +++ b/src/core/server/core_app/assets/legacy_light_theme.css @@ -12,7 +12,7 @@ /*! * Bootstrap v3.3.6 (http://getbootstrap.com) * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ /* @notice * This product bundles bootstrap@3.3.6 which is available under a diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index becea6f17c80..5e1bcbe7867b 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -39,7 +39,7 @@ const createStartContractMock = () => { new BehaviorSubject({ config: { opensearch: { - apiVersion: 'master', + apiVersion: 'main', customHeadersConfigured: false, healthCheckDelayMs: 2500, logQueries: false, diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 38d9e755ef30..e3a1b066db23 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -41,7 +41,7 @@ const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; // before update to make sure it's in sync with validation rules in Legacy -// https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/src/legacy/server/config/schema.js +// https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/legacy/server/config/schema.js export const config = { path: 'server', schema: schema.object( diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 3663d4e432a2..4db4c4fac17f 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -869,7 +869,7 @@ describe('conditional compression', () => { expect(response.header).toHaveProperty('content-encoding', 'gzip'); }); - test('enables compression for whitelisted referer', async () => { + test('enables compression for whitelisted (deprecated) or allowlisted referer', async () => { const response = await supertest(listener) .get('/') .set('accept-encoding', 'gzip') @@ -878,7 +878,7 @@ describe('conditional compression', () => { expect(response.header).toHaveProperty('content-encoding', 'gzip'); }); - test('disables compression for non-whitelisted referer', async () => { + test('disables compression for non-whitelisted (deprecated) or non-allowlisted referer', async () => { const response = await supertest(listener) .get('/') .set('accept-encoding', 'gzip') diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index fa6842a9f064..8627557c7332 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -164,7 +164,7 @@ export class HttpService * Indicates if http server has configured to start listening on a configured port. * We shouldn't start http service in two cases: * 1. If `server.autoListen` is explicitly set to `false`. - * 2. When the process is run as dev cluster master in which case cluster manager + * 2. When the process is run as dev cluster manager. * will fork a dedicated process where http service will be set up instead. * @internal * */ diff --git a/src/core/server/opensearch/legacy/api_types.ts b/src/core/server/opensearch/legacy/api_types.ts index 9fbbe6dd3632..6f2f5f92ab84 100644 --- a/src/core/server/opensearch/legacy/api_types.ts +++ b/src/core/server/opensearch/legacy/api_types.ts @@ -232,6 +232,7 @@ export interface LegacyAPICaller { (endpoint: 'cat.help', params: CatHelpParams, options?: LegacyCallAPIOptions): ReturnType; (endpoint: 'cat.indices', params: CatIndicesParams, options?: LegacyCallAPIOptions): ReturnType; (endpoint: 'cat.master', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; + (endpoint: 'cat.cluster_manager', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; (endpoint: 'cat.nodes', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; diff --git a/src/core/server/opensearch/legacy/cluster_client.test.ts b/src/core/server/opensearch/legacy/cluster_client.test.ts index 8d91a8e22ed1..c7820952b451 100644 --- a/src/core/server/opensearch/legacy/cluster_client.test.ts +++ b/src/core/server/opensearch/legacy/cluster_client.test.ts @@ -48,7 +48,7 @@ const logger = loggingSystemMock.create(); afterEach(() => jest.clearAllMocks()); test('#constructor creates client with parsed config', () => { - const mockOpenSearchClientConfig = { apiVersion: 'opensearch-client-master' }; + const mockOpenSearchClientConfig = { apiVersion: 'opensearch-client-main' }; mockParseOpenSearchClientConfig.mockReturnValue(mockOpenSearchClientConfig); const mockOpenSearchConfig = { apiVersion: 'opensearch-version' } as any; diff --git a/src/core/server/opensearch/legacy/opensearch_client_config.test.ts b/src/core/server/opensearch/legacy/opensearch_client_config.test.ts index e7ccccb286ac..4b4451c8ed41 100644 --- a/src/core/server/opensearch/legacy/opensearch_client_config.test.ts +++ b/src/core/server/opensearch/legacy/opensearch_client_config.test.ts @@ -42,7 +42,7 @@ test('parses minimally specified config', () => { expect( parseOpenSearchClientConfig( { - apiVersion: 'master', + apiVersion: 'main', customHeaders: { xsrf: 'something' }, logQueries: false, sniffOnStart: false, @@ -50,11 +50,12 @@ test('parses minimally specified config', () => { hosts: ['http://localhost/opensearch'], requestHeadersWhitelist: [], }, + logger.get() ) ).toMatchInlineSnapshot(` Object { - "apiVersion": "master", + "apiVersion": "main", "hosts": Array [ Object { "headers": Object { @@ -180,7 +181,7 @@ test('parses config timeouts of moment.Duration type', () => { expect( parseOpenSearchClientConfig( { - apiVersion: 'master', + apiVersion: 'main', customHeaders: { xsrf: 'something' }, logQueries: false, sniffOnStart: false, @@ -191,11 +192,12 @@ test('parses config timeouts of moment.Duration type', () => { hosts: ['http://localhost:9200/opensearch'], requestHeadersWhitelist: [], }, + logger.get() ) ).toMatchInlineSnapshot(` Object { - "apiVersion": "master", + "apiVersion": "main", "hosts": Array [ Object { "headers": Object { @@ -359,7 +361,7 @@ describe('#customHeaders', () => { const headerKey = Object.keys(DEFAULT_HEADERS)[0]; const parsedConfig = parseOpenSearchClientConfig( { - apiVersion: 'master', + apiVersion: 'main', customHeaders: { [headerKey]: 'foo' }, logQueries: false, sniffOnStart: false, @@ -379,7 +381,7 @@ describe('#log', () => { test('default logger with #logQueries = false', () => { const parsedConfig = parseOpenSearchClientConfig( { - apiVersion: 'master', + apiVersion: 'main', customHeaders: { xsrf: 'something' }, logQueries: false, sniffOnStart: false, @@ -423,7 +425,7 @@ describe('#log', () => { test('default logger with #logQueries = true', () => { const parsedConfig = parseOpenSearchClientConfig( { - apiVersion: 'master', + apiVersion: 'main', customHeaders: { xsrf: 'something' }, logQueries: true, sniffOnStart: false, @@ -482,7 +484,7 @@ describe('#log', () => { const parsedConfig = parseOpenSearchClientConfig( { - apiVersion: 'master', + apiVersion: 'main', customHeaders: { xsrf: 'something' }, logQueries: true, sniffOnStart: false, diff --git a/src/core/server/opensearch/opensearch_config.test.ts b/src/core/server/opensearch/opensearch_config.test.ts index 51529829dd9f..25cbee718d00 100644 --- a/src/core/server/opensearch/opensearch_config.test.ts +++ b/src/core/server/opensearch/opensearch_config.test.ts @@ -82,7 +82,7 @@ test('set correct defaults', () => { "enabled": false, "maxPercentage": 1, }, - "optimizedHealthcheckId": undefined, + "optimizedHealthcheck": undefined, "password": undefined, "pingTimeout": "PT30S", "requestHeadersWhitelist": Array [ @@ -491,11 +491,20 @@ describe('deprecations', () => { `); }); - it('logs a warning if elasticsearch.optimizedHealthcheckId is set and opensearch.optimizedHealthcheckId is not', () => { + it('logs a warning if elasticsearch.optimizedHealthcheckId is set and opensearch.optimizedHealthcheck.id is not', () => { const { messages } = applyLegacyDeprecations({ optimizedHealthcheckId: '' }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"elasticsearch.optimizedHealthcheckId\\" is deprecated and has been replaced by \\"opensearch.optimizedHealthcheckId\\"", + "\\"elasticsearch.optimizedHealthcheckId\\" is deprecated and has been replaced by \\"opensearch.optimizedHealthcheck.id\\"", + ] + `); + }); + + it('logs a warning if opensearch.optimizedHealthcheckId is set and opensearch.optimizedHealthcheck.id is not', () => { + const { messages } = applyOpenSearchDeprecations({ optimizedHealthcheckId: '' }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"opensearch.optimizedHealthcheckId\\" is deprecated and has been replaced by \\"opensearch.optimizedHealthcheck.id\\"", ] `); }); diff --git a/src/core/server/opensearch/opensearch_config.ts b/src/core/server/opensearch/opensearch_config.ts index bbb26dba29cf..9b7bdff21cdc 100644 --- a/src/core/server/opensearch/opensearch_config.ts +++ b/src/core/server/opensearch/opensearch_config.ts @@ -86,7 +86,14 @@ export const configSchema = schema.object({ requestTimeout: schema.duration({ defaultValue: '30s' }), pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), logQueries: schema.boolean({ defaultValue: false }), - optimizedHealthcheckId: schema.maybe(schema.string()), + optimizedHealthcheck: schema.maybe( + schema.object({ + id: schema.string(), + filters: schema.maybe( + schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }) + ), + }) + ), ssl: schema.object( { verificationMode: schema.oneOf( @@ -149,12 +156,17 @@ const deprecations: ConfigDeprecationProvider = ({ renameFromRoot, renameFromRoo 'opensearch.requestHeadersWhitelist', 'opensearch.requestHeadersAllowlist' ), + renameFromRootWithoutMap( + 'opensearch.requestHeadersWhitelistConfigured', + 'opensearch.requestHeadersAllowlistConfigured' + ), renameFromRoot('elasticsearch.customHeaders', 'opensearch.customHeaders'), renameFromRoot('elasticsearch.shardTimeout', 'opensearch.shardTimeout'), renameFromRoot('elasticsearch.requestTimeout', 'opensearch.requestTimeout'), renameFromRoot('elasticsearch.pingTimeout', 'opensearch.pingTimeout'), renameFromRoot('elasticsearch.logQueries', 'opensearch.logQueries'), - renameFromRoot('elasticsearch.optimizedHealthcheckId', 'opensearch.optimizedHealthcheckId'), + renameFromRoot('elasticsearch.optimizedHealthcheckId', 'opensearch.optimizedHealthcheck.id'), + renameFromRoot('opensearch.optimizedHealthcheckId', 'opensearch.optimizedHealthcheck.id'), renameFromRoot('elasticsearch.ssl', 'opensearch.ssl'), renameFromRoot('elasticsearch.apiVersion', 'opensearch.apiVersion'), renameFromRoot('elasticsearch.healthCheck', 'opensearch.healthCheck'), @@ -208,7 +220,7 @@ export class OpenSearchConfig { public readonly ignoreVersionMismatch: boolean; /** - * Version of the OpenSearch (6.7, 7.1 or `master`) client will be connecting to. + * Version of the OpenSearch (1.1, 2.1 or `main`) client will be connecting to. */ public readonly apiVersion: string; @@ -222,7 +234,7 @@ export class OpenSearchConfig { * Specifies whether Dashboards should only query the local OpenSearch node when * all nodes in the cluster have the same node attribute value */ - public readonly optimizedHealthcheckId?: string; + public readonly optimizedHealthcheck?: OpenSearchConfigType['optimizedHealthcheck']; /** * Hosts that the client will connect to. If sniffing is enabled, this list will @@ -310,7 +322,7 @@ export class OpenSearchConfig { this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch; this.apiVersion = rawConfig.apiVersion; this.logQueries = rawConfig.logQueries; - this.optimizedHealthcheckId = rawConfig.optimizedHealthcheckId; + this.optimizedHealthcheck = rawConfig.optimizedHealthcheck; this.hosts = Array.isArray(rawConfig.hosts) ? rawConfig.hosts : [rawConfig.hosts]; this.requestHeadersWhitelist = Array.isArray(rawConfig.requestHeadersWhitelist) ? rawConfig.requestHeadersWhitelist diff --git a/src/core/server/opensearch/opensearch_service.ts b/src/core/server/opensearch/opensearch_service.ts index e7098588a73d..87a2da974ba1 100644 --- a/src/core/server/opensearch/opensearch_service.ts +++ b/src/core/server/opensearch/opensearch_service.ts @@ -93,7 +93,7 @@ export class OpenSearchService const opensearchNodesCompatibility$ = pollOpenSearchNodesVersion({ internalClient: this.client.asInternalUser, - optimizedHealthcheckId: config.optimizedHealthcheckId, + optimizedHealthcheck: config.optimizedHealthcheck, log: this.log, ignoreVersionMismatch: config.ignoreVersionMismatch, opensearchVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), diff --git a/src/core/server/opensearch/version_check/ensure_opensearch_version.test.ts b/src/core/server/opensearch/version_check/ensure_opensearch_version.test.ts index b29778b6b0a2..709e5c4309f2 100644 --- a/src/core/server/opensearch/version_check/ensure_opensearch_version.test.ts +++ b/src/core/server/opensearch/version_check/ensure_opensearch_version.test.ts @@ -66,6 +66,35 @@ function createNodes(...versions: string[]): NodesInfo { return { nodes }; } +function createNodesWithAttribute( + targetId: string, + filterId: string, + targetAttributeValue: string, + filterAttributeValue: string, + ...versions: string[] +): NodesInfo { + const nodes = {} as any; + versions + .map((version, i) => { + return { + version, + http: { + publish_address: 'http_address', + }, + ip: 'ip', + attributes: { + cluster_id: i % 2 === 0 ? targetId : filterId, + custom_attribute: i % 2 === 0 ? targetAttributeValue : filterAttributeValue, + }, + }; + }) + .forEach((node, i) => { + nodes[`node-${i}`] = node; + }); + + return { nodes }; +} + describe('mapNodesVersionCompatibility', () => { function createNodesInfoWithoutHTTP(version: string): NodesInfo { return { nodes: { 'node-without-http': { version, ip: 'ip' } } } as any; @@ -179,7 +208,7 @@ describe('pollOpenSearchNodesVersion', () => { pollOpenSearchNodesVersion({ internalClient, - optimizedHealthcheckId: 'cluster_id', + optimizedHealthcheck: { id: 'cluster_id' }, opensearchVersionCheckInterval: 1, ignoreVersionMismatch: false, opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, @@ -203,7 +232,7 @@ describe('pollOpenSearchNodesVersion', () => { pollOpenSearchNodesVersion({ internalClient, - optimizedHealthcheckId: 'cluster_id', + optimizedHealthcheck: { id: 'cluster_id' }, opensearchVersionCheckInterval: 1, ignoreVersionMismatch: false, opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, @@ -221,6 +250,112 @@ describe('pollOpenSearchNodesVersion', () => { }); }); + it('returns compatibility results and isCompatible=true with filters', (done) => { + expect.assertions(2); + const target = { + cluster_id: '0', + attribute: 'foo', + }; + const filter = { + cluster_id: '1', + attribute: 'bar', + }; + + // will filter out every odd index + const nodes = createNodesWithAttribute( + target.cluster_id, + filter.cluster_id, + target.attribute, + filter.attribute, + '5.1.0', + '6.2.0', + '5.1.0', + '5.1.1-Beta1' + ); + + const filteredNodes = nodes; + delete filteredNodes.nodes['node-1']; + delete filteredNodes.nodes['node-3']; + + // @ts-expect-error we need to return an incompatible type to use the testScheduler here + internalClient.cluster.state.mockReturnValueOnce({ body: nodes }); + + nodeInfosSuccessOnce(nodes); + + pollOpenSearchNodesVersion({ + internalClient, + optimizedHealthcheck: { id: 'cluster_id', filters: { custom_attribute: filter.attribute } }, + opensearchVersionCheckInterval: 1, + ignoreVersionMismatch: false, + opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: (result) => { + expect(result).toEqual( + mapNodesVersionCompatibility(filteredNodes, OPENSEARCH_DASHBOARDS_VERSION, false) + ); + expect(result.isCompatible).toBe(true); + }, + complete: done, + error: done, + }); + }); + + it('returns compatibility results and isCompatible=false with filters', (done) => { + expect.assertions(2); + const target = { + cluster_id: '0', + attribute: 'foo', + }; + const filter = { + cluster_id: '1', + attribute: 'bar', + }; + + // will filter out every odd index + const nodes = createNodesWithAttribute( + target.cluster_id, + filter.cluster_id, + target.attribute, + filter.attribute, + '5.1.0', + '5.1.0', + '6.2.0', + '5.1.1-Beta1' + ); + + const filteredNodes = nodes; + delete filteredNodes.nodes['node-1']; + delete filteredNodes.nodes['node-3']; + + // @ts-expect-error we need to return an incompatible type to use the testScheduler here + internalClient.cluster.state.mockReturnValueOnce({ body: nodes }); + + nodeInfosSuccessOnce(nodes); + + pollOpenSearchNodesVersion({ + internalClient, + optimizedHealthcheck: { id: 'cluster_id', filters: { custom_attribute: filter.attribute } }, + opensearchVersionCheckInterval: 1, + ignoreVersionMismatch: false, + opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: (result) => { + expect(result).toEqual( + mapNodesVersionCompatibility(filteredNodes, OPENSEARCH_DASHBOARDS_VERSION, false) + ); + expect(result.isCompatible).toBe(false); + }, + complete: done, + error: done, + }); + }); + it('only emits if the node versions changed since the previous poll', (done) => { expect.assertions(4); nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit @@ -232,7 +367,7 @@ describe('pollOpenSearchNodesVersion', () => { pollOpenSearchNodesVersion({ internalClient, - optimizedHealthcheckId: 'cluster_id', + optimizedHealthcheck: { id: 'cluster_id' }, opensearchVersionCheckInterval: 1, ignoreVersionMismatch: false, opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, @@ -268,7 +403,7 @@ describe('pollOpenSearchNodesVersion', () => { const opensearchNodesCompatibility$ = pollOpenSearchNodesVersion({ internalClient, - optimizedHealthcheckId: 'cluster_id', + optimizedHealthcheck: { id: 'cluster_id' }, opensearchVersionCheckInterval: 100, ignoreVersionMismatch: false, opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, @@ -308,7 +443,7 @@ describe('pollOpenSearchNodesVersion', () => { const opensearchNodesCompatibility$ = pollOpenSearchNodesVersion({ internalClient, - optimizedHealthcheckId: 'cluster_id', + optimizedHealthcheck: { id: 'cluster_id' }, opensearchVersionCheckInterval: 10, ignoreVersionMismatch: false, opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION, diff --git a/src/core/server/opensearch/version_check/ensure_opensearch_version.ts b/src/core/server/opensearch/version_check/ensure_opensearch_version.ts index 1eaffa9fc9cb..fc35818e2929 100644 --- a/src/core/server/opensearch/version_check/ensure_opensearch_version.ts +++ b/src/core/server/opensearch/version_check/ensure_opensearch_version.ts @@ -35,7 +35,6 @@ import { timer, of, from, Observable } from 'rxjs'; import { map, distinctUntilChanged, catchError, exhaustMap, mergeMap } from 'rxjs/operators'; -import { get } from 'lodash'; import { ApiResponse } from '@opensearch-project/opensearch'; import { opensearchVersionCompatibleWithOpenSearchDashboards, @@ -46,40 +45,69 @@ import type { OpenSearchClient } from '../client'; /** * Checks if all nodes in the cluster have the same cluster id node attribute - * that is supplied through the healthcheckAttributeName param. This node attribute is configurable - * in opensearch_dashboards.yml. + * that is supplied through the healthcheck param. This node attribute is configurable + * in opensearch_dashboards.yml. It can also filter attributes out by key-value pair. * If all nodes have the same cluster id then we do not fan out the healthcheck and use '_local' node - * If there are multiple cluster ids then we use the default fan out behavior + * If there are multiple cluster ids then we return an array of node ids to check. * If the supplied node attribute is missing then we return null and use default fan out behavior * @param {OpenSearchClient} internalClient - * @param {string} healthcheckAttributeName - * @returns {string|null} '_local' if all nodes have the same cluster_id, otherwise null + * @param {OptimizedHealthcheck} healthcheck + * @returns {string|string[]|null} '_local' if all nodes have the same cluster_id, array of node ids if different cluster_id, null if no cluster_id or nodes returned */ export const getNodeId = async ( internalClient: OpenSearchClient, - healthcheckAttributeName: string -): Promise => { + healthcheck: OptimizedHealthcheck +): Promise<'_local' | string[] | null> => { try { + // If missing an id, we have nothing to check + if (!healthcheck.id) return null; + + let path = `nodes.*.attributes.${healthcheck.id}`; + const filters = healthcheck.filters; + const filterKeys = filters ? Object.keys(filters) : []; + + for (const key of filterKeys) { + path += `,nodes.*.attributes.${key}`; + } + + /* + * Using _cluster/state/nodes to retrieve the cluster_id of each node from cluster manager node which + * is considered to be a lightweight operation to aggegrate different cluster_ids from the OpenSearch nodes. + */ const state = (await internalClient.cluster.state({ metric: 'nodes', - filter_path: [`nodes.*.attributes.${healthcheckAttributeName}`], + filter_path: [path], })) as ApiResponse; - /* Aggregate different cluster_ids from the OpenSearch nodes - * if all the nodes have the same cluster_id, retrieve nodes.info from _local node only - * Using _cluster/state/nodes to retrieve the cluster_id of each node from master node which is considered to be a lightweight operation - * else if the nodes have different cluster_ids then fan out the request to all nodes - * else there are no nodes in the cluster + + const nodes = state.body.nodes; + const nodeIds = new Set(Object.keys(nodes)); + + /* + * If filters are set look for the key and value and filter out any node that matches + * the value for that attribute. */ - const sharedClusterId = - state.body.nodes.length > 0 - ? get(state.body.nodes[0], `attributes.${healthcheckAttributeName}`, null) - : null; - return sharedClusterId === null || - state.body.nodes.find( - (node: any) => sharedClusterId !== get(node, `attributes.${healthcheckAttributeName}`, null) - ) - ? null - : '_local'; + for (const id of nodeIds) { + for (const key of filterKeys) { + const attributeValue = nodes[id].attributes?.[key] ?? null; + + if (attributeValue === filters![key]) nodeIds.delete(id); + } + } + + if (nodeIds.size === 0) return null; + + const [firstNodeId] = nodeIds; + const sharedClusterId = nodes[firstNodeId].attributes?.[healthcheck.id] ?? null; + // If cluster_id is not set then fan out + if (sharedClusterId === null) return null; + + // If a node is found to have a different cluster_id, return node ids + for (const id of nodeIds) { + if (nodes[id].attributes?.[healthcheck.id] !== sharedClusterId) return Array.from(nodeIds); + } + + // When all nodes share the same cluster_id, return _local + return '_local'; } catch (e) { return null; } @@ -87,7 +115,7 @@ export const getNodeId = async ( export interface PollOpenSearchNodesVersionOptions { internalClient: OpenSearchClient; - optimizedHealthcheckId?: string; + optimizedHealthcheck?: OptimizedHealthcheck; log: Logger; opensearchDashboardsVersion: string; ignoreVersionMismatch: boolean; @@ -118,6 +146,13 @@ export interface NodesVersionCompatibility { nodesInfoRequestError?: Error; } +export interface OptimizedHealthcheck { + id?: string; + filters?: { + [key: string]: string; + }; +} + function getHumanizedNodeName(node: NodeInfo) { const publishAddress = node?.http?.publish_address + ' ' || ''; return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; @@ -201,7 +236,7 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati export const pollOpenSearchNodesVersion = ({ internalClient, - optimizedHealthcheckId, + optimizedHealthcheck, log, opensearchDashboardsVersion, ignoreVersionMismatch, @@ -214,10 +249,10 @@ export const pollOpenSearchNodesVersion = ({ * Originally, Dashboards queries OpenSearch cluster to get the version info of each node and check the version compatibility with each node. * The /nodes request could fail even one node in cluster fail to response * For better dashboards resilience, the behaviour is changed to only query the local node when all the nodes have the same cluster_id - * Using _cluster/state/nodes to retrieve the cluster_id of each node from the master node + * Using _cluster/state/nodes to retrieve the cluster_id of each node from the cluster manager node */ - if (optimizedHealthcheckId) { - return from(getNodeId(internalClient, optimizedHealthcheckId)).pipe( + if (optimizedHealthcheck) { + return from(getNodeId(internalClient, optimizedHealthcheck)).pipe( mergeMap((nodeId: any) => from( internalClient.nodes.info({ diff --git a/src/core/server/opensearch_dashboards_config.ts b/src/core/server/opensearch_dashboards_config.ts index ad88e8a89cad..f2c6bcdf31c5 100644 --- a/src/core/server/opensearch_dashboards_config.ts +++ b/src/core/server/opensearch_dashboards_config.ts @@ -81,6 +81,9 @@ export const config = { applicationTitle: schema.string({ defaultValue: '', }), + useExpandedHeader: schema.boolean({ + defaultValue: true, + }), }), }), deprecations, diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts index fec3fb0e677f..6103b2d748b6 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts @@ -40,7 +40,7 @@ const logger = loggingSystemMock.createLogger(); const pluginPath = resolve('path', 'existent-dir'); const pluginManifestPath = resolve(pluginPath, 'opensearch_dashboards.json'); const packageInfo = { - branch: 'master', + branch: 'main', buildNum: 1, buildSha: '', version: '7.0.0-alpha1', diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index a6828e4bd3b3..bc7480bb8adb 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -76,7 +76,7 @@ const Plugins = { }; const packageMock = { - branch: 'master', + branch: 'main', version: '1.2.3', build: { distributable: true, diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index a92325814a81..6e936484c2db 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -12,6 +12,7 @@ Object { "loadingLogo": Object {}, "logo": Object {}, "mark": Object {}, + "useExpandedHeader": true, }, "buildNumber": Any, "csp": Object { @@ -63,6 +64,7 @@ Object { "loadingLogo": Object {}, "logo": Object {}, "mark": Object {}, + "useExpandedHeader": true, }, "buildNumber": Any, "csp": Object { @@ -114,6 +116,7 @@ Object { "loadingLogo": Object {}, "logo": Object {}, "mark": Object {}, + "useExpandedHeader": true, }, "buildNumber": Any, "csp": Object { @@ -169,6 +172,7 @@ Object { "loadingLogo": Object {}, "logo": Object {}, "mark": Object {}, + "useExpandedHeader": true, }, "buildNumber": Any, "csp": Object { @@ -220,6 +224,7 @@ Object { "loadingLogo": Object {}, "logo": Object {}, "mark": Object {}, + "useExpandedHeader": true, }, "buildNumber": Any, "csp": Object { diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 28d2b38869fc..2362be550078 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -51,13 +51,16 @@ import { import { OpenSearchDashboardsConfigType } from '../opensearch_dashboards_config'; import { HttpConfigType } from '../http/http_config'; import { SslConfig } from '../http/ssl_config'; +import { LoggerFactory } from '../logging'; const DEFAULT_TITLE = 'OpenSearch Dashboards'; /** @internal */ export class RenderingService { - constructor(private readonly coreContext: CoreContext) {} - private logger = this.coreContext.logger; + constructor(private readonly coreContext: CoreContext) { + this.logger = this.coreContext.logger; + } + private logger: LoggerFactory; private httpsAgent?: HttpsAgent; public async setup({ @@ -148,6 +151,7 @@ export class RenderingService { }, faviconUrl: brandingAssignment.favicon, applicationTitle: brandingAssignment.applicationTitle, + useExpandedHeader: brandingAssignment.useExpandedHeader, }, }, }; @@ -260,11 +264,14 @@ export class RenderingService { // assign favicon based on brandingValidation function result const favicon = brandingValidation.isFaviconValid ? branding.faviconUrl : undefined; - // assign applition title based on brandingValidation function result + // assign application title based on brandingValidation function result const applicationTitle = brandingValidation.isTitleValid ? branding.applicationTitle : DEFAULT_TITLE; + // use expanded menu by default unless explicitly set to false + const { useExpandedHeader = true } = branding; + const brandingAssignment: BrandingAssignment = { logoDefault, logoDarkmode, @@ -274,6 +281,7 @@ export class RenderingService { loadingLogoDarkmode, favicon, applicationTitle, + useExpandedHeader, }; return brandingAssignment; diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index b8b0456bb757..79b016329f76 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -119,10 +119,10 @@ export interface InternalRenderingServiceSetup { } /** - * For each branding config: - * check if user provides a valid URL. - * Assign True -- if user provides a valid URL - * Assign False -- if user provides an invalid URL or user does not provide any URL + * For each string branding config: + * check if user provides a valid string. + * Assign True -- if user provides a valid string + * Assign False -- if user provides an invalid string or user does not provide any string */ export interface BrandingValidation { isLogoDefaultValid: boolean; @@ -137,7 +137,7 @@ export interface BrandingValidation { /** * For each branding config: - * if user provides a valid URL, the URL will be assigned; + * if user provides a valid value, the value will be assigned; * otherwise, undefined will be assigned. */ export interface BrandingAssignment { @@ -149,4 +149,5 @@ export interface BrandingAssignment { loadingLogoDarkmode?: string; favicon?: string; applicationTitle?: string; + useExpandedHeader?: boolean; } diff --git a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts index 23070484be45..05b8541e288d 100644 --- a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts +++ b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts @@ -179,7 +179,7 @@ test(`includes one object with type === 'object' and excludes one object without }); }); -test('excludes references and migrationVersion which are part of the blacklist', () => { +test('excludes references and migrationVersion which are part of the denylist', () => { const mappings = { properties: { references: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6061f78557b5..4cfe9f477e74 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1041,9 +1041,11 @@ export interface LegacyAPICaller { (endpoint: 'cat.help', params: CatHelpParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) (endpoint: 'cat.indices', params: CatIndicesParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) + // @deprecated (undocumented) (endpoint: 'cat.master', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) + (endpoint: 'cat.cluster_manager', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; + // (undocumented) (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; // (undocumented) (endpoint: 'cat.nodes', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; diff --git a/src/core/types/custom_branding.ts b/src/core/types/custom_branding.ts index 3e2b52c3c8e0..ca5631ea6cc5 100644 --- a/src/core/types/custom_branding.ts +++ b/src/core/types/custom_branding.ts @@ -57,4 +57,6 @@ export interface Branding { faviconUrl?: string; /** Application title that will replace the default opensearch dashboard string */ applicationTitle?: string; + /** Whether to use expanded menu (true) or condensed menu (false) */ + useExpandedHeader?: boolean; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index 9b90d70711f5..cd81f8d16c3f 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -149,6 +149,7 @@ opensearch_dashboards_vars=( telemetry.optIn telemetry.optInStatusUrl telemetry.sendUsageFrom + wizard.enabled ) longopts='' diff --git a/src/dev/ci_setup/checkout_sibling_opensearch.sh b/src/dev/ci_setup/checkout_sibling_opensearch.sh index dcab2b356d43..1ca3fcf4291c 100755 --- a/src/dev/ci_setup/checkout_sibling_opensearch.sh +++ b/src/dev/ci_setup/checkout_sibling_opensearch.sh @@ -45,7 +45,7 @@ function checkout_sibling { function pick_clone_target { echo "To develop OpenSearch Dashboards features against a specific branch of ${project} and being able to" echo "test that feature also on CI, the CI is trying to find branches on ${project} with the same name as" - echo "the OpenSearch Dashboards branch (first on your fork and then upstream) before building from master." + echo "the OpenSearch Dashboards branch (first on your fork and then upstream) before building from main." echo "picking which branch of ${project} to clone:" if [[ -n "$PR_AUTHOR" && -n "$PR_SOURCE_BRANCH" ]]; then cloneAuthor="$PR_AUTHOR" diff --git a/src/dev/ci_setup/get_percy_env.js b/src/dev/ci_setup/get_percy_env.js index d5ce19f5f834..e3890ad6846f 100644 --- a/src/dev/ci_setup/get_percy_env.js +++ b/src/dev/ci_setup/get_percy_env.js @@ -63,5 +63,5 @@ console.log( `export PERCY_PARALLEL_NONCE="${shortCommit}/${isPr ? 'PR' : branch}/${process.env.BUILD_ID}";` ); console.log(`export PERCY_BRANCH="${branch}";`); -// percy snapshots always target pkg.branch, so that feature branches can be based on master/7.x/etc. +// percy snapshots always target pkg.branch, so that feature branches can be based on main/7.x/etc. console.log(`export PERCY_TARGET_BRANCH="${isPr ? process.env.PR_TARGET_BRANCH : pkg.branch}";`); diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index c3dc1a07a94f..f685da4ed5b6 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -30,7 +30,7 @@ // The following list applies to packages both // used as dependencies or dev dependencies -export const LICENSE_WHITELIST = [ +export const LICENSE_ALLOWLIST = [ 'Elastic-License', '0BSD', '(BSD-2-Clause OR MIT OR Apache-2.0)', @@ -86,7 +86,7 @@ export const LICENSE_WHITELIST = [ // The following list only applies to licenses that // we wanna allow in packages only used as dev dependencies -export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0']; +export const DEV_ONLY_LICENSE_ALLOWLIST = ['MPL-2.0']; // Globally overrides a license for a given package@version export const LICENSE_OVERRIDES = { diff --git a/src/dev/license_checker/index.ts b/src/dev/license_checker/index.ts index 19450a7840ef..e8a309d15da0 100644 --- a/src/dev/license_checker/index.ts +++ b/src/dev/license_checker/index.ts @@ -28,5 +28,5 @@ * under the License. */ -export { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config'; +export { LICENSE_ALLOWLIST, DEV_ONLY_LICENSE_ALLOWLIST, LICENSE_OVERRIDES } from './config'; export { assertLicensesValid } from './valid'; diff --git a/src/dev/license_checker/run_check_licenses_cli.ts b/src/dev/license_checker/run_check_licenses_cli.ts index db7a9e9cbaa9..064e0d3d75ac 100644 --- a/src/dev/license_checker/run_check_licenses_cli.ts +++ b/src/dev/license_checker/run_check_licenses_cli.ts @@ -32,7 +32,7 @@ import { REPO_ROOT } from '@osd/utils'; import { run } from '@osd/dev-utils'; import { getInstalledPackages } from '../npm'; -import { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config'; +import { LICENSE_ALLOWLIST, DEV_ONLY_LICENSE_ALLOWLIST, LICENSE_OVERRIDES } from './config'; import { assertLicensesValid } from './valid'; run( @@ -47,7 +47,7 @@ run( // packages are valid assertLicensesValid({ packages: packages.filter((pkg) => !pkg.isDevOnly), - validLicenses: LICENSE_WHITELIST, + validLicenses: LICENSE_ALLOWLIST, }); log.success('All production dependency licenses are allowed'); @@ -56,7 +56,7 @@ run( if (flags.dev) { assertLicensesValid({ packages: packages.filter((pkg) => pkg.isDevOnly), - validLicenses: LICENSE_WHITELIST.concat(DEV_ONLY_LICENSE_WHITELIST), + validLicenses: LICENSE_ALLOWLIST.concat(DEV_ONLY_LICENSE_ALLOWLIST), }); log.success('All development dependency licenses are allowed'); } diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index d1d1a9337f05..7e7574d62009 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -61,7 +61,7 @@ export default () => name: Joi.string().default(os.hostname()), // keep them for BWC, remove when not used in Legacy. // validation should be in sync with one in New platform. - // https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/src/core/server/http/http_config.ts + // https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/http/http_config.ts basePath: Joi.string() .default('') .allow('') @@ -154,7 +154,7 @@ export default () => map: Joi.object({ includeOpenSearchMapsService: Joi.boolean().default(true), proxyOpenSearchMapsServiceInMaps: Joi.boolean().default(false), - showRegionBlockedWarning: Joi.boolean().default(false), + showRegionDeniedWarning: Joi.boolean().default(false), tilemap: Joi.object({ url: Joi.string(), options: Joi.object({ @@ -245,6 +245,7 @@ export default () => }), faviconUrl: Joi.any().default('/'), applicationTitle: Joi.any().default(''), + useExpandedHeader: Joi.boolean().default(true), }), }).default(), diff --git a/src/plugins/console/public/lib/osd/osd.js b/src/plugins/console/public/lib/osd/osd.js index cf6d2e03ad1c..529fba754a93 100644 --- a/src/plugins/console/public/lib/osd/osd.js +++ b/src/plugins/console/public/lib/osd/osd.js @@ -101,7 +101,17 @@ const parametrizedComponentFactories = { nodes: function (name, parent) { return new ListComponent( name, - ['_local', '_master', 'data:true', 'data:false', 'master:true', 'master:false'], + [ + '_local', + '_master', + '_cluster_manager', + 'data:true', + 'data:false', + 'master:true', + 'cluster_manager:true', + 'master:false', + 'cluster_manager:false', + ], parent ); }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json index 5529771a8f06..b95edbc237bd 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json @@ -13,7 +13,8 @@ "_source": [], "_source_excludes": [], "_source_includes": [], - "pipeline": "" + "pipeline": "", + "require_alias": "__flag__" }, "methods": [ "POST", @@ -21,8 +22,7 @@ ], "patterns": [ "_bulk", - "{indices}/_bulk", - "{indices}/{type}/_bulk" + "{indices}/_bulk" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/bulk/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json index 57b782e94618..90a2de2061d4 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json @@ -5,7 +5,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", @@ -16,7 +16,7 @@ "pb" ], "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.cluster_manager.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.cluster_manager.json new file mode 100644 index 000000000000..cb3c07dcd9f3 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.cluster_manager.json @@ -0,0 +1,20 @@ +{ + "cat.cluster_manager": { + "url_params": { + "format": "", + "local": "__flag__", + "cluster_manager_timeout": "", + "h": [], + "help": "__flag__", + "s": [], + "v": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_cat/cluster_manager" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/cat/cat-master/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json index c5e28cd072cd..29dbe8ab361f 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json @@ -5,7 +5,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json index c930cce0b863..4768cebcc958 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json @@ -5,7 +5,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", @@ -16,7 +16,7 @@ "pb" ], "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "health": [ "green", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json index fff5c9c30726..5c4475ddad2b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json @@ -3,7 +3,6 @@ "url_params": { "format": "", "local": "__flag__", - "master_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json index 84c1d13dbf6c..b2694a45187b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json @@ -3,7 +3,7 @@ "url_params": { "format": "", "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json index ae22f88c8567..7378a0f4745f 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json @@ -4,7 +4,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", @@ -17,7 +17,7 @@ "format": "", "full_id": "__flag__", "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json index c2e1731e5230..a86d02da2a63 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json @@ -3,7 +3,7 @@ "url_params": { "format": "", "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json index 9e2699a41ae8..d55197b71f5d 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json @@ -3,7 +3,7 @@ "url_params": { "format": "", "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json index 4c3d8a6cd204..2a37d9f5d9e8 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json @@ -6,7 +6,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json index 8e0e417fa5d4..442ebb67661e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json @@ -3,7 +3,7 @@ "url_params": { "format": "", "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json index 2f74834f9c81..7a089f29b69e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json @@ -5,7 +5,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", @@ -15,6 +15,7 @@ "p", "pb" ], + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json index 6aa7fe8e4663..e9c3929bc15e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json @@ -5,7 +5,7 @@ "bytes": [ "b", "k", - "osd", + "kb", "m", "mb", "g", @@ -16,7 +16,7 @@ "pb" ], "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json index b055aa585b6f..e612b6d5b2ae 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json @@ -3,7 +3,7 @@ "url_params": { "format": "", "ignore_unavailable": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json index c3575fac3aa1..a8b2a734f2d9 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json @@ -2,10 +2,10 @@ "cat.tasks": { "url_params": { "format": "", - "node_id": [], + "nodes": [], "actions": [], "detailed": "__flag__", - "parent_task": "", + "parent_task_id": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json index d68b52655241..d9ee651287ac 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json @@ -3,7 +3,7 @@ "url_params": { "format": "", "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json index a6c75370334c..c723812ca716 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json @@ -11,7 +11,7 @@ "p" ], "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json index 145c366683dc..ceb8e0a3fb20 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json @@ -2,7 +2,7 @@ "cluster.delete_component_template": { "url_params": { "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "DELETE" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_voting_config_exclusions.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_voting_config_exclusions.json new file mode 100644 index 000000000000..ca05fd8c8c8d --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_voting_config_exclusions.json @@ -0,0 +1,14 @@ +{ + "cluster.delete_voting_config_exclusions": { + "url_params": { + "wait_for_removal": "__flag__" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_cluster/voting_config_exclusions" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.exists_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.exists_component_template.json new file mode 100644 index 000000000000..040bf24f7fda --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.exists_component_template.json @@ -0,0 +1,14 @@ +{ + "cluster.exists_component_template": { + "url_params": { + "local": "__flag__" + }, + "methods": [ + "HEAD" + ], + "patterns": [ + "_component_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json new file mode 100644 index 000000000000..0b3c72580a09 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json @@ -0,0 +1,16 @@ +{ + "cluster.get_component_template": { + "url_params": { + "cluster_manager_timeout": "", + "local": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_component_template", + "_component_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json index 10427fa913f5..7a6d5b0d83c0 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json @@ -2,7 +2,7 @@ "cluster.get_settings": { "url_params": { "flat_settings": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "", "include_defaults": "__flag__" }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json index 02e5daeba1b3..289399f54b08 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json @@ -14,7 +14,7 @@ "shards" ], "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "", "wait_for_active_shards": "", "wait_for_nodes": "", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json index 68bb48c9c207..951c77972c1b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json @@ -2,7 +2,7 @@ "cluster.pending_tasks": { "url_params": { "local": "__flag__", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "GET" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.post_voting_config_exclusions.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.post_voting_config_exclusions.json new file mode 100644 index 000000000000..ef4f027dc6bc --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.post_voting_config_exclusions.json @@ -0,0 +1,16 @@ +{ + "cluster.post_voting_config_exclusions": { + "url_params": { + "node_ids": "", + "node_names": "", + "timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_cluster/voting_config_exclusions" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json new file mode 100644 index 000000000000..e304fdce140d --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json @@ -0,0 +1,17 @@ +{ + "cluster.put_component_template": { + "url_params": { + "create": "__flag__", + "timeout": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "PUT", + "POST" + ], + "patterns": [ + "_component_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json index 01965a331b07..afe29f06f036 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json @@ -2,7 +2,7 @@ "cluster.put_settings": { "url_params": { "flat_settings": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json index f0408c040d57..76282c495d9e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json @@ -5,7 +5,7 @@ "explain": "__flag__", "retry_failed": "__flag__", "metric": [], - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json index 230762b1ece9..a88a3f7a426a 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json @@ -2,7 +2,7 @@ "cluster.state": { "url_params": { "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "flat_settings": "__flag__", "wait_for_metadata_version": "", "wait_for_timeout": "", @@ -28,6 +28,7 @@ "metrics": [ "_all", "blocks", + "cluster_manager_node", "master_node", "metadata", "nodes", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/count.json index 3989ba8e7b0d..edecca119fcc 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/count.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/count.json @@ -31,8 +31,7 @@ ], "patterns": [ "_count", - "{indices}/_count", - "{indices}/{type}/_count" + "{indices}/_count" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/count/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.delete_dangling_index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.delete_dangling_index.json new file mode 100644 index 000000000000..0c12919eb4a9 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.delete_dangling_index.json @@ -0,0 +1,16 @@ +{ + "dangling_indices.delete_dangling_index": { + "url_params": { + "accept_data_loss": "__flag__", + "timeout": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_dangling/{index_uuid}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.import_dangling_index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.import_dangling_index.json new file mode 100644 index 000000000000..d9fa6f213071 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.import_dangling_index.json @@ -0,0 +1,16 @@ +{ + "dangling_indices.import_dangling_index": { + "url_params": { + "accept_data_loss": "__flag__", + "timeout": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_dangling/{index_uuid}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.list_dangling_indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.list_dangling_indices.json new file mode 100644 index 000000000000..b3899ffc180f --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.list_dangling_indices.json @@ -0,0 +1,11 @@ +{ + "dangling_indices.list_dangling_indices": { + "methods": [ + "GET" + ], + "patterns": [ + "_dangling" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json index 824d226644c6..916f83d4e5a4 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json @@ -54,8 +54,7 @@ "POST" ], "patterns": [ - "{indices}/_delete_by_query", - "{indices}/{type}/_delete_by_query" + "{indices}/_delete_by_query" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/delete-by-query/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json index 88d64b9920ff..effd13273069 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json @@ -2,7 +2,7 @@ "delete_script": { "url_params": { "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "DELETE" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json index ae2c8ca1e935..73ce43c1075f 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json @@ -21,8 +21,7 @@ "HEAD" ], "patterns": [ - "{indices}/_doc/{id}", - "{indices}/{type}/{id}" + "{indices}/_doc/{id}" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/get-documents/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json index 400f0dc767a1..a953c9ff6b94 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json @@ -22,8 +22,7 @@ "POST" ], "patterns": [ - "{indices}/_explain/{id}", - "{indices}/{type}/{id}/_explain" + "{indices}/_explain/{id}" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/explain/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json index 64599b6604be..edcb230dcee7 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json @@ -21,8 +21,7 @@ "GET" ], "patterns": [ - "{indices}/_doc/{id}", - "{indices}/{type}/{id}" + "{indices}/_doc/{id}" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/get-documents/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json index 685155e5b8d8..3ad5ca9b8f8d 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json @@ -1,7 +1,7 @@ { "get_script": { "url_params": { - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "GET" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json index 8396fc2412a1..92eab8b24977 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json @@ -20,8 +20,7 @@ "GET" ], "patterns": [ - "{indices}/_source/{id}", - "{indices}/{type}/{id}/_source" + "{indices}/_source/{id}" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/get-documents/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json index f70f6c9c8022..42244e800074 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/index.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json @@ -21,7 +21,8 @@ ], "if_seq_no": "", "if_primary_term": "", - "pipeline": "" + "pipeline": "", + "require_alias": "__flag__" }, "methods": [ "PUT", @@ -29,9 +30,7 @@ ], "patterns": [ "{indices}/_doc/{id}", - "{indices}/_doc", - "{indices}/{type}", - "{indices}/{type}/{id}" + "{indices}/_doc" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/index-document/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.add_block.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.add_block.json new file mode 100644 index 000000000000..b2ae41029b0e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.add_block.json @@ -0,0 +1,24 @@ +{ + "indices.add_block": { + "url_params": { + "timeout": "", + "cluster_manager_timeout": "", + "ignore_unavailable": "__flag__", + "allow_no_indices": "__flag__", + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "PUT" + ], + "patterns": [ + "{indices}/_block/{block}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json index 90d8a4e9d482..447b62c3775a 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json @@ -2,7 +2,7 @@ "indices.clone": { "url_params": { "timeout": "", - "master_timeout": "", + "cluster_manager_timeout": "", "wait_for_active_shards": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json index 2a390fe00548..db2ccf3edd35 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json @@ -2,7 +2,7 @@ "indices.close": { "url_params": { "timeout": "", - "master_timeout": "", + "cluster_manager_timeout": "", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json index 76f60474a5b7..48edafb6d8f4 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json @@ -1,10 +1,9 @@ { "indices.create": { "url_params": { - "include_type_name": "__flag__", "wait_for_active_shards": "", "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "PUT" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create_data_stream.json new file mode 100644 index 000000000000..f85e06e8493e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create_data_stream.json @@ -0,0 +1,11 @@ +{ + "indices.create_data_stream": { + "methods": [ + "PUT" + ], + "patterns": [ + "_data_stream/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.data_streams_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.data_streams_stats.json new file mode 100644 index 000000000000..b649524f728f --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.data_streams_stats.json @@ -0,0 +1,12 @@ +{ + "indices.data_streams_stats": { + "methods": [ + "GET" + ], + "patterns": [ + "_data_stream/_stats", + "_data_stream/{name}/_stats" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json index f8e02097101f..ac2dde5b6f8b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json @@ -2,7 +2,6 @@ "indices.delete": { "url_params": { "timeout": "", - "master_timeout": "", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json index deb73655b4da..560a255dbcc2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json @@ -2,7 +2,7 @@ "indices.delete_alias": { "url_params": { "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "DELETE" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json new file mode 100644 index 000000000000..8bc6472f314e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json @@ -0,0 +1,11 @@ +{ + "indices.delete_data_stream": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_data_stream/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_index_template.json new file mode 100644 index 000000000000..b7c458bfb786 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_index_template.json @@ -0,0 +1,15 @@ +{ + "indices.delete_index_template": { + "url_params": { + "timeout": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_index_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json index 7acda3fb6094..3287f293346b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json @@ -2,7 +2,7 @@ "indices.delete_template": { "url_params": { "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "DELETE" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_index_template.json new file mode 100644 index 000000000000..73fc4569f588 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_index_template.json @@ -0,0 +1,15 @@ +{ + "indices.exists_index_template": { + "url_params": { + "flat_settings": "__flag__", + "local": "__flag__" + }, + "methods": [ + "HEAD" + ], + "patterns": [ + "_index_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json index fa1e7bdfe869..c75c0b24a444 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json @@ -2,7 +2,6 @@ "indices.exists_template": { "url_params": { "flat_settings": "__flag__", - "master_timeout": "", "local": "__flag__" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json index 553a4839616d..57afba5ccabf 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json @@ -1,7 +1,6 @@ { "indices.get": { "url_params": { - "include_type_name": "__flag__", "local": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", @@ -14,7 +13,7 @@ ], "flat_settings": "__flag__", "include_defaults": "__flag__", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "GET" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json new file mode 100644 index 000000000000..f47d8bccebf4 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json @@ -0,0 +1,12 @@ +{ + "indices.get_data_stream": { + "methods": [ + "GET" + ], + "patterns": [ + "_data_stream", + "_data_stream/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json index d4fcde075033..43d5e1d29e06 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json @@ -1,7 +1,6 @@ { "indices.get_field_mapping": { "url_params": { - "include_type_name": "__flag__", "include_defaults": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", @@ -19,9 +18,7 @@ ], "patterns": [ "_mapping/field/{fields}", - "{indices}/_mapping/field/{fields}", - "_mapping/{type}/field/{fields}", - "{indices}/_mapping/{type}/field/{fields}" + "{indices}/_mapping/field/{fields}" ], "documentation": "https://opensearch.org/docs/latest/guide/en/elasticsearch/reference/master/indices-get-field-mapping.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_index_template.json new file mode 100644 index 000000000000..72fa958e0fbc --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_index_template.json @@ -0,0 +1,17 @@ +{ + "indices.get_index_template": { + "url_params": { + "flat_settings": "__flag__", + "cluster_manager_timeout": "", + "local": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_index_template", + "_index_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json index 584b0ad5a9bb..6504d7a5f4c7 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json @@ -1,7 +1,6 @@ { "indices.get_mapping": { "url_params": { - "include_type_name": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ @@ -11,7 +10,7 @@ "none", "all" ], - "master_timeout": "", + "cluster_manager_timeout": "", "local": "__flag__" }, "methods": [ @@ -19,9 +18,7 @@ ], "patterns": [ "_mapping", - "{indices}/_mapping", - "_mapping/{type}", - "{indices}/_mapping/{type}" + "{indices}/_mapping" ], "documentation": "https://opensearch.org/docs/latest/guide/en/elasticsearch/reference/master/indices-get-mapping.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json index e1dab98ca9cc..50896ad45ed9 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json @@ -1,7 +1,7 @@ { "indices.get_settings": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json index 46d679ce320e..61ce555296e3 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json @@ -1,9 +1,8 @@ { "indices.get_template": { "url_params": { - "include_type_name": "__flag__", "flat_settings": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "local": "__flag__" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json index 75e64a773a5f..e4789017a52a 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json @@ -2,7 +2,6 @@ "indices.open": { "url_params": { "timeout": "", - "master_timeout": "", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json index c326ec5f4df8..62d76260ee6b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json @@ -2,7 +2,7 @@ "indices.put_alias": { "url_params": { "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "PUT", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_index_template.json new file mode 100644 index 000000000000..e4d6be489a41 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_index_template.json @@ -0,0 +1,17 @@ +{ + "indices.put_index_template": { + "url_params": { + "create": "__flag__", + "cause": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "PUT", + "POST" + ], + "patterns": [ + "_index_template/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json index d161dfaf54c9..dc411ce03bde 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json @@ -1,9 +1,8 @@ { "indices.put_mapping": { "url_params": { - "include_type_name": "__flag__", "timeout": "", - "master_timeout": "", + "cluster_manager_timeout": "", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ @@ -12,21 +11,15 @@ "hidden", "none", "all" - ] + ], + "write_index_only": "__flag__" }, "methods": [ "PUT", "POST" ], "patterns": [ - "{indices}/_mapping", - "{indices}/{type}/_mapping", - "{indices}/_mapping/{type}", - "{indices}/{type}/_mappings", - "{indices}/_mappings/{type}", - "_mappings/{type}", - "{indices}/_mappings", - "_mapping/{type}" + "{indices}/_mapping" ], "documentation": "https://opensearch.org/docs/latest/guide/en/elasticsearch/reference/master/indices-put-mapping.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json index b81bde62f986..25025a58b2ca 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json @@ -1,7 +1,7 @@ { "indices.put_settings": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "", "preserve_existing": "__flag__", "ignore_unavailable": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json index 79e2d734c63f..0e8c2aa7d830 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json @@ -1,11 +1,9 @@ { "indices.put_template": { "url_params": { - "include_type_name": "__flag__", "order": "", "create": "__flag__", - "master_timeout": "", - "flat_settings": "__flag__" + "cluster_manager_timeout": "" }, "methods": [ "PUT", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.resolve_index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.resolve_index.json new file mode 100644 index 000000000000..4f19c7b8bed7 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.resolve_index.json @@ -0,0 +1,20 @@ +{ + "indices.resolve_index": { + "url_params": { + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "GET" + ], + "patterns": [ + "_resolve/index/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json index 3cdbfa07b7fe..dd0db6793521 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json @@ -1,10 +1,9 @@ { "indices.rollover": { "url_params": { - "include_type_name": "__flag__", "timeout": "", "dry_run": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "wait_for_active_shards": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json index cac199d9b0a0..0b1ce9dcc0dc 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json @@ -3,7 +3,7 @@ "url_params": { "copy_settings": "__flag__", "timeout": "", - "master_timeout": "", + "cluster_manager_timeout": "", "wait_for_active_shards": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_index_template.json new file mode 100644 index 000000000000..d942bdcb4cb9 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_index_template.json @@ -0,0 +1,16 @@ +{ + "indices.simulate_index_template": { + "url_params": { + "create": "__flag__", + "cause": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_index_template/_simulate_index/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_template.json new file mode 100644 index 000000000000..12255fd7b746 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_template.json @@ -0,0 +1,17 @@ +{ + "indices.simulate_template": { + "url_params": { + "create": "__flag__", + "cause": "", + "cluster_manager_timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_index_template/_simulate", + "_index_template/_simulate/{name}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json index 2331487becdc..42b97d6b1506 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json @@ -3,7 +3,7 @@ "url_params": { "copy_settings": "__flag__", "timeout": "", - "master_timeout": "", + "cluster_manager_timeout": "", "wait_for_active_shards": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json index c99e47998b6d..95a270ac3a32 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json @@ -10,7 +10,6 @@ "indices", "shards" ], - "types": [], "include_segment_file_sizes": "__flag__", "include_unloaded_segments": "__flag__", "expand_wildcards": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json index 097c3c8ec9a4..06ca27885464 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json @@ -2,7 +2,7 @@ "indices.update_aliases": { "url_params": { "timeout": "", - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "POST" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json index 9340854d9fd5..2b35a3aa7d15 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json @@ -1,7 +1,7 @@ { "ingest.delete_pipeline": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json index df442f3c72a6..df108b4a5a61 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json @@ -1,7 +1,7 @@ { "ingest.get_pipeline": { "url_params": { - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "GET" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json index 7bf882902252..68d1c980874d 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json @@ -1,7 +1,7 @@ { "ingest.put_pipeline": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json index 183a0c3a43a4..95ba338a673b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json @@ -16,8 +16,7 @@ ], "patterns": [ "_mget", - "{indices}/_mget", - "{indices}/{type}/_mget" + "{indices}/_mget" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/multi-get/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json index ee211859a788..f1d912e08b25 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json @@ -20,8 +20,7 @@ ], "patterns": [ "_msearch", - "{indices}/_msearch", - "{indices}/{type}/_msearch" + "{indices}/_msearch" ], "documentation": "https://opensearch.org/docs/latest/opensearch/query-dsl/full-text/#multi-match" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json index 4c6bfd73be73..b48f713e9f65 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json @@ -18,8 +18,7 @@ ], "patterns": [ "_msearch/template", - "{indices}/_msearch/template", - "{indices}/{type}/_msearch/template" + "{indices}/_msearch/template" ], "documentation": "https://opensearch.org/docs/latest/opensearch/query-dsl/full-text/#multi-match" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json index ce0c0024e383..07c1833ae3b2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json @@ -25,8 +25,7 @@ ], "patterns": [ "_mtermvectors", - "{indices}/_mtermvectors", - "{indices}/{type}/_mtermvectors" + "{indices}/_mtermvectors" ], "documentation": "https://opensearch.org/docs/latest/guide/en/elasticsearch/reference/master/docs-multi-termvectors.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json index f8d7065e954c..dbe08cbfe8a0 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json @@ -33,6 +33,7 @@ "discovery", "fs", "http", + "indexing_pressure", "indices", "jvm", "os", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json index 1de476ad8feb..008ecd5d2ec6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json @@ -2,7 +2,7 @@ "put_script": { "url_params": { "timeout": "", - "master_timeout": "", + "cluster_manager_timeout": "", "context": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/remote_store.restore.json b/src/plugins/console/server/lib/spec_definitions/json/generated/remote_store.restore.json new file mode 100644 index 000000000000..7c32a38850ea --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/remote_store.restore.json @@ -0,0 +1,15 @@ +{ + "remote_store.restore": { + "url_params": { + "cluster_manager_timeout": "", + "wait_for_completion": "__flag__" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_remotestore/_restore" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json index df7373fff040..5f98d0bd4ab3 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/search.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json @@ -66,8 +66,7 @@ ], "patterns": [ "_search", - "{indices}/_search", - "{indices}/{type}/_search" + "{indices}/_search" ], "documentation": "https://opensearch.org/docs/latest/opensearch/ux/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json index 5ecf508cd641..5c29f31db689 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json @@ -32,8 +32,7 @@ ], "patterns": [ "_search/template", - "{indices}/_search/template", - "{indices}/{type}/_search/template" + "{indices}/_search/template" ], "documentation": "https://opensearch.org/docs/latest/opensearch/search-template/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json index da6c33ff6972..7dbf9f6e5695 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json @@ -1,7 +1,7 @@ { "snapshot.cleanup_repository": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.clone.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.clone.json new file mode 100644 index 000000000000..175940c6f59a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.clone.json @@ -0,0 +1,14 @@ +{ + "snapshot.clone": { + "url_params": { + "cluster_manager_timeout": "" + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_snapshot/{repository}/{snapshot}/_clone/{target_snapshot}" + ], + "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json index 1e90cfa4cdb6..52cbcd80c253 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json @@ -1,7 +1,7 @@ { "snapshot.create": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "wait_for_completion": "__flag__" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json index 95e0cc4a5678..767bb2ca0dce 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json @@ -1,7 +1,7 @@ { "snapshot.create_repository": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "", "verify": "__flag__" }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json index 679d72c782d9..042f0882ec2c 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json @@ -1,7 +1,7 @@ { "snapshot.delete": { "url_params": { - "master_timeout": "" + "cluster_manager_timeout": "" }, "methods": [ "DELETE" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json index ef5e5117e94f..b2e742552aed 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json @@ -1,7 +1,7 @@ { "snapshot.delete_repository": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json index a582656ce029..10dfe6888dd7 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json @@ -1,7 +1,7 @@ { "snapshot.get": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "ignore_unavailable": "__flag__", "verbose": "__flag__" }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json index dbae1687dc90..e620736d3917 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json @@ -1,7 +1,7 @@ { "snapshot.get_repository": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "local": "__flag__" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json index 972fce7b39ce..20dfd170c55d 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json @@ -1,7 +1,7 @@ { "snapshot.restore": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "wait_for_completion": "__flag__" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json index 19fb493c695e..2ebf123f100e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json @@ -1,7 +1,7 @@ { "snapshot.status": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "ignore_unavailable": "__flag__" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json index 253c7e988505..db3409f37852 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json @@ -1,7 +1,7 @@ { "snapshot.verify_repository": { "url_params": { - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "" }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json index 49f81071204a..94e8aefa4e06 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json @@ -3,7 +3,8 @@ "url_params": { "nodes": [], "actions": [], - "parent_task_id": "" + "parent_task_id": "", + "wait_for_completion": "__flag__" }, "methods": [ "POST" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json index 552716b75a30..ebba502d9048 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json @@ -24,9 +24,7 @@ ], "patterns": [ "{indices}/_termvectors/{id}", - "{indices}/_termvectors", - "{indices}/{type}/{id}/_termvectors", - "{indices}/{type}/_termvectors" + "{indices}/_termvectors" ], "documentation": "https://opensearch.org/docs/latest/guide/en/elasticsearch/reference/master/docs-termvectors.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/update.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json index 837d958815a8..5a091d296a08 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/update.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json @@ -15,7 +15,8 @@ "routing": "", "timeout": "", "if_seq_no": "", - "if_primary_term": "" + "if_primary_term": "", + "require_alias": "__flag__" }, "methods": [ "POST" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json index 968956417cca..54cbca14d274 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json @@ -56,8 +56,7 @@ "POST" ], "patterns": [ - "{indices}/_update_by_query", - "{indices}/{type}/_update_by_query" + "{indices}/_update_by_query" ], "documentation": "https://opensearch.org/docs/latest/opensearch/rest-api/document-apis/update-by-query/" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json index 949b897b29ff..a8f515d6d044 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json @@ -14,7 +14,7 @@ "shards" ], "local": "__flag__", - "master_timeout": "", + "cluster_manager_timeout": "", "timeout": "", "wait_for_active_shards": "", "wait_for_nodes": "", diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js index 5ece8659260d..1b0e03653d0d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js @@ -41,6 +41,7 @@ import { setScopedHistory, setServices, setDocViewsRegistry, + setDocViewsLinksRegistry, } from '../../../../opensearch_dashboards_services'; import { coreMock } from '../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../data/public/mocks'; @@ -98,6 +99,18 @@ describe('Doc Table', () => { }, }); + setDocViewsLinksRegistry({ + addDocViewLink(view) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }); + getInnerAngularModule( 'app/discover', core, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html index fd20bea8fb3d..a28f1b9906cb 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html @@ -15,28 +15,12 @@ -
-
-
- -
-
- -
-
+
+
diff --git a/src/plugins/discover/public/application/angular/doc_viewer_links.tsx b/src/plugins/discover/public/application/angular/doc_viewer_links.tsx new file mode 100644 index 000000000000..763a75e51300 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_viewer_links.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DocViewerLinks } from '../components/doc_viewer_links/doc_viewer_links'; + +export function createDocViewerLinksDirective(reactDirective: any) { + return reactDirective( + (props: any) => { + return ; + }, + [ + 'hit', + ['indexPattern', { watchDepth: 'reference' }], + ['columns', { watchDepth: 'collection' }], + ], + { + restrict: 'E', + scope: { + hit: '=', + indexPattern: '=', + columns: '=?', + }, + } + ); +} 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 b107ae46b37a..7385f0d360a1 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -63,6 +63,17 @@ jest.mock('../../../opensearch_dashboards_services', () => { registry = []; }, }), + getDocViewsLinksRegistry: () => ({ + addDocViewLink(view: any) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), }; }); diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap new file mode 100644 index 000000000000..95fb0c377180 --- /dev/null +++ b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dont Render if generateCb.hide 1`] = ` + +`; + +exports[`Render with 2 different links 1`] = ` + + + + + + + + +`; diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx new file mode 100644 index 000000000000..8aba555b3a37 --- /dev/null +++ b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { DocViewerLinks } from './doc_viewer_links'; +import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services'; +import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types'; + +jest.mock('../../../opensearch_dashboards_services', () => { + let registry: any[] = []; + return { + getDocViewsLinksRegistry: () => ({ + addDocViewLink(view: any) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), + }; +}); + +beforeEach(() => { + (getDocViewsLinksRegistry() as any).resetRegistry(); + jest.clearAllMocks(); +}); + +test('Render with 2 different links', () => { + const registry = getDocViewsLinksRegistry(); + registry.addDocViewLink({ + order: 10, + label: 'generateCb link', + generateCb: () => ({ + url: 'aaa', + }), + }); + registry.addDocViewLink({ order: 20, label: 'href link', href: 'bbb' }); + + const renderProps = { hit: {} } as DocViewLinkRenderProps; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); +}); + +test('Dont Render if generateCb.hide', () => { + const registry = getDocViewsLinksRegistry(); + registry.addDocViewLink({ + order: 10, + label: 'generateCb link', + generateCb: () => ({ + url: 'aaa', + hide: true, + }), + }); + + const renderProps = { hit: {} } as DocViewLinkRenderProps; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx new file mode 100644 index 000000000000..9efb0693fde6 --- /dev/null +++ b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiListGroupItem, EuiListGroupItemProps } from '@elastic/eui'; +import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services'; +import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types'; + +export function DocViewerLinks(renderProps: DocViewLinkRenderProps) { + const listItems = getDocViewsLinksRegistry() + .getDocViewsLinksSorted() + .filter((item) => !(item.generateCb && item.generateCb(renderProps)?.hide)) + .map((item) => { + const { generateCb, href, ...props } = item; + const listItem: EuiListGroupItemProps = { + 'data-test-subj': 'docTableRowAction', + ...props, + href: generateCb ? generateCb(renderProps).url : href, + }; + + return listItem; + }); + + return ( + + {listItems.map((item, index) => ( + + + + ))} + + ); +} diff --git a/src/plugins/discover/public/application/doc_views_links/doc_views_links_registry.ts b/src/plugins/discover/public/application/doc_views_links/doc_views_links_registry.ts new file mode 100644 index 000000000000..16653f5d5377 --- /dev/null +++ b/src/plugins/discover/public/application/doc_views_links/doc_views_links_registry.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DocViewLink } from './doc_views_links_types'; + +export class DocViewsLinksRegistry { + private docViewsLinks: DocViewLink[] = []; + + addDocViewLink(docViewLink: DocViewLink) { + this.docViewsLinks.push(docViewLink); + } + + getDocViewsLinksSorted() { + return this.docViewsLinks.sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1)); + } +} diff --git a/src/plugins/discover/public/application/doc_views_links/doc_views_links_types.ts b/src/plugins/discover/public/application/doc_views_links/doc_views_links_types.ts new file mode 100644 index 000000000000..bbc5caadafcd --- /dev/null +++ b/src/plugins/discover/public/application/doc_views_links/doc_views_links_types.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiListGroupItemProps } from '@elastic/eui'; +import { OpenSearchSearchHit } from '../doc_views/doc_views_types'; +import { IndexPattern } from '../../../../data/public'; + +export interface DocViewLink extends EuiListGroupItemProps { + href?: string; + order: number; + generateCb?( + renderProps: any + ): { + url: string; + hide?: boolean; + }; +} + +export interface DocViewLinkRenderProps { + columns?: string[]; + hit: OpenSearchSearchHit; + indexPattern: IndexPattern; +} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 6764f6caccea..b4a7a17357ab 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -51,6 +51,7 @@ import { createTableRowDirective } from './application/angular/doc_table/compone import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; import { createDocViewerDirective } from './application/angular/doc_viewer'; +import { createDocViewerLinksDirective } from './application/angular/doc_viewer_links'; import { createRenderCompleteDirective } from './application/angular/directives/render_complete'; import { initAngularBootstrap, @@ -202,5 +203,6 @@ function createDocTableModule() { .directive('osdTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('osdInfiniteScroll', createInfiniteScrollDirective) - .directive('docViewer', createDocViewerDirective); + .directive('docViewer', createDocViewerDirective) + .directive('docViewerLinks', createDocViewerLinksDirective); } diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 6f4d7b8ea4f7..4724ced290ff 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -38,6 +38,9 @@ const createSetupContract = (): Setup => { docViews: { addDocView: jest.fn(), }, + docViewsLinks: { + addDocViewLink: jest.fn(), + }, }; return setupContract; }; diff --git a/src/plugins/discover/public/opensearch_dashboards_services.ts b/src/plugins/discover/public/opensearch_dashboards_services.ts index eb06e88ecacf..8531564e0cc7 100644 --- a/src/plugins/discover/public/opensearch_dashboards_services.ts +++ b/src/plugins/discover/public/opensearch_dashboards_services.ts @@ -36,6 +36,7 @@ import { DiscoverServices } from './build_services'; import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry'; let angularModule: any = null; let services: DiscoverServices | null = null; @@ -81,6 +82,10 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( 'DocViewsRegistry' ); + +export const [getDocViewsLinksRegistry, setDocViewsLinksRegistry] = createGetterSetter< + DocViewsLinksRegistry +>('DocViewsLinksRegistry'); /** * Makes sure discover and context are using one instance of history. */ diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index ea8659489d40..62f6e6908ba1 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -54,17 +54,22 @@ import { import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; +import { stringify } from 'query-string'; +import rison from 'rison-node'; import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; -import { createOsdUrlTracker } from '../../opensearch_dashboards_utils/public'; +import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; +import { DocViewLink } from './application/doc_views_links/doc_views_links_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry'; import { DocViewTable } from './application/components/table/table'; import { JsonCodeBlock } from './application/components/json_code_block/json_code_block'; import { setDocViewsRegistry, + setDocViewsLinksRegistry, setUrlTracker, setAngularModule, setServices, @@ -103,6 +108,10 @@ export interface DiscoverSetup { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; }; + + docViewsLinks: { + addDocViewLink(docViewLinkRaw: DocViewLink): void; + }; } export interface DiscoverStart { @@ -171,6 +180,7 @@ export class DiscoverPlugin private appStateUpdater = new BehaviorSubject(() => ({})); private docViewsRegistry: DocViewsRegistry | null = null; + private docViewsLinksRegistry: DocViewsLinksRegistry | null = null; private embeddableInjector: auto.IInjectorService | null = null; private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; @@ -205,6 +215,7 @@ export class DiscoverPlugin order: 10, component: DocViewTable, }); + this.docViewsRegistry.addDocView({ title: i18n.translate('discover.docViews.json.jsonTitle', { defaultMessage: 'JSON', @@ -213,6 +224,52 @@ export class DiscoverPlugin component: JsonCodeBlock, }); + this.docViewsLinksRegistry = new DocViewsLinksRegistry(); + setDocViewsLinksRegistry(this.docViewsLinksRegistry); + + this.docViewsLinksRegistry.addDocViewLink({ + label: i18n.translate('discover.docTable.tableRow.viewSurroundingDocumentsLinkText', { + defaultMessage: 'View surrounding documents', + }), + generateCb: (renderProps: any) => { + const globalFilters: any = getServices().filterManager.getGlobalFilters(); + const appFilters: any = getServices().filterManager.getAppFilters(); + + const hash = stringify( + url.encodeQuery({ + _g: rison.encode({ + filters: globalFilters || [], + }), + _a: rison.encode({ + columns: renderProps.columns, + filters: (appFilters || []).map(opensearchFilters.disableFilter), + }), + }), + { encode: false, sort: false } + ); + + return { + url: `#/context/${encodeURIComponent(renderProps.indexPattern.id)}/${encodeURIComponent( + renderProps.hit._id + )}?${hash}`, + hide: !renderProps.indexPattern.isTimeBased(), + }; + }, + order: 1, + }); + + this.docViewsLinksRegistry.addDocViewLink({ + label: i18n.translate('discover.docTable.tableRow.viewSingleDocumentLinkText', { + defaultMessage: 'View single document', + }), + generateCb: (renderProps) => ({ + url: `#/doc/${renderProps.indexPattern.id}/${ + renderProps.hit._index + }?id=${encodeURIComponent(renderProps.hit._id)}`, + }), + order: 2, + }); + const { appMounted, appUnMounted, @@ -319,6 +376,9 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + docViewsLinks: { + addDocViewLink: this.docViewsLinksRegistry.addDocViewLink.bind(this.docViewsLinksRegistry), + }, }; } diff --git a/src/plugins/embeddable/README.md b/src/plugins/embeddable/README.md index fb03e5893060..88b91125f8d9 100644 --- a/src/plugins/embeddable/README.md +++ b/src/plugins/embeddable/README.md @@ -8,7 +8,7 @@ Containers are a special type of embeddable that can contain nested embeddables. ## Examples -Many example embeddables are implemented and registered [here](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/examples/embeddable_examples). They can be played around with and explored [in the Embeddable Explorer example plugin](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/examples/embeddable_explorer). Just run OpenSearch Dashboards with +Many example embeddables are implemented and registered [here](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/examples/embeddable_examples). They can be played around with and explored [in the Embeddable Explorer example plugin](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/examples/embeddable_explorer). Just run OpenSearch Dashboards with ``` yarn start --run-examples @@ -16,7 +16,7 @@ yarn start --run-examples and navigate to the Embeddable explorer app. -There is also an example of rendering dashboard container outside of dashboard app [here](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/examples/dashboard_embeddable_examples). +There is also an example of rendering dashboard container outside of dashboard app [here](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/examples/dashboard_embeddable_examples). ## Docs diff --git a/src/plugins/home/public/application/components/_home.scss b/src/plugins/home/public/application/components/_home.scss index 3195fcbb0367..374470427d8e 100644 --- a/src/plugins/home/public/application/components/_home.scss +++ b/src/plugins/home/public/application/components/_home.scss @@ -33,6 +33,10 @@ display: flex; flex-direction: column; min-height: calc(100vh - #{$euiHeaderHeightCompensation}); + + .headerIsExpanded & { + min-height: calc(100vh - #{$euiHeaderHeightCompensation * 2}); + } } .homContent { diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/content.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/content.test.js.snap index 690875dc9690..1f8ad1822b0f 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/content.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/content.test.js.snap @@ -2,6 +2,14 @@ exports[`should render content with markdown 1`] = ` ); } diff --git a/src/plugins/home/server/tutorials/santa_logs/index.ts b/src/plugins/home/server/tutorials/santa_logs/index.ts index 46642bdce5e8..0008a3adb73d 100644 --- a/src/plugins/home/server/tutorials/santa_logs/index.ts +++ b/src/plugins/home/server/tutorials/santa_logs/index.ts @@ -52,7 +52,7 @@ export function santaLogsSpecProvider(context: TutorialContext): TutorialSchema longDescription: i18n.translate('home.tutorials.santaLogs.longDescription', { defaultMessage: 'The module collects and parses logs from [Google Santa](https://github.com/google/santa), \ - a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \ + a security tool for macOS that monitors process executions and can denylist/allowlist binaries. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-santa.html', diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx index bf6c069ac89a..42b5e4645c84 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx @@ -37,6 +37,7 @@ import { EuiPageContent, EuiSpacer, EuiText, EuiFlexItem, EuiFlexGroup } from '@ import { EuiDescriptionListTitle } from '@elastic/eui'; import { EuiDescriptionListDescription, EuiDescriptionList } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; +import { useMount } from 'react-use'; import { getListBreadcrumbs } from '../../breadcrumbs'; import { IndexPatternCreationOption } from '../../types'; import { CreateButton } from '../../create_button'; @@ -56,7 +57,9 @@ export const EmptyIndexPatternPrompt = ({ docLinksIndexPatternIntro, setBreadcrumbs, }: Props) => { - setBreadcrumbs(getListBreadcrumbs()); + useMount(() => { + setBreadcrumbs(getListBreadcrumbs()); + }); return ( { const [isLoadingSources, setIsLoadingSources] = useState(true); const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); - setBreadcrumbs(getListBreadcrumbs()); + useMount(() => { + setBreadcrumbs(getListBreadcrumbs()); + }); + useEffect(() => { (async function () { const options = await indexPatternManagementStart.creation.getIndexPatternCreationOptions( diff --git a/src/plugins/management/public/components/landing/landing.tsx b/src/plugins/management/public/components/landing/landing.tsx index 1ef25934d103..8c44f014d8e8 100644 --- a/src/plugins/management/public/components/landing/landing.tsx +++ b/src/plugins/management/public/components/landing/landing.tsx @@ -39,6 +39,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { useMount } from 'react-use'; interface ManagementLandingPageProps { version: string; @@ -46,7 +47,9 @@ interface ManagementLandingPageProps { } export const ManagementLandingPage = ({ version, setBreadcrumbs }: ManagementLandingPageProps) => { - setBreadcrumbs(); + useMount(() => { + setBreadcrumbs(); + }); return ( diff --git a/src/plugins/maps_legacy/config.ts b/src/plugins/maps_legacy/config.ts index 88de91170329..4ed5b4522c12 100644 --- a/src/plugins/maps_legacy/config.ts +++ b/src/plugins/maps_legacy/config.ts @@ -35,7 +35,7 @@ import { configSchema as regionmapSchema } from '../region_map/config'; export const configSchema = schema.object({ includeOpenSearchMapsService: schema.boolean({ defaultValue: true }), proxyOpenSearchMapsServiceInMaps: schema.boolean({ defaultValue: false }), - showRegionBlockedWarning: schema.boolean({ defaultValue: false }), + showRegionDeniedWarning: schema.boolean({ defaultValue: false }), tilemap: tilemapSchema, regionmap: regionmapSchema, manifestServiceUrl: schema.string({ defaultValue: '' }), diff --git a/src/plugins/maps_legacy/public/common/types/region_map_types.ts b/src/plugins/maps_legacy/public/common/types/region_map_types.ts index 58efaae5f640..c36d33f34184 100644 --- a/src/plugins/maps_legacy/public/common/types/region_map_types.ts +++ b/src/plugins/maps_legacy/public/common/types/region_map_types.ts @@ -41,6 +41,8 @@ export interface RegionMapVisParams { outlineWeight: number | ''; isDisplayWarning: boolean; showAllShapes: boolean; + selectedCustomLayer?: VectorLayer; + selectedCustomJoinField?: FileLayerField; selectedLayer?: VectorLayer; selectedJoinField?: FileLayerField; wms: WMSOptions; diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index b639fdf9bb7f..4eccea6faf92 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -164,7 +164,8 @@ export function BaseMapsVisualizationProvider() { try { if (this._wmsConfigured()) { - if (WMS_MINZOOM > this._opensearchDashboardsMap.getMaxZoomLevel()) { + const currentMaxZoomLevel = this._opensearchDashboardsMap.getMaxZoomLevel(); + if (WMS_MINZOOM < currentMaxZoomLevel || WMS_MAXZOOM > currentMaxZoomLevel) { this._opensearchDashboardsMap.setMinZoom(WMS_MINZOOM); this._opensearchDashboardsMap.setMaxZoom(WMS_MAXZOOM); } @@ -205,12 +206,13 @@ export function BaseMapsVisualizationProvider() { isDarkMode ); const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); + const showRegionDeniedWarning = serviceSettings.shouldShowRegionDeniedWarning(); const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._opensearchDashboardsMap.setBaseLayer({ baseLayerType: 'tms', - options: { ...options, showZoomMessage, ...meta }, + options: { ...options, showZoomMessage, showRegionDeniedWarning, ...meta }, }); } diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js index bce3220a5dc3..e87161c1ba1f 100644 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ b/src/plugins/maps_legacy/public/map/map_messages.js @@ -35,9 +35,9 @@ import { FormattedMessage } from '@osd/i18n/react'; import { EuiSpacer, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui'; import { toMountPoint } from '../../../opensearch_dashboards_react/public'; -export const createRegionBlockedWarning = (function () { +export const createRegionDeniedWarning = (function () { /* eslint-disable react/prefer-stateless-function */ - class RegionBlockedWarningOverlay extends React.Component { + class RegionDeniedWarningOverlay extends React.Component { constructor(props) { super(props); } @@ -71,7 +71,7 @@ export const createRegionBlockedWarning = (function () { document.getElementsByClassName('leaflet-container'), (leafletDom) => { ReactDOM.render( - new RegionBlockedWarningOverlay().render(), + new RegionDeniedWarningOverlay().render(), leafletDom.appendChild(messageBlock) ); } @@ -80,7 +80,7 @@ export const createRegionBlockedWarning = (function () { }; })(); -export const removeRegionBlockedWarning = (function () { +export const removeRegionDeniedWarning = (function () { return () => { const childEle = document.getElementById('blocker-div'); if (childEle) { diff --git a/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js b/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js index 0031d99fc0fe..3ca067c27796 100644 --- a/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js +++ b/src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js @@ -31,8 +31,8 @@ import { EventEmitter } from 'events'; import { createZoomWarningMsg, - createRegionBlockedWarning, - removeRegionBlockedWarning, + createRegionDeniedWarning, + removeRegionDeniedWarning, } from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; @@ -609,8 +609,8 @@ export class OpenSearchDashboardsMap extends EventEmitter { this.emit('baseLayer:loading'); }); baseLayer.on('tileerror', () => { - if (settings.options.showRegionBlockedWarning) { - createRegionBlockedWarning(); + if (settings.options.showRegionDeniedWarning) { + createRegionDeniedWarning(); } }); @@ -691,7 +691,7 @@ export class OpenSearchDashboardsMap extends EventEmitter { } _updateDesaturation() { - removeRegionBlockedWarning(); + removeRegionDeniedWarning(); const tiles = $('img.leaflet-tile-loaded'); // Don't apply client-side styling to EMS basemaps if (get(this._baseLayerSettings, 'options.origin') === ORIGIN.EMS) { diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 8aed2287632d..26f2cda24aaa 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -58,6 +58,7 @@ export class ServiceSettings { this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; this._showZoomMessage = true; + this._showRegionDeniedWarning = this._mapConfig.showRegionDeniedWarning; this._emsClient = null; this._opensearchMapsClient = new OpenSearchMapsClient({ language: i18n.getLocale(), @@ -88,6 +89,10 @@ export class ServiceSettings { }); } + shouldShowRegionDeniedWarning() { + return this._showRegionDeniedWarning; + } + shouldShowZoomMessage({ origin }) { return origin === ORIGIN.EMS && this._showZoomMessage; } diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 612c8be9745a..f6240d8aef50 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -38,7 +38,7 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { includeOpenSearchMapsService: true, proxyOpenSearchMapsServiceInMaps: true, - showRegionBlockedWarning: true, + showRegionDeniedWarning: true, tilemap: true, regionmap: true, manifestServiceUrl: true, @@ -57,6 +57,7 @@ export const config: PluginConfigDescriptor = { 'map.regionmap.includeElasticMapsService', 'map.regionmap.includeOpenSearchMapsService' ), + renameFromRoot('map.showRegionBlockedWarning', 'map.showRegionDeniedWarning'), ], }; diff --git a/src/plugins/navigation/README.md b/src/plugins/navigation/README.md index 2b32cb50f0b1..db65dd82ccfe 100644 --- a/src/plugins/navigation/README.md +++ b/src/plugins/navigation/README.md @@ -3,3 +3,11 @@ The navigation plugins exports the `TopNavMenu` component. It also provides a stateful version of it on the `start` contract. +## navigation.ui.TopNavMenu + +The `naivgation.ui` module exposes the `TopNavMenu` component that features The search bar, time filter and menu options to be used across the app in multiple locations. It primarity consists of 2 components: + +- Menu options: Options to show on the menu bar alongside the breadcrumbs +- Search bar: The [`data.ui.SearchBar`](../data/public/ui/search_bar/) component responsible for the query bar, time filter and field filters. + +Most of the logic for the component resides in the `SearchBar` component. This simply adds a way to add menu options on top of the search bar. diff --git a/src/plugins/opensearch_dashboards_overview/README.md b/src/plugins/opensearch_dashboards_overview/README.md index 1183b109d76f..a49b549e3dcd 100644 --- a/src/plugins/opensearch_dashboards_overview/README.md +++ b/src/plugins/opensearch_dashboards_overview/README.md @@ -6,4 +6,4 @@ ## Development -See the [OpenSearch Dashboards contributing guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. +See the [OpenSearch Dashboards contributing guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss b/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss index 45d5575d33cd..45dc59f59edb 100644 --- a/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss +++ b/src/plugins/opensearch_dashboards_overview/public/components/_overview.scss @@ -33,6 +33,10 @@ display: flex; flex-direction: column; min-height: calc(100vh - #{$euiHeaderHeightCompensation}); + + .headerIsExpanded & { + min-height: calc(100vh - #{$euiHeaderHeightCompensation * 2}); + } } .osdOverviewContent { diff --git a/src/plugins/opensearch_dashboards_react/public/markdown/__snapshots__/markdown.test.tsx.snap b/src/plugins/opensearch_dashboards_react/public/markdown/__snapshots__/markdown.test.tsx.snap index 29a68a55a978..2cd06435a104 100644 --- a/src/plugins/opensearch_dashboards_react/public/markdown/__snapshots__/markdown.test.tsx.snap +++ b/src/plugins/opensearch_dashboards_react/public/markdown/__snapshots__/markdown.test.tsx.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`props allowListedRules 1`] = ` +
I am some [content](https://en.wikipedia.org/wiki/Content) with markdown

+", + } + } +/> +`; + exports[`props markdown 1`] = `
{ expect(component).toMatchSnapshot(); }); + test('allowListedRules', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + test('should update markdown when openLinksInNewTab prop change', () => { const component = shallow(); expect(component.render().find('a').prop('target')).not.toBe('_blank'); @@ -111,4 +118,16 @@ describe('props', () => { expect(component.render().find('code')).toHaveLength(1); expect(component.render().find('em')).toHaveLength(0); }); + + test('should update markdown when allowListedRules prop change', () => { + const md = '*emphasis* `backticks`'; + const component = shallow( + + ); + expect(component.render().find('em')).toHaveLength(1); + expect(component.render().find('code')).toHaveLength(1); + component.setProps({ allowListedRules: ['backticks'] }); + expect(component.render().find('code')).toHaveLength(1); + expect(component.render().find('em')).toHaveLength(0); + }); }); diff --git a/src/plugins/opensearch_dashboards_react/public/markdown/markdown.tsx b/src/plugins/opensearch_dashboards_react/public/markdown/markdown.tsx index eeccf16eea53..1602b3c01c3d 100644 --- a/src/plugins/opensearch_dashboards_react/public/markdown/markdown.tsx +++ b/src/plugins/opensearch_dashboards_react/public/markdown/markdown.tsx @@ -37,22 +37,30 @@ import { getSecureRelForTarget } from '@elastic/eui'; import './index.scss'; /** * Return a memoized markdown rendering function that use the specified - * whiteListedRules and openLinksInNewTab configurations. - * @param {Array of Strings} whiteListedRules - white list of markdown rules + * whiteListedRules (deprecated) (use allowListedRules) and openLinksInNewTab configurations. + * @param {Array of Strings} whiteListedRules - allow list of markdown rules + * @param {Array of Strings} allowListedRules - allow list of markdown rules * list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361 * @param {Boolean} openLinksInNewTab * @return {Function} Returns an Object to use with dangerouslySetInnerHTML * with the rendered markdown HTML */ export const markdownFactory = memoize( - (whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => { + ( + whiteListedRules: string[] = [], + allowListedRules: string[] = [], + openLinksInNewTab: boolean = false + ) => { let markdownIt: MarkdownIt; // It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is // fed directly to the DOM via React's dangerouslySetInnerHTML below. - if (whiteListedRules && whiteListedRules.length > 0) { - markdownIt = new MarkdownIt('zero', { html: false, linkify: true }); + markdownIt = new MarkdownIt('zero', { html: false, linkify: true }); + + if (allowListedRules && allowListedRules.length > 0) { + markdownIt.enable(allowListedRules); + } else if (whiteListedRules && whiteListedRules.length > 0) { markdownIt.enable(whiteListedRules); } else { markdownIt = new MarkdownIt({ html: false, linkify: true }); @@ -90,8 +98,14 @@ export const markdownFactory = memoize( return markdown ? markdownIt.render(markdown) : ''; }; }, - (whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => { - return `${whiteListedRules.join('_')}${openLinksInNewTab}`; + ( + whiteListedRules: string[] = [], + allowListedRules: string[] = [], + openLinksInNewTab: boolean = false + ) => { + return whiteListedRules.length > 0 + ? `${whiteListedRules.join('_')}${openLinksInNewTab}` + : `${allowListedRules.join('_')}${openLinksInNewTab}`; } ); @@ -99,15 +113,24 @@ export interface MarkdownProps extends React.HTMLAttributes { className?: string; markdown?: string; openLinksInNewTab?: boolean; + /** @deprecated use allowListedRules: */ whiteListedRules?: string[]; + allowListedRules?: string[]; } export class Markdown extends PureComponent { render() { - const { className, markdown = '', openLinksInNewTab, whiteListedRules, ...rest } = this.props; + const { + className, + markdown = '', + openLinksInNewTab, + whiteListedRules, + allowListedRules, + ...rest + } = this.props; const classes = classNames('osdMarkdown__body', className); - const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab); + const markdownRenderer = markdownFactory(whiteListedRules, allowListedRules, openLinksInNewTab); const renderedMarkdown = markdownRenderer(markdown); return (
, * Creates helpers for using {@link StateContainer | State Containers} with react * * TODO Update link - * Refer to {@link https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/src/plugins/opensearch_dashboards_utils/docs/state_containers/react.md | guide} for details + * Refer to {@link https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/opensearch_dashboards_utils/docs/state_containers/react.md | guide} for details * @public */ export const createStateContainerReactHelpers = >() => { diff --git a/src/plugins/opensearch_dashboards_utils/common/state_containers/index.ts b/src/plugins/opensearch_dashboards_utils/common/state_containers/index.ts index ba2d8d6a26d3..e4522710e30c 100644 --- a/src/plugins/opensearch_dashboards_utils/common/state_containers/index.ts +++ b/src/plugins/opensearch_dashboards_utils/common/state_containers/index.ts @@ -31,7 +31,7 @@ /** * State containers are Redux-store-like objects meant to help you manage state in your services or apps. * TODO: Update link - * Refer to {@link https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/src/plugins/opensearch_dashboards_utils/docs/state_containers | guides and examples} for more info + * Refer to {@link https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_containers | guides and examples} for more info * * @packageDocumentation */ diff --git a/src/plugins/opensearch_dashboards_utils/public/state_sync/README.md b/src/plugins/opensearch_dashboards_utils/public/state_sync/README.md index a78616a8980e..c1e1f70c381a 100644 --- a/src/plugins/opensearch_dashboards_utils/public/state_sync/README.md +++ b/src/plugins/opensearch_dashboards_utils/public/state_sync/README.md @@ -1,3 +1,3 @@ - [docs](../../docs/state_sync) - [demo plugins](../../../../../examples/state_containers_examples): run OpenSearch Dashboards with `--run-examples` flag. -- [api reference](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/src/plugins/opensearch_dashboards_utils/docs/state_sync) +- [api reference](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_sync) diff --git a/src/plugins/opensearch_dashboards_utils/public/state_sync/index.ts b/src/plugins/opensearch_dashboards_utils/public/state_sync/index.ts index 15d1d8ddf78f..5d538679e1f9 100644 --- a/src/plugins/opensearch_dashboards_utils/public/state_sync/index.ts +++ b/src/plugins/opensearch_dashboards_utils/public/state_sync/index.ts @@ -32,7 +32,7 @@ * State syncing utilities are a set of helpers for syncing your application state * with browser URL or browser storage. * - * They are designed to work together with {@link https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/src/plugins/opensearch_dashboards_utils/docs/state_containers | state containers}. But state containers are not required. + * They are designed to work together with {@link https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_containers | state containers}. But state containers are not required. * * State syncing utilities include: * @@ -44,7 +44,7 @@ * Listens for state updates in the URL and pushes them back to state. * * {@link ISessionStorageStateStorage} - Serializes state and persists it to browser storage. * - * Refer {@link https://github.com/opensearch-project/OpenSearch-Dashboards/tree/master/src/plugins/opensearch_dashboards_utils/docs/state_sync | here} for a complete guide and examples. + * Refer {@link https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_sync | here} for a complete guide and examples. * @packageDocumentation */ diff --git a/src/plugins/opensearch_dashboards_utils/public/state_sync/state_sync.ts b/src/plugins/opensearch_dashboards_utils/public/state_sync/state_sync.ts index 8745ed436a5b..92185d004091 100644 --- a/src/plugins/opensearch_dashboards_utils/public/state_sync/state_sync.ts +++ b/src/plugins/opensearch_dashboards_utils/public/state_sync/state_sync.ts @@ -64,7 +64,7 @@ export interface ISyncStateRef { return arr1.concat(arr2).filter((item) => !arr1.includes(item) || !arr2.includes(item)); }; @@ -201,9 +202,10 @@ provided base maps, or add your own. Darker colors represent higher values.', const customIndices = await getCustomIndices(); let selectedLayer = vectorLayers[0]; - let selectedCustomLayer = customVectorLayers[0]; let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null; - const selectedCustomJoinField = selectedCustomLayer ? selectedCustomLayer.fields[0] : null; + + let selectedCustomLayer = customVectorLayers[0]; + let selectedCustomJoinField = selectedCustomLayer ? selectedCustomLayer.fields[0] : null; if (regionmapsConfig.includeOpenSearchMapsService) { const layers = await serviceSettings.getFileLayers(); @@ -241,11 +243,9 @@ provided base maps, or add your own. Darker colors represent higher values.', ]; [selectedLayer] = vis.type.editorConfig.collections.vectorLayers; - [selectedCustomLayer] = vis.type.editorConfig.collections.customVectorLayers; - vis.params.selectedCustomLayer = selectedCustomLayer; - vis.params.selectedCustomJoinField = selectedCustomJoinField; - selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null; + [selectedCustomLayer] = vis.type.editorConfig.collections.customVectorLayers; + selectedCustomJoinField = selectedCustomLayer ? selectedCustomLayer.fields[0] : null; if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) { vis.params.emsHotLink = await serviceSettings.getEMSHotLink(selectedLayer); @@ -257,6 +257,11 @@ provided base maps, or add your own. Darker colors represent higher values.', vis.params.selectedJoinField = selectedJoinField; } + if (!vis.params.selectedCustomLayer) { + vis.params.selectedCustomLayer = selectedCustomLayer; + vis.params.selectedCustomJoinField = selectedCustomJoinField; + } + vis.params.layerChosenByUser = vis.params.layerChosenByUser ? vis.params.layerChosenByUser : DEFAULT_MAP_CHOICE; diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 4580d9911205..101e4233ca72 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -37,7 +37,7 @@ import { import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; import { mapTooltipProvider, ORIGIN, lazyLoadMapsLegacyModules } from '../../maps_legacy/public'; -import { DEFAULT_MAP_CHOICE } from '../common'; +import { DEFAULT_MAP_CHOICE, CUSTOM_MAP_CHOICE } from '../common'; export function createRegionMapVisualization({ http, @@ -86,7 +86,7 @@ export function createRegionMapVisualization({ let selectedLayer; if (DEFAULT_MAP_CHOICE === this._params.layerChosenByUser) { selectedLayer = await this._loadConfig(this._params.selectedLayer); - this._params.selectedJoinField = selectedLayer.fields[0]; + this._params.selectedJoinField = selectedLayer?.fields[0]; } else { selectedLayer = this._params.selectedCustomLayer; this._params.selectedJoinField = this._params.selectedCustomJoinField; @@ -156,12 +156,15 @@ export function createRegionMapVisualization({ async _updateParams() { await super._updateParams(); let selectedLayer; - if (DEFAULT_MAP_CHOICE === this._params.layerChosenByUser) { + if (DEFAULT_MAP_CHOICE === this._params.layerChosenByUser && this._params.selectedLayer) { selectedLayer = await this._loadConfig(this._params.selectedLayer); - this._params.selectedJoinField = selectedLayer.fields[0]; - } else { + this._params.selectedJoinField = selectedLayer?.fields[0]; + } else if ( + CUSTOM_MAP_CHOICE === this._params.layerChosenByUser && + this._params.selectedCustomLayer + ) { selectedLayer = this._params.selectedCustomLayer; - this._params.selectedJoinField = this._params.selectedCustomJoinField; + this._params.selectedJoinField = this._params?.selectedCustomJoinField; } if (!this._params.selectedJoinField && selectedLayer) { diff --git a/src/plugins/region_map/server/plugin.js b/src/plugins/region_map/server/plugin.js deleted file mode 100644 index 14aeb0801d23..000000000000 --- a/src/plugins/region_map/server/plugin.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { OpensearchService } from './services'; -import { opensearch } from '../server/routes'; -import { getUiSettings } from './ui_settings'; - -export class RegionMapPlugin { - constructor(initializerContext) { - this.logger = initializerContext.logger.get(); - } - - async setup(core) { - const opensearchClient = core.opensearch.legacy.createClient('opensearch'); - - // Initialize services - const opensearchService = new OpensearchService(opensearchClient); - - // Register server side APIs - const router = core.http.createRouter(); - core.uiSettings.register(getUiSettings()); - opensearch(opensearchService, router); - - return {}; - } - - async start() { - return {}; - } - - async stop() {} -} diff --git a/src/plugins/region_map/server/plugin.ts b/src/plugins/region_map/server/plugin.ts new file mode 100644 index 000000000000..6fc6e8987bc0 --- /dev/null +++ b/src/plugins/region_map/server/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from 'opensearch-dashboards/server'; +import { registerGeospatialRoutes } from '../server/routes'; +import { getUiSettings } from './ui_settings'; +import { RegionMapPluginSetup, RegionMapPluginStart } from './types'; + +export class RegionMapPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('RegionMap: Setup'); + const router = core.http.createRouter(); + core.uiSettings.register(getUiSettings()); + registerGeospatialRoutes(router); + return {}; + } + + public start(_core: CoreStart) { + this.logger.debug('RegionMap: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/region_map/server/routes/index.ts b/src/plugins/region_map/server/routes/index.ts index bdbd7bdca88e..37d50c83f609 100644 --- a/src/plugins/region_map/server/routes/index.ts +++ b/src/plugins/region_map/server/routes/index.ts @@ -3,6 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -import opensearch from './opensearch'; - -export { opensearch }; +export { registerGeospatialRoutes } from './opensearch'; diff --git a/src/plugins/region_map/server/routes/opensearch.ts b/src/plugins/region_map/server/routes/opensearch.ts index 2d67bc9c4e32..5eebc9a0ffda 100644 --- a/src/plugins/region_map/server/routes/opensearch.ts +++ b/src/plugins/region_map/server/routes/opensearch.ts @@ -4,9 +4,9 @@ */ import { schema } from '@osd/config-schema'; +import { IRouter } from 'opensearch-dashboards/server'; -// eslint-disable-next-line import/no-default-export -export default function (services, router) { +export function registerGeospatialRoutes(router: IRouter) { router.post( { path: '/api/geospatial/_indices', @@ -16,7 +16,39 @@ export default function (services, router) { }), }, }, - services.getIndex + async (context, req, res) => { + const client = context.core.opensearch.client.asCurrentUser; + try { + const { index } = req.body; + const indices = await client.cat.indices({ + index, + format: 'json', + }); + return res.ok({ + body: { + ok: true, + resp: indices.body, + }, + }); + } catch (err: any) { + // Opensearch throws an index_not_found_exception which we'll treat as a success + if (err.statusCode === 404) { + return res.ok({ + body: { + ok: false, + resp: [], + }, + }); + } else { + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + } + } ); router.post( @@ -28,7 +60,27 @@ export default function (services, router) { }), }, }, - services.search + async (context, req, res) => { + const client = context.core.opensearch.client.asCurrentUser; + try { + const { index } = req.body; + const params = { index, body: {} }; + const results = await client.search(params); + return res.ok({ + body: { + ok: true, + resp: results.body, + }, + }); + } catch (err: any) { + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + } ); router.post( @@ -40,6 +92,25 @@ export default function (services, router) { }), }, }, - services.getMappings + async (context, req, res) => { + const client = context.core.opensearch.client.asCurrentUser; + try { + const { index } = req.body; + const mappings = await client.indices.getMapping({ index }); + return res.ok({ + body: { + ok: true, + resp: mappings.body, + }, + }); + } catch (err: any) { + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + } ); } diff --git a/src/plugins/region_map/server/services/index.js b/src/plugins/region_map/server/services/index.js deleted file mode 100644 index 63a48fdfdd4d..000000000000 --- a/src/plugins/region_map/server/services/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import OpensearchService from './opensearch_service'; - -export { OpensearchService }; diff --git a/src/plugins/region_map/server/services/opensearch_service.js b/src/plugins/region_map/server/services/opensearch_service.js deleted file mode 100644 index e9be890b72f0..000000000000 --- a/src/plugins/region_map/server/services/opensearch_service.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export default class OpensearchService { - constructor(esDriver) { - this.esDriver = esDriver; - } - - getMappings = async (context, req, res) => { - try { - const { index } = req.body; - const { callAsCurrentUser } = this.esDriver.asScoped(req); - const mappings = await callAsCurrentUser('indices.getMapping', { index }); - return res.ok({ - body: { - ok: true, - resp: mappings, - }, - }); - } catch (err) { - return res.ok({ - body: { - ok: false, - resp: err.message, - }, - }); - } - }; - - search = async (context, req, res) => { - try { - const { query, index, size } = req.body; - const params = { index, size, body: query }; - const { callAsCurrentUser } = this.esDriver.asScoped(req); - const results = await callAsCurrentUser('search', params); - return res.ok({ - body: { - ok: true, - resp: results, - }, - }); - } catch (err) { - return res.ok({ - body: { - ok: false, - resp: err.message, - }, - }); - } - }; - - getIndex = async (context, req, res) => { - try { - const { index } = req.body; - const { callAsCurrentUser } = this.esDriver.asScoped(req); - const indices = await callAsCurrentUser('cat.indices', { - index, - format: 'json', - h: 'health,index,status', - }); - return res.ok({ - body: { - ok: true, - resp: indices, - }, - }); - } catch (err) { - // Opensearch throws an index_not_found_exception which we'll treat as a success - if (err.statusCode === 404) { - return res.ok({ - body: { - ok: false, - resp: [], - }, - }); - } else { - return res.ok({ - body: { - ok: false, - resp: err.message, - }, - }); - } - } - }; -} diff --git a/src/plugins/saved_objects/README.md b/src/plugins/saved_objects/README.md new file mode 100644 index 000000000000..2ea57a3d6334 --- /dev/null +++ b/src/plugins/saved_objects/README.md @@ -0,0 +1,119 @@ +# Saved object + +The saved object plugin provides all the core services and functionalities of saved objects. It is utilized by many core plugins such as [`visualization`](../visualizations/), [`dashboard`](../dashboard/) and [`wizard`](../wizard/), as well as external plugins. Saved object is the primary way to store app and plugin data in a standardized form in OpenSearch Dashboards. They allow plugin developers to manage creating, saving, editing and retrieving data for the application. They can also make reference to other saved objects and have useful features out of the box, such as migrations and strict typings. The saved objects can be managed by the Saved Object Management UI. + +## Save relationships to index pattern + +Saved objects that have relationships to index patterns are saved using the [`kibanaSavedObjectMeta`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts#L59) attribute and the [`references`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts#L60) array structure. Functions from the data plugin are used by the saved object plugin to manage this index pattern relationship. + +A standard saved object and its index pattern relationship: + +```ts + +"kibanaSavedObjectMeta" : { + "searchSourceJSON" : """{"filter":[],"query":{"query":"","language":"kuery"},"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}""" + } + }, + "type" : "visualization", + "references" : [ + { + "name" : "kibanaSavedObjectMeta.searchSourceJSON.index", + "type" : "index-pattern", + "id" : "90943e30-9a47-11e8-b64d-95841ca0b247" + } + ], + +``` + +### Saving a saved object + +When saving a saved object and its relationship to the index pattern: + +1. A saved object will be built using [`buildSavedObject`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts#L46) function. Services such as hydrating index pattern, initializing and serializing the saved object are set, and configs such as saved object id, migration version are defined. +2. The saved object will then be serialized by three steps: + + a. By using [`extractReferences`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/data/common/search/search_source/extract_references.ts#L35) function from the data plugin, the index pattern information will be extracted using the index pattern id within the `kibanaSavedObjectMeta`, and the id will be replaced by a reference name, such as `indexRefName`. A corresponding index pattern object will then be created to include more detailed information of the index pattern: name (`kibanaSavedObjectMeta.searchSourceJSON.index`), type, and id. + + ```ts + let searchSourceFields = { ...state }; + const references = []; + + if (searchSourceFields.index) { + const indexId = searchSourceFields.index.id || searchSourceFields.index; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId + }); + searchSourceFields = { ...searchSourceFields, + indexRefName: refName, + index: undefined + }; + } + ``` + + b. The `indexRefName` along with other information will be stringified and saved into `kibanaSavedObjectMeta.searchSourceJSON`. + + c. Saved object client will create the reference array attribute, and the index pattern object will be pushed into the reference array. + + +### Loading an existing or creating a new saved object + +1. When loading an existing object or creating a new saved object, [`initializeSavedObject`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts#L38) function will be called. +2. The saved object will be deserialized in the [`applyOpenSearchResp`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/saved_objects/public/saved_object/helpers/apply_opensearch_resp.ts#L50) function. + + a. Using [`injectReferences`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/data/common/search/search_source/inject_references.ts#L34) function from the data plugin, the index pattern reference name within the `kibanaSavedObject` will be substituted by the index pattern id and the corresponding index pattern reference object will be deleted if filters are applied. + + ```ts + searchSourceReturnFields.index = reference.id; + delete searchSourceReturnFields.indexRefName; + ``` + +### Others + +If a saved object type wishes to have additional custom functionalities when extracting/injecting references, or after OpenSearch's response, it can define functions in the class constructor when extending the `SavedObjectClass`. For example, visualization plugin's [`SavedVis`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4a06f5a6fe404a65b11775d292afaff4b8677c33/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#L91) class has additional `extractReferences`, `injectReferences` and `afterOpenSearchResp` functions defined in [`_saved_vis.ts`](../visualizations/public/saved_visualizations/_saved_vis.ts). + +```ts +class SavedVis extends SavedObjectClass { + constructor(opts: Record | string = {}) { + super({ + ... ... + extractReferences, + injectReferences, + ... ... + afterOpenSearchResp: async (savedObject: SavedObject) => { + const savedVis = (savedObject as any) as ISavedVis; + ... ... + + return (savedVis as any) as SavedObject; + }, +``` + +## Migration + +When a saved object is created using a previous version, the migration will trigger if there is a new way of saving the saved object and the migration functions alter the structure of the old saved object to follow the new structure. Migrations can be defined in the specific saved object type in the plugin's server folder. For example, + +```ts +export const visualizationSavedObjectType: SavedObjectsType = { + name: 'visualization', + management: {}, + mappings: {}, + migrations: visualizationSavedObjectTypeMigrations, +}; +``` + +```ts +const visualizationSavedObjectTypeMigrations = { + '1.0.0': flow(migrateIndexPattern), +``` + +The migraton version will be saved as a `migrationVersion` attribute in the saved object, not to be confused with the other `verson` attribute. + +```ts +"migrationVersion" : { + "visualization" : "1.0.0" +}, +``` + +For a more detailed explanation on the migration, refer to [`saved objects management`](src/core/server/saved_objects/migrations/README.md). \ No newline at end of file diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md new file mode 100644 index 000000000000..4afae978c258 --- /dev/null +++ b/src/plugins/saved_objects_management/README.md @@ -0,0 +1,42 @@ +# Saved objects management + +Provides a UI (via the `management` plugin) to find and manage all saved objects in one place (you can see the primary page by navigating to `/app/management/opensearch-dashboards/objects`). Not to be confused with the `savedObjects` plugin, which provides all the core capabilities of saved objects. + +From the primary UI page, this plugin allows you to: +1. Search/view/delete saved objects and their relationships +2. Import/export saved objects +3. Inspect/edit raw saved object values without validation + +For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are also free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly). + +## Making a new saved object type manageable + +1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See [`SavedObjectsTypeManagementDefinition`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) for explanation of its properties) +2. Register saved object type via `core.savedObjects.registerType(...)` as part of plugin server setup method +3. Implement a way to save the object (e.g. via `savedObjectsClient.create(...)` or a `savedObjectLoader`) +4. After these steps, you should be able to save objects and view/search for them in Saved Objects management (`/app/management/opensearch-dashboards/objects`) + +## Enabling edit links from saved objects management + +1. Make sure `management.getInAppUrl` method of the `SavedObjectsType` is defined with a `path` (which will specify the link target) and the `uiCapabilitiesPath` +2. For `uiCapabilitiesPath` to work without additional hardcoding, it should be in the format `{plugin}.show`, so that [the default logic of `src/plugins/saved_objects_management/public/lib/in_app_url.ts`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L48-L50) will correctly match. Otherwise, you'll need to [add a case for your `uiCapabilities` path](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L45-L47) to that function +3. Create default plugin capabilities provider +4. Register plugin capabilities via `core.capabilities.registerProvider(...);` as part of plugin server setup method + +## Using saved objects management to inspect/edit new plugin objects + +You'll notice that when clicking on the "Inspect" button from the saved objects management table, you'll usually be routed to something like `/app/management/opensearch-dashboards/objects/savedVisualizations/` (where the route itself is determined by the `management.getEditUrl` method of the `SavedObjectsType`). But to register a similar route for a new saved object type, you'll need to create a new `savedObjectLoader` and register it with the management plugin. + +### Creating `savedObjectLoader` + +1. In your plugin's public directory, create a class for your saved object that extends `SavedObjectClass`. The mapping should match the `mappings` defined in your `SavedObjectsType`. +2. Create a `savedObjectLoader` creation function that returns a `new SavedObjectLoader(YourSavedObjectClass, savedObjectsClient)` +3. Return that `savedObjectLoader` as part of your public plugin `start` method + +### Registering + +Ideally, we'd allow plugins to self-register their `savedObjectLoader` and (declare a dependency on this plugin). However, as currently implemented, any plugins that want this plugin to handle their inspect routes need to be added as optional dependencies and registered here. + +1. Add your plugin to the `optionalPlugins` array in `./opensearch_dashboards.json` +2. Update the `StartDependencies` interface of this plugin to include the public plugin start type +3. Update `registerServices` to register a new type of `SavedObjectLoader`, where `id` will be the route, `title` will likely match your saved object type, and `service` is your `SavedObjectLoader` that is defined in your plugin start. diff --git a/src/plugins/saved_objects_management/opensearch_dashboards.json b/src/plugins/saved_objects_management/opensearch_dashboards.json index deca4778e34c..21a0b00860b4 100644 --- a/src/plugins/saved_objects_management/opensearch_dashboards.json +++ b/src/plugins/saved_objects_management/opensearch_dashboards.json @@ -4,7 +4,7 @@ "server": true, "ui": true, "requiredPlugins": ["management", "data"], - "optionalPlugins": ["dashboard", "visualizations", "discover", "home"], + "optionalPlugins": ["dashboard", "visualizations", "discover", "home", "wizard"], "extraPublicDirs": ["public/lib"], "requiredBundles": ["opensearchDashboardsReact", "home"] } diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index fd9eebba9016..74d03a150470 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -30,6 +30,8 @@ import { i18n } from '@osd/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; + +import { WizardStart } from '../../wizard/public'; import { ManagementSetup } from '../../management/public'; import { DataPublicPluginStart } from '../../data/public'; import { DashboardStart } from '../../dashboard/public'; @@ -69,6 +71,7 @@ export interface StartDependencies { dashboard?: DashboardStart; visualizations?: VisualizationsStart; discover?: DiscoverStart; + wizard?: WizardStart; } export class SavedObjectsManagementPlugin diff --git a/src/plugins/saved_objects_management/public/register_services.ts b/src/plugins/saved_objects_management/public/register_services.ts index 757876b8407b..8ce6ed20bc9f 100644 --- a/src/plugins/saved_objects_management/public/register_services.ts +++ b/src/plugins/saved_objects_management/public/register_services.ts @@ -36,7 +36,7 @@ export const registerServices = async ( registry: ISavedObjectsManagementServiceRegistry, getStartServices: StartServicesAccessor ) => { - const [, { dashboard, visualizations, discover }] = await getStartServices(); + const [, { dashboard, visualizations, discover, wizard }] = await getStartServices(); if (dashboard) { registry.register({ @@ -61,4 +61,12 @@ export const registerServices = async ( service: discover.savedSearchLoader, }); } + + if (wizard) { + registry.register({ + id: 'savedWizard', + title: 'wizard', + service: wizard.savedWizardLoader, + }); + } }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts index a4e290a4c1a6..b3e12d69b757 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts @@ -53,7 +53,7 @@ export interface OpenSearchClusterInfo { * * @param {function} opensearchClient The asInternalUser handler (exposed for testing) */ -export async function getClusterInfo(opensearchClient: OpenSearchClusterInfo) { - const { body } = await opensearchClient.info(); +export async function getClusterInfo(opensearchClient: OpenSearchClient) { + const { body } = await opensearchClient.info(); return body; } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts index 9fd10ec3c629..8433c6f4a65c 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts @@ -49,18 +49,19 @@ async function fetchLicense(opensearchClient: OpenSearchClient, local: boolean) * Like any X-Pack related API, X-Pack must installed for this to work. * * In OSS we'll get a 400 response using the new opensearch client. + * @deprecated */ -async function getLicenseFromLocalOrMaster(opensearchClient: OpenSearchClient) { - // Fetching the local license is cheaper than getting it from the master node and good enough +async function getLicenseFromLocalOrClusterManager(opensearchClient: OpenSearchClient) { + // Fetching the local license is cheaper than getting it from the cluster manager node and good enough const { license } = await fetchLicense(opensearchClient, true).catch(async (err) => { if (cachedLicense) { try { - // Fallback to the master node's license info + // Fallback to the cluster manager node's license info const response = await fetchLicense(opensearchClient, false); return response; - } catch (masterError) { - if ([400, 404].includes(masterError.statusCode)) { - // If the master node does not have a license, we can assume there is no license + } catch (clusterManagerError) { + if ([400, 404].includes(clusterManagerError.statusCode)) { + // If the cluster manager node does not have a license, we can assume there is no license cachedLicense = undefined; } else { throw err; @@ -77,7 +78,7 @@ async function getLicenseFromLocalOrMaster(opensearchClient: OpenSearchClient) { } export const getLocalLicense: LicenseGetter = async (clustersDetails, { opensearchClient }) => { - const license = await getLicenseFromLocalOrMaster(opensearchClient); + const license = await getLicenseFromLocalOrClusterManager(opensearchClient); // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); }; diff --git a/src/plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts index 5364be4df6de..0ef9c16f617d 100644 --- a/src/plugins/vis_default_editor/public/components/agg_common_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_common_props.ts @@ -43,7 +43,7 @@ export interface DefaultEditorCommonProps { formIsTouched: boolean; groupName: AggGroupName; metricAggs: IAggConfig[]; - state: EditorVisState; + state: Partial; setAggParamValue: ( aggId: AggId, paramName: T, diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 8255c2945411..a16071f51c08 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -51,7 +51,7 @@ interface ParamInstanceBase { agg: IAggConfig; editorConfig: EditorConfig; metricAggs: IAggConfig[]; - state: EditorVisState; + state: Partial; schemas: Schema[]; hideCustomLabel?: boolean; } diff --git a/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index f41b07698df8..7d2f1cd0a9cd 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -57,6 +57,7 @@ const createEditorStateReducer = ({ !state.data.aggs!.aggs.find((agg) => agg.schema === schema.name) && schema.defaults ? (schema as any).defaults.slice(0, schema.max) : { schema: schema.name }; + const aggConfig = state.data.aggs!.createAggConfig(defaultConfig, { addToAggConfigs: false, }); diff --git a/src/plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts index af0b54956a97..88769ac71f9f 100644 --- a/src/plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -31,6 +31,7 @@ export { DefaultEditorController } from './default_editor_controller'; export { useValidation } from './components/controls/utils'; export { RangesParamEditor, RangeValues } from './components/controls/ranges'; +export { DefaultEditorAggParams } from './components/agg_params'; export * from './editor_size'; export * from './vis_options_props'; export * from './utils'; diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx index 76533eae4da3..02f7b6cafb45 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -97,7 +97,7 @@ function MetricVisOptions({ ); const setColorMode: EuiButtonGroupProps['onChange'] = useCallback( - (id) => setMetricValue('metricColorMode', id as ColorModes), + (id: string) => setMetricValue('metricColorMode', id as ColorModes), [setMetricValue] ); diff --git a/src/plugins/vis_type_metric/public/index.ts b/src/plugins/vis_type_metric/public/index.ts index 428b40f24acd..1b90e139b03c 100644 --- a/src/plugins/vis_type_metric/public/index.ts +++ b/src/plugins/vis_type_metric/public/index.ts @@ -34,3 +34,6 @@ import { MetricVisPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } + +/* Public Types */ +export { MetricVisExpressionFunctionDefinition } from './metric_vis_fn'; diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts index fcdb10b74e23..e03571089fa1 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -54,7 +54,7 @@ interface Arguments { colorRange: Range[]; font: Style; metric: any[]; // these aren't typed yet - bucket: any; // these aren't typed yet + bucket?: any; // these aren't typed yet } export interface MetricVisRenderValue { diff --git a/src/plugins/vis_type_timeline/public/timeline_vis_type.tsx b/src/plugins/vis_type_timeline/public/timeline_vis_type.tsx index b182e60a29a6..d3a076add8a5 100644 --- a/src/plugins/vis_type_timeline/public/timeline_vis_type.tsx +++ b/src/plugins/vis_type_timeline/public/timeline_vis_type.tsx @@ -51,7 +51,7 @@ export function getTimelineVisDefinition(dependencies: TimelineVisDependencies) return { name: TIMELINE_VIS_NAME, title: 'Timeline', - icon: 'visTimelion', + icon: 'timeline', description: i18n.translate('timeline.timelineDescription', { defaultMessage: 'Build time-series using functional expressions', }), diff --git a/src/plugins/vis_type_timeline/server/index.ts b/src/plugins/vis_type_timeline/server/index.ts index fb1118250bfa..9be2c2083414 100644 --- a/src/plugins/vis_type_timeline/server/index.ts +++ b/src/plugins/vis_type_timeline/server/index.ts @@ -39,7 +39,7 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ renameFromRoot, renameFromRootWithoutMap }) => [ // timelion.enabled and timelion_vis.enabled deprecation renameFromRoot('timelion.enabled', 'vis_type_timeline.enabled'), renameFromRoot('timelion_vis.enabled', 'vis_type_timeline.enabled'), @@ -61,6 +61,11 @@ export const config: PluginConfigDescriptor = { renameFromRoot('timelion.ui.enabled', 'vis_type_timeline.ui.enabled', true), renameFromRoot('vis_type_timelion.ui.enabled', 'vis_type_timeline.ui.enabled', true), renameFromRoot('timeline.ui.enabled', 'vis_type_timeline.ui.enabled', true), + + renameFromRootWithoutMap( + 'vis_type_timeline.graphiteBlockedIPs', + 'vis_type_timeline.graphiteDeniedIPs' + ), ], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/src/plugins/vis_type_timeline/server/lib/config_manager.ts b/src/plugins/vis_type_timeline/server/lib/config_manager.ts index 8d5e9edc0eb6..048db67422a6 100644 --- a/src/plugins/vis_type_timeline/server/lib/config_manager.ts +++ b/src/plugins/vis_type_timeline/server/lib/config_manager.ts @@ -36,7 +36,6 @@ export class ConfigManager { private opensearchShardTimeout: number = 0; private graphiteAllowedUrls: string[] = []; private graphiteBlockedIPs: string[] = []; - constructor(config: PluginInitializerContext['config']) { config.create>().subscribe((configUpdate) => { this.graphiteAllowedUrls = configUpdate.graphiteAllowedUrls || []; diff --git a/src/plugins/vis_type_timeline/server/series_functions/fixtures/tl_config.js b/src/plugins/vis_type_timeline/server/series_functions/fixtures/tl_config.js index 55a1f644936c..e9fc8d4c1487 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/fixtures/tl_config.js +++ b/src/plugins/vis_type_timeline/server/series_functions/fixtures/tl_config.js @@ -54,6 +54,7 @@ export default function () { opensearchShardTimeout: moment.duration(30000), allowedGraphiteUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'], + blockedGraphiteIPs: [], }); diff --git a/src/plugins/vis_type_timeline/server/series_functions/graphite.test.js b/src/plugins/vis_type_timeline/server/series_functions/graphite.test.js index e5ef1c987992..e251f3963a17 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/graphite.test.js +++ b/src/plugins/vis_type_timeline/server/series_functions/graphite.test.js @@ -90,7 +90,17 @@ describe('graphite', function () { }); }); - it('should return error message if both allowlist and blocklist are disabled', function () { + it('should return error message if both allowlist and blockedlist are disabled', function () { + return invoke(fn, [], { + settings: { 'timeline:graphite.url': 'http://127.0.0.1' }, + allowedGraphiteUrls: [], + blockedGraphiteIPs: [], + }).catch((e) => { + expect(e.message).to.eql(MISS_CHECKLIST_MESSAGE); + }); + }); + + it('should return error message if both allowlist and denylist are disabled', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'http://127.0.0.1' }, allowedGraphiteUrls: [], @@ -122,7 +132,7 @@ describe('graphite', function () { }); }); - it('setting with matched blocklist url should return error message', function () { + it('setting with matched denylist url should return error message', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'http://127.0.0.1' }, allowedGraphiteUrls: [], @@ -132,7 +142,7 @@ describe('graphite', function () { }); }); - it('setting with matched blocklist localhost should return error message', function () { + it('setting with matched denylist localhost should return error message', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'http://localhost' }, allowedGraphiteUrls: [], @@ -142,7 +152,7 @@ describe('graphite', function () { }); }); - it('setting with unmatched blocklist https url should return result', function () { + it('setting with unmatched denylist https url should return result', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'https://opensearch.org/' }, allowedGraphiteUrls: [], @@ -152,7 +162,7 @@ describe('graphite', function () { }); }); - it('setting with unmatched blocklist ftp url should return result', function () { + it('setting with unmatched denylist ftp url should return result', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'ftp://www.opensearch.org' }, allowedGraphiteUrls: [], @@ -182,7 +192,7 @@ describe('graphite', function () { }); }); - it('with both allowlist and blocklist, setting not in blocklist but in allowlist should return result', function () { + it('with both allowlist and denylist, setting not in denylist but in allowlist should return result', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', @@ -194,7 +204,7 @@ describe('graphite', function () { }); }); - it('with conflict allowlist and blocklist, setting in blocklist and in allowlist should return error message', function () { + it('with conflict allowlist and denylist, setting in denylist and in allowlist should return error message', function () { return invoke(fn, [], { settings: { 'timeline:graphite.url': 'http://127.0.0.1', diff --git a/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.js b/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.js index ceb19a0f819e..ace4987eb3b5 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.js +++ b/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.js @@ -26,16 +26,16 @@ function getIpAddress(urlObject) { return null; } /** - * Check whether customer input URL is blocked + * Check whether customer input URL is denied * This function first check the format of URL, URL has be in the format as * scheme://server/path/resource otherwise an TypeError would be thrown * Then IPCIDR check if a specific IP address fall in the * range of an IP address block * @param {string} configuredUrls - * @param {Array|string} blockedIPs - * @returns {boolean} true if the configuredUrl is blocked + * @param {Array|string} deniedIPs + * @returns {boolean} true if the configuredUrl is denied */ -function isBlockedURL(configuredUrl, blockedIPs) { +function isDeniedURL(configuredUrl, deniedIPs) { let configuredUrlObject; try { configuredUrlObject = new URL(configuredUrl); @@ -46,28 +46,28 @@ function isBlockedURL(configuredUrl, blockedIPs) { if (!ip) { return true; } - const isBlocked = blockedIPs.some((blockedIP) => new IPCIDR(blockedIP).contains(ip)); - return isBlocked; + const isDenied = deniedIPs.some((deniedIP) => new IPCIDR(deniedIP).contains(ip)); + return isDenied; } /** - * Check configured url using blocklist and allowlist + * Check configured url using denylist and allowlist * If allowlist is used, return false if allowlist does not contain configured url - * If blocklist is used, return false if blocklist contains configured url - * If both allowlist and blocklist are used, check blocklist first then allowlist - * @param {Array|string} blockedIPs + * If denylist is used, return false if denylist contains configured url + * If both allowlist and denylist are used, check denylist first then allowlist + * @param {Array|string} deniedIPs * @param {Array|string} allowedUrls * @param {string} configuredUrls * @returns {boolean} true if the configuredUrl is valid */ -function isValidConfig(blockedIPs, allowedUrls, configuredUrl) { - if (blockedIPs.length === 0) { +function isValidConfig(deniedIPs, allowedUrls, configuredUrl) { + if (deniedIPs.length === 0) { if (!allowedUrls.includes(configuredUrl)) return false; } else if (allowedUrls.length === 0) { - if (exports.isBlockedURL(configuredUrl, blockedIPs)) return false; + if (exports.isDeniedURL(configuredUrl, deniedIPs)) return false; } else { - if (exports.isBlockedURL(configuredUrl, blockedIPs) || !allowedUrls.includes(configuredUrl)) + if (exports.isDeniedURL(configuredUrl, deniedIPs) || !allowedUrls.includes(configuredUrl)) return false; } return true; } -export { getIpAddress, isBlockedURL, isValidConfig }; +export { getIpAddress, isDeniedURL, isValidConfig }; diff --git a/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.test.js b/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.test.js index 20053c7bc212..9c1ca1a9c176 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.test.js +++ b/src/plugins/vis_type_timeline/server/series_functions/helpers/graphite_helper.test.js @@ -6,32 +6,32 @@ import * as helper from './graphite_helper'; describe('graphite_helper', function () { - it('valid Url should not be blocked and isBlockedURL should return false', function () { - expect(helper.isBlockedURL('https://opensearch.org', ['127.0.0.0/8'])).toEqual(false); + it('valid Url should not be blocked and isDeniedURL should return false', function () { + expect(helper.isDeniedURL('https://opensearch.org', ['127.0.0.0/8'])).toEqual(false); }); - it('blocked Url should be blocked and isBlockedURL should return true', function () { - expect(helper.isBlockedURL('https://127.0.0.1', ['127.0.0.0/8'])).toEqual(true); + it('blocked Url should be blocked and isDeniedURL should return true', function () { + expect(helper.isDeniedURL('https://127.0.0.1', ['127.0.0.0/8'])).toEqual(true); }); - it('invalid Url should be blocked and isBlockedURL should return true', function () { - expect(helper.isBlockedURL('www.opensearch.org', ['127.0.0.0/8'])).toEqual(true); + it('invalid Url should be blocked and isDeniedURL should return true', function () { + expect(helper.isDeniedURL('www.opensearch.org', ['127.0.0.0/8'])).toEqual(true); }); - it('blocklist should be checked if blocklist is enabled', function () { - jest.spyOn(helper, 'isBlockedURL').mockReturnValueOnce(false); + it('denylist should be checked if denylist is enabled', function () { + jest.spyOn(helper, 'isDeniedURL').mockReturnValueOnce(false); helper.isValidConfig(['127.0.0.0/8'], [], 'https://opensearch.org'); - expect(helper.isBlockedURL).toBeCalled(); + expect(helper.isDeniedURL).toBeCalled(); }); - it('blocklist should be checked it both allowlist and blocklist are enabled', function () { - jest.spyOn(helper, 'isBlockedURL').mockReturnValueOnce(false); + it('denylist should be checked it both allowlist and denylist are enabled', function () { + jest.spyOn(helper, 'isDeniedURL').mockReturnValueOnce(false); helper.isValidConfig( ['127.0.0.0/8'], ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'], 'https://opensearch.org' ); - expect(helper.isBlockedURL).toBeCalled(); + expect(helper.isDeniedURL).toBeCalled(); }); it('with only allowlist, isValidConfig should return false for Url not in the allowlist', function () { @@ -54,15 +54,15 @@ describe('graphite_helper', function () { ).toEqual(true); }); - it('with only blocklist, isValidConfig should return false for Url in the blocklist', function () { + it('with only denylist, isValidConfig should return false for Url in the denylist', function () { expect(helper.isValidConfig(['127.0.0.0/8'], [], 'https://127.0.0.1')).toEqual(false); }); - it('with only blocklist, isValidConfig should return true for Url not in the blocklist', function () { + it('with only denylist, isValidConfig should return true for Url not in the denylist', function () { expect(helper.isValidConfig(['127.0.0.0/8'], [], 'https://opensearch.org')).toEqual(true); }); - it('with both blocklist and allowlist, isValidConfig should return false if allowlist check fails', function () { + it('with both denylist and allowlist, isValidConfig should return false if allowlist check fails', function () { expect( helper.isValidConfig( ['127.0.0.0/8'], @@ -72,7 +72,7 @@ describe('graphite_helper', function () { ).toEqual(false); }); - it('with both blocklist and allowlist, isValidConfig should return false if blocklist check fails', function () { + it('with both denylist and allowlist, isValidConfig should return false if denylist check fails', function () { expect( helper.isValidConfig( ['127.0.0.0/8'], @@ -82,7 +82,7 @@ describe('graphite_helper', function () { ).toEqual(false); }); - it('with conflict blocklist and allowlist, isValidConfig should return false if blocklist check fails', function () { + it('with conflict denylist and allowlist, isValidConfig should return false if denylist check fails', function () { expect( helper.isValidConfig(['127.0.0.0/8'], ['https://127.0.0.1'], 'https://127.0.0.1') ).toEqual(false); diff --git a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts index a153c4329a5b..d9e0c6a80e32 100644 --- a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts +++ b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts @@ -37,19 +37,25 @@ import { PANEL_TYPES } from './panel_types'; */ export enum RESTRICTIONS_KEYS { /** - * Key for getting the white listed group by fields from the UIRestrictions object. + * Key for getting the allow listed group by fields from the UIRestrictions object. */ + /** @deprecated use ALLOW_LISTED_GROUP_BY_FIELDS*/ WHITE_LISTED_GROUP_BY_FIELDS = 'whiteListedGroupByFields', + ALLOW_LISTED_GROUP_BY_FIELDS = 'allowListedGroupByFields', /** - * Key for getting the white listed metrics from the UIRestrictions object. + * Key for getting the allow listed metrics from the UIRestrictions object. */ + /** @deprecated use ALLOW_LISTED_METRICS*/ WHITE_LISTED_METRICS = 'whiteListedMetrics', + ALLOW_LISTED_METRICS = 'allowListedMetrics', /** - * Key for getting the white listed Time Range modes from the UIRestrictions object. + * Key for getting the allow listed Time Range modes from the UIRestrictions object. */ + /** @deprecated use ALLOW_LISTED_TIMERANGE_MODES*/ WHITE_LISTED_TIMERANGE_MODES = 'whiteListedTimerangeModes', + ALLOW_LISTED_TIMERANGE_MODES = 'allowListedTimerangeModes', } export interface UIRestrictions { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 0fd58769ea76..db0dc4de77a6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -212,7 +212,6 @@ export class TimeseriesVisualization extends Component { seriesDataRow.groupId = groupId; seriesDataRow.yScaleType = yScaleType; seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); - seriesDataRow.useDefaultGroupDomain = !isCustomDomain; }); if (isCustomDomain) { diff --git a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js index e2128d77c758..379643fcc726 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/check_ui_restrictions.js @@ -53,7 +53,7 @@ const checkUIRestrictions = (key, restrictions = DEFAULT_UI_RESTRICTION, type) = * @return {boolean} */ export const isMetricEnabled = (key, restrictions) => { - return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_METRICS); + return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.ALLOW_LISTED_METRICS); }; /** @@ -69,7 +69,7 @@ export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_REST if (isMetricEnabled(metricType, restrictions)) { return checkUIRestrictions( field, - restrictions[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS], + restrictions[RESTRICTIONS_KEYS.ALLOW_LISTED_METRICS], metricType ); } @@ -86,7 +86,7 @@ export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_REST * @return {boolean} */ export const isGroupByFieldsEnabled = (key, restrictions) => { - return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS); + return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.ALLOW_LISTED_GROUP_BY_FIELDS); }; /** @@ -99,5 +99,5 @@ export const isGroupByFieldsEnabled = (key, restrictions) => { * @return {boolean} */ export const isTimerangeModeEnabled = (key, restrictions) => { - return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES); + return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.ALLOW_LISTED_TIMERANGE_MODES); }; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js index 7807d0fb54c3..85c1de00557e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js @@ -49,23 +49,41 @@ export class DefaultSearchCapabilities { return null; } + /** @deprecated use allowListedMetrics*/ get whiteListedMetrics() { return this.createUiRestriction(); } + /** @deprecated use allowListedGroupByFields*/ get whiteListedGroupByFields() { return this.createUiRestriction(); } + /** @deprecated use allowListedTimerangeMode*/ get whiteListedTimerangeModes() { return this.createUiRestriction(); } + get allowListedMetrics() { + return this.createUiRestriction(); + } + + get allowListedGroupByFields() { + return this.createUiRestriction(); + } + + get allowListedTimerangeModes() { + return this.createUiRestriction(); + } + get uiRestrictions() { return { [RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics, [RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields, [RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes, + [RESTRICTIONS_KEYS.ALLOW_LISTED_METRICS]: this.allowListedMetrics, + [RESTRICTIONS_KEYS.ALLOW_LISTED_GROUP_BY_FIELDS]: this.allowListedGroupByFields, + [RESTRICTIONS_KEYS.ALLOW_LISTED_TIMERANGE_MODES]: this.allowListedTimerangeModes, }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js index b318a06755f0..0bfe242a0d08 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js @@ -53,6 +53,9 @@ describe('DefaultSearchCapabilities', () => { whiteListedMetrics: { '*': true }, whiteListedGroupByFields: { '*': true }, whiteListedTimerangeModes: { '*': true }, + allowListedMetrics: { '*': true }, + allowListedGroupByFields: { '*': true }, + allowListedTimerangeModes: { '*': true }, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index 3dc47dbaccd3..cb8fc3ccdfcf 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -74,8 +74,6 @@ export function dateHistogram( ? getDateHistogramForLastBucketMode() : getDateHistogramForEntireTimerangeMode(); - // master - overwrite(doc, `aggs.${series.id}.meta`, { timeField, intervalString, diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 0a4eb34c41e5..4dcf079c5aab 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -58,7 +58,7 @@ export const visDataRoutes = ( (typeof request.body === 'object' && (request.body as any).savedObjectId) || 'unavailable'; framework.logger.warn( - `Request validation error: ${error.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/opensearch-dashboards/issues/new?template=Bug_report.md` + `Request validation error: ${error.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new?template=bug_template.md` ); } diff --git a/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx b/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx deleted file mode 100644 index 1ba1ede5670f..000000000000 --- a/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { parse } from 'hjson'; -import React from 'react'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; -import { Vis } from '../../../visualizations/public'; - -function ExperimentalMapLayerInfo() { - const title = ( - - GitHub - - ), - }} - /> - ); - - return ( - - ); -} - -export const getInfoMessage = (vis: Vis) => { - if (vis.params.spec) { - try { - const spec = parse(vis.params.spec, { legacyRoot: false, keepWsc: true }); - - if (spec.config?.kibana?.type === 'map') { - return ; - } - } catch (e) { - // spec is invalid - } - } - - return null; -}; diff --git a/src/plugins/vis_type_vega/public/data_model/utils.test.js b/src/plugins/vis_type_vega/public/data_model/utils.test.js new file mode 100644 index 000000000000..9fe648c71139 --- /dev/null +++ b/src/plugins/vis_type_vega/public/data_model/utils.test.js @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Utils } from './utils'; + +describe('Utils.handleNonStringIndex', () => { + test('should return same string on string input', async () => { + expect(Utils.handleNonStringIndex('*')).toBe('*'); + }); + + test('should return empty string on empty string input', async () => { + expect(Utils.handleNonStringIndex('')).toBe(''); + }); + + test('should return undefined on non-string input', async () => { + expect(Utils.handleNonStringIndex(123)).toBe(undefined); + expect(Utils.handleNonStringIndex(null)).toBe(undefined); + expect(Utils.handleNonStringIndex(undefined)).toBe(undefined); + }); +}); + +describe('Utils.handleInvalidDate', () => { + test('should return null if passed timestamp is Not A Number', async () => { + expect(Utils.handleInvalidDate(Number.NaN)).toBe(null); + }); + + test('should return timestamp if passed timestamp is valid Number', async () => { + expect(Utils.handleInvalidDate(1658189958487)).toBe(1658189958487); + }); + + test('should return string on string input', async () => { + expect(Utils.handleInvalidDate('Sat Jul 16 2022 22:59:42')).toBe('Sat Jul 16 2022 22:59:42'); + }); + + test('should return date on date input', async () => { + const date = Date.now(); + expect(Utils.handleInvalidDate(date)).toBe(date); + }); + + test('should return null if input neigther timestamp, nor date, nor string', async () => { + expect(Utils.handleInvalidDate(undefined)).toBe(null); + expect(Utils.handleInvalidDate({ key: 'value' })).toBe(null); + }); +}); + +describe('Utils.handleInvalidQuery', () => { + test('should return valid object on valid DSL query', async () => { + const testQuery = { match_phrase: { customer_gender: 'MALE' } }; + expect(Utils.handleInvalidQuery(testQuery)).toStrictEqual(testQuery); + }); + + test('should return null on null or undefined input', async () => { + expect(Utils.handleInvalidQuery(null)).toBe(null); + expect(Utils.handleInvalidQuery(undefined)).toBe(null); + }); + + test('should return null if input object has function as property', async () => { + const input = { + key1: 'value1', + key2: () => { + alert('Hello!'); + }, + }; + + expect(Utils.handleInvalidQuery(input)).toBe(null); + }); + + test('should return null if nested object has function as property', async () => { + const input = { + key1: 'value1', + key2: { + func: () => { + alert('Hello!'); + }, + }, + }; + expect(Utils.handleInvalidQuery(input)).toBe(null); + }); + + test('should throw error on polluted query', async () => { + const maliciousQueries = [ + JSON.parse(`{ "__proto__": null }`), + JSON.parse(`{ "constructor": { "prototype" : null } }`), + ]; + + maliciousQueries.forEach((value) => { + expect(() => { + Utils.handleInvalidQuery(value); + }).toThrowError(); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/data_model/utils.ts b/src/plugins/vis_type_vega/public/data_model/utils.ts index f363740f536d..4e7a85062354 100644 --- a/src/plugins/vis_type_vega/public/data_model/utils.ts +++ b/src/plugins/vis_type_vega/public/data_model/utils.ts @@ -29,6 +29,7 @@ */ import compactStringify from 'json-stringify-pretty-compact'; +import { validateObject } from '@osd/std'; export class Utils { /** @@ -59,4 +60,41 @@ export class Utils { } return Utils.formatWarningToStr(error, ...Array.from(args).slice(1)); } + + static handleNonStringIndex(index: unknown): string | undefined { + return typeof index === 'string' ? index : undefined; + } + + static handleInvalidQuery(query: unknown): object | null { + if (Utils.isObject(query) && !Utils.checkForFunctionProperty(query as object)) { + // Validating object against prototype pollution + validateObject(query); + return JSON.parse(JSON.stringify(query)); + } + return null; + } + + static isObject(object: unknown): boolean { + return !!(object && typeof object === 'object' && !Array.isArray(object)); + } + + static checkForFunctionProperty(object: object): boolean { + let result = false; + Object.values(object).forEach((value) => { + result = + typeof value === 'function' + ? true + : Utils.isObject(value) && Utils.checkForFunctionProperty(value); + }); + return result; + } + + static handleInvalidDate(date: unknown): number | string | Date | null { + if (typeof date === 'number') { + return !isNaN(date) ? date : null; + } else if (date instanceof Date || typeof date === 'string') { + return date; + } + return null; + } } diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js index 73d1af5d30af..1c3068bae9ec 100644 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ b/src/plugins/vis_type_vega/public/lib/vega.js @@ -30,5 +30,6 @@ import * as vegaLite from 'vega-lite/build-es5/vega-lite'; import * as vega from 'vega/build-es5/vega'; +import { expressionInterpreter as vegaExpressionInterpreter } from 'vega-interpreter/build/vega-interpreter.module'; -export { vega, vegaLite }; +export { vega, vegaLite, vegaExpressionInterpreter }; diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index ab6cae0024b0..d241b66d472c 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -57,3 +57,4 @@ export const [getMapsLegacyConfig, setMapsLegacyConfig] = createGetterSetter getInjectedVars().enableExternalUrls; export const getEmsTileLayerId = () => getMapsLegacyConfig().emsTileLayerId; +export const getShowRegionDeniedWarning = () => getMapsLegacyConfig().showRegionDeniedWarning; diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 7f4a422f3b1a..03fc05fdee89 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -45,8 +45,6 @@ import { getDefaultSpec } from './default_spec'; import { createInspectorAdapters } from './vega_inspector'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; -import { getInfoMessage } from './components/experimental_map_vis_info'; - export const createVegaTypeDefinition = ( dependencies: VegaVisualizationDependencies ): BaseVisTypeOptions => { @@ -56,7 +54,6 @@ export const createVegaTypeDefinition = ( return { name: 'vega', title: 'Vega', - getInfoMessage, description: i18n.translate('visTypeVega.type.vegaDescription', { defaultMessage: 'Create custom visualizations using Vega and Vega-Lite', description: 'Vega and Vega-Lite are product names and should not be translated', diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 6040c8ffb2d3..1286495af901 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -31,7 +31,7 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { vega, vegaLite } from '../lib/vega'; +import { vega, vegaLite, vegaExpressionInterpreter } from '../lib/vega'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@osd/i18n'; @@ -79,6 +79,7 @@ export class VegaBaseView { this._timefilter = opts.timefilter; this._view = null; this._vegaViewConfig = null; + this._vegaViewOptions = null; this._$messages = null; this._destroyHandlers = []; this._initialized = false; @@ -130,6 +131,7 @@ export class VegaBaseView { }); this._vegaViewConfig = this.createViewConfig(); + this._vegaViewOptions = { ast: true }; // The derived class should create this method await this._initViewCustomizations(); @@ -185,6 +187,7 @@ export class VegaBaseView { // eslint-disable-next-line import/namespace logLevel: vega.Warn, // note: eslint has a false positive here renderer: this._parser.renderer, + expr: vegaExpressionInterpreter, }; // Override URL sanitizer to prevent external data loading (if disabled) @@ -310,23 +313,25 @@ export class VegaBaseView { } /** - * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor + * @param {object} query Query DSL snippet, as used in the query DSL editor * @param {string} [index] as defined in OpenSearch Dashboards, or default if missing */ async addFilterHandler(query, index) { - const indexId = await this.findIndex(index); - const filter = opensearchFilters.buildQueryFilter(query, indexId); - + const indexId = await this.findIndex(Utils.handleNonStringIndex(index)); + const filter = opensearchFilters.buildQueryFilter(Utils.handleInvalidQuery(query), indexId); this._applyFilter({ filters: [filter] }); } /** - * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor + * @param {object} query Query DSL snippet, as used in the query DSL editor * @param {string} [index] as defined in OpenSearch Dashboards, or default if missing */ async removeFilterHandler(query, index) { - const indexId = await this.findIndex(index); - const filterToRemove = opensearchFilters.buildQueryFilter(query, indexId); + const indexId = await this.findIndex(Utils.handleNonStringIndex(index)); + const filterToRemove = opensearchFilters.buildQueryFilter( + Utils.handleInvalidQuery(query), + indexId + ); const currentFilters = this._filterManager.getFilters(); const existingFilter = currentFilters.find((filter) => @@ -352,7 +357,10 @@ export class VegaBaseView { * @param {number|string|Date} end */ setTimeFilterHandler(start, end) { - const { from, to, mode } = VegaBaseView._parseTimeRange(start, end); + const { from, to, mode } = VegaBaseView._parseTimeRange( + Utils.handleInvalidDate(start), + Utils.handleInvalidDate(end) + ); this._applyFilter({ timeFieldName: '*', diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 2a604f8d834f..6f0a961f7a62 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -32,7 +32,7 @@ import { i18n } from '@osd/i18n'; import { vega } from '../lib/vega'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { getEmsTileLayerId, getUISettings } from '../services'; +import { getEmsTileLayerId, getShowRegionDeniedWarning, getUISettings } from '../services'; import { lazyLoadMapsLegacyModules } from '../../../maps_legacy/public'; export class VegaMapView extends VegaBaseView { @@ -58,7 +58,7 @@ export class VegaMapView extends VegaBaseView { baseMapOpts = { ...baseMapOpts, ...(await this._serviceSettings.getAttributesForTMSLayer(baseMapOpts, true, isDarkMode)), - showRegionBlockedWarning: this._serviceSettings._mapConfig.showRegionBlockedWarning, + showRegionDeniedWarning: getShowRegionDeniedWarning(), }; if (!baseMapOpts) { this.onWarn( @@ -144,6 +144,7 @@ export class VegaMapView extends VegaBaseView { bindingsContainer: this._$controls.get(0), delayRepaint: mapConfig.delayRepaint, viewConfig: this._vegaViewConfig, + viewOptions: this._vegaViewOptions, onWarning: this.onWarn.bind(this), onError: this.onError.bind(this), }, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 50119439f680..45d15849ed47 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -36,7 +36,10 @@ export class VegaView extends VegaBaseView { // In some cases, Vega may be initialized twice... TBD if (!this._$container) return; - const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); + const view = new vega.View( + vega.parse(this._parser.spec, null, this._vegaViewOptions), + this._vegaViewConfig + ); view.warn = this.onWarn.bind(this); view.error = this.onError.bind(this); diff --git a/src/plugins/vis_type_vislib/public/index.ts b/src/plugins/vis_type_vislib/public/index.ts index 525be8eaf829..1b13f06ca6d2 100644 --- a/src/plugins/vis_type_vislib/public/index.ts +++ b/src/plugins/vis_type_vislib/public/index.ts @@ -35,4 +35,6 @@ export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } +export { getConfigCollections } from './utils/collections'; + export * from './types'; diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts index 3416a1ee3c4a..3267f04db87b 100644 --- a/src/plugins/vis_type_vislib/public/types.ts +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -106,3 +106,5 @@ export interface BasicVislibParams extends CommonVislibParams { times: TimeMarker[]; type: string; } + +export { Positions }; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/binder.ts b/src/plugins/vis_type_vislib/public/vislib/lib/binder.ts index 33ef39590d35..0dc7fbb6f537 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/binder.ts +++ b/src/plugins/vis_type_vislib/public/vislib/lib/binder.ts @@ -30,7 +30,6 @@ import d3 from 'd3'; import $ from 'jquery'; -import { IScope } from 'angular'; export interface Emitter { on: (...args: any[]) => void; @@ -42,13 +41,6 @@ export interface Emitter { export class Binder { private disposal: Array<() => void> = []; - constructor($scope: IScope) { - // support auto-binding to $scope objects - if ($scope) { - $scope.$on('$destroy', () => this.destroy()); - } - } - public on(emitter: Emitter, ...args: any[]) { const on = emitter.on || emitter.addListener; const off = emitter.off || emitter.removeListener; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 4eda49da6bec..46d3b3dd7d03 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -43,7 +43,7 @@ export { Vis } from './vis'; export { TypesService } from './vis_types/types_service'; export { VISUALIZE_EMBEDDABLE_TYPE, VIS_EVENT_TO_TRIGGER } from './embeddable'; export { VisualizationContainer, VisualizationNoResults } from './components'; -export { getSchemas as getVisSchemas } from './legacy/build_pipeline'; +export { getSchemas as getVisSchemas, buildVislibDimensions } from './legacy/build_pipeline'; /** @public types */ export { VisualizationsSetup, VisualizationsStart }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index e0f2ee7e1cca..7a28ae7ac394 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -373,7 +373,8 @@ export const buildVislibDimensions = async (vis: any, params: BuildPipelineParam splitColumn: schemas.split_column, }; if (schemas.segment) { - const xAgg = vis.data.aggs.getResponseAggs()[dimensions.x.accessor]; + const a = vis.data.aggs.getResponseAggs(); + const xAgg = a[dimensions.x.accessor]; if (xAgg.type.name === 'date_histogram') { dimensions.x.params.date = true; const { opensearchUnit, opensearchValue } = xAgg.buckets.getInterval(); @@ -423,10 +424,10 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { // request handler if (vis.type.requestHandler === 'courier') { pipeline += `opensearchaggs - ${prepareString('index', indexPattern!.id)} - metricsAtAllLevels=${vis.isHierarchical()} - partialRows=${vis.params.showPartialRows || false} - ${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `; + ${prepareString('index', indexPattern!.id)} + metricsAtAllLevels=${vis.isHierarchical()} + partialRows=${vis.params.showPartialRows || false} + ${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `; } const schemas = getSchemas(vis, params); @@ -444,8 +445,10 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { } else { const visConfig = { ...vis.params }; visConfig.dimensions = schemas; + visConfig.title = title; pipeline += `visualization type='${vis.type.name}' ${prepareJson('visConfig', visConfig)} + ${prepareJson('uiState', uiState)} metricsAtAllLevels=${vis.isHierarchical()} partialRows=${vis.params.showPartialRows || false} `; if (indexPattern) { @@ -456,5 +459,6 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { } } } + return pipeline; }; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 0d196482be37..7f5323cee06a 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -28,6 +28,7 @@ * under the License. */ +import { SavedObjectAttributes } from 'opensearch-dashboards/public'; import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisualizationListItem { @@ -51,7 +52,7 @@ export interface VisualizationsAppExtension { toListItem: (savedObject: { id: string; type: string; - attributes: object; + attributes: SavedObjectAttributes; }) => VisualizationListItem; } diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 0c72df9a5fe5..2685e7cc8d21 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -264,10 +264,9 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
-

-

-

-

- - + + + +

- - + + + +

- - + + + +

- - + + + +

-

-

-

-

- - + + + +

- - + + + +

- - + + + +

- - + + + +

{ - // Filter out all lab visualizations if lab mode is not enabled + const filterExperimental = (type: VisType | VisTypeAlias): boolean => { if (!this.props.showExperimental && type.stage === 'experimental') { return false; } - - // Filter out hidden visualizations - if (type.hidden) { - return false; - } - return true; - }); + }; - const allTypes = [...types, ...visTypes.getAliases()]; + const types = visTypes + .all() + .filter(filterExperimental) + .filter((type) => !type.hidden); // Filter out hidden visualizations + const aliasedTypes = visTypes.getAliases().filter(filterExperimental); + const allTypes = [...types, ...aliasedTypes]; let entries: VisTypeListEntry[]; if (!query) { @@ -222,7 +220,7 @@ class TypeSelection extends React.Component { let stage = {}; let highlightMsg; - if (!isVisTypeAlias(visType.type) && visType.type.stage === 'experimental') { + if (visType.type.stage === 'experimental') { stage = { betaBadgeLabel: i18n.translate('visualizations.newVisWizard.experimentalTitle', { defaultMessage: 'Experimental', @@ -277,8 +275,8 @@ class TypeSelection extends React.Component ); diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md new file mode 100755 index 000000000000..bb2d08fecbbb --- /dev/null +++ b/src/plugins/wizard/README.md @@ -0,0 +1,36 @@ +# Wizard + +An OpenSearch Dashboards plugin for a visualization experience that makes exploring data and creating visualizations much easier. It will act as an additional way to create visualizations alongside the exiting tools within the current visualizations plugin. The tool will be incremental to the visualization tools available to users in OpenSearch Dashboards today. + +## Usage + +To use this plugin, navigate to: + +Visualize -> Create Visualization -> Wizard + +## Add a visualization + +All new visualizations currently reside in [public/visualizations](./public/visualizations). To add a new one, create a new visualization directory and add the required code (below) to setup and register a new vis type. + +### Anatomy of a visualization + +``` +metric/ +├─ index.ts +├─ metric_viz_type.ts +├─ to_expression.ts +├─ components/ + ├─ metric_viz_options.tsx +``` + +Outline: +- `index.ts`: Exposes the `createConfig` function that is used to register the viz type +- `_viz_type.ts`: Contains the config that the type service needs to register the new vis type +- `to_expression.ts`: The expression function that the plugin will use to render the visualization given the state of the plugin +- `_viz_options.tsx`: The component that will render the other properties that user can set in the `Style` tab + +**Notes:** + +- Currently only the metric viz is defined, so schema properties that other vis types might need may be missing and require further setup. +- `to_expression` has not yet been abstracted into a common utility for different visualizations. Adding more visualization types should make it easier to identify which parts of expression creation are common, and which are visualization-specific. + diff --git a/src/plugins/wizard/common/index.ts b/src/plugins/wizard/common/index.ts new file mode 100644 index 000000000000..b55653d41549 --- /dev/null +++ b/src/plugins/wizard/common/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const PLUGIN_ID = 'wizard'; +export const PLUGIN_NAME = 'Wizard'; +export const VISUALIZE_ID = 'visualize'; +export const EDIT_PATH = '/edit'; + +export { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from './wizard_saved_object_attributes'; diff --git a/src/plugins/wizard/common/wizard_saved_object_attributes.ts b/src/plugins/wizard/common/wizard_saved_object_attributes.ts new file mode 100644 index 000000000000..e0e1e1cb6de7 --- /dev/null +++ b/src/plugins/wizard/common/wizard_saved_object_attributes.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from '../../../core/types'; + +export const WIZARD_SAVED_OBJECT = 'wizard'; + +export interface WizardSavedObjectAttributes extends SavedObjectAttributes { + title: string; + description?: string; + visualizationState?: string; + styleState?: string; + version: number; + searchSourceFields?: { + index?: string; + }; +} diff --git a/src/plugins/wizard/config.ts b/src/plugins/wizard/config.ts new file mode 100644 index 000000000000..79412f5c02ee --- /dev/null +++ b/src/plugins/wizard/config.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema, TypeOf } from '@osd/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json new file mode 100644 index 000000000000..ae088bfe4c8f --- /dev/null +++ b/src/plugins/wizard/opensearch_dashboards.json @@ -0,0 +1,23 @@ +{ + "id": "wizard", + "version": "1.0.0", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": [ + "navigation", + "charts", + "data", + "opensearchDashboardsReact", + "opensearchDashboardsUtils", + "savedObjects", + "embeddable", + "expressions", + "dashboard", + "visualizations", + "opensearchUiShared", + "visDefaultEditor", + "visTypeVislib" + ], + "optionalPlugins": [] +} diff --git a/src/plugins/wizard/public/application/_util.scss b/src/plugins/wizard/public/application/_util.scss new file mode 100644 index 000000000000..165879c2ab12 --- /dev/null +++ b/src/plugins/wizard/public/application/_util.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@mixin scrollNavParent($template-row: none) { + display: grid; + min-height: 0; + + @if $template-row != "none" { + grid-template-rows: $template-row; + } +} diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss new file mode 100644 index 000000000000..da530e2e46a4 --- /dev/null +++ b/src/plugins/wizard/public/application/_variables.scss @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "@elastic/eui/src/global_styling/variables/header"; +@import "@elastic/eui/src/global_styling/variables/form"; + +$osdHeaderOffset: $euiHeaderHeightCompensation; +$wizLeftNavWidth: 462px; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss new file mode 100644 index 000000000000..f4a73eb3ecd4 --- /dev/null +++ b/src/plugins/wizard/public/application/app.scss @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "variables"; + +.wizLayout { + padding: 0; + display: grid; + grid-template: + "topNav topNav" min-content + "leftNav workspaceNav" 1fr / #{$wizLeftNavWidth} 1fr; + height: calc(100vh - #{$osdHeaderOffset}); + + &__resizeContainer { + min-height: 0; + background-color: $euiColorEmptyShade; + } + + &__resizeButton { + transform: translateX(-$euiSizeM / 2); + } +} + +.headerIsExpanded .wizLayout { + height: calc(100vh - #{$osdHeaderOffset * 2}); +} diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx new file mode 100644 index 000000000000..52a0f537646e --- /dev/null +++ b/src/plugins/wizard/public/application/app.tsx @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { EuiPage, EuiResizableContainer } from '@elastic/eui'; +import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; +import { LeftNav } from './components/left_nav'; +import { TopNav } from './components/top_nav'; +import { Workspace } from './components/workspace'; +import './app.scss'; +import { RightNav } from './components/right_nav'; + +export const WizardApp = () => { + // Render the application DOM. + return ( + + + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + )} + + + + + ); +}; + +export { Option } from './components/option'; diff --git a/src/plugins/wizard/public/application/components/data_source_select.tsx b/src/plugins/wizard/public/application/components/data_source_select.tsx new file mode 100644 index 000000000000..c51aa40a08e2 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_source_select.tsx @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiIcon } from '@elastic/eui'; +import { SearchableDropdown, SearchableDropdownOption } from './searchable_dropdown'; +import { useIndexPatterns } from '../utils/use'; +import { useTypedDispatch } from '../utils/state_management'; +import { setIndexPattern } from '../utils/state_management/visualization_slice'; +import { IndexPattern } from '../../../../data/public'; + +function indexPatternEquality(A?: SearchableDropdownOption, B?: SearchableDropdownOption): boolean { + return !A || !B ? false : A.id === B.id; +} + +function toSearchableDropdownOption(indexPattern: IndexPattern): SearchableDropdownOption { + return { + id: indexPattern.id || '', + label: indexPattern.title, + searchableLabel: indexPattern.title, + prepend: , + }; +} + +export const DataSourceSelect = () => { + const { indexPatterns, loading, error, selected } = useIndexPatterns(); + const dispatch = useTypedDispatch(); + + // TODO: Should be a standard EUI component + return ( + { + const foundOption = indexPatterns.filter((s) => s.id === option.id)[0]; + if (foundOption !== undefined && typeof foundOption.id === 'string') { + dispatch(setIndexPattern(foundOption.id)); + } + }} + prepend={i18n.translate('wizard.nav.dataSource.selector.title', { + defaultMessage: 'Data Source', + })} + error={error} + loading={loading} + options={indexPatterns.map(toSearchableDropdownOption)} + equality={indexPatternEquality} + /> + ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss new file mode 100644 index 000000000000..6a7f4ac4fc78 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizConfig { + @include euiYScrollWithShadows; + + background: $euiColorLightestShade; + border-left: $euiBorderThin; + position: relative; + overflow-x: hidden; + + &__section { + width: 100%; + transition: transform $euiAnimSpeedNormal 0s $euiAnimSlightResistance; + } + + &__title { + padding: $euiSizeS; + padding-bottom: 0; + + &.showDivider { + border-bottom: 1px solid $euiColorLightShade; + } + } + + &__content { + padding: $euiSizeS; + } + + &__aggEditor { + padding: 0 $euiSizeM; + } + + &--secondary { + position: absolute; + top: 0; + left: 100%; + } + + &.showSecondary > .wizConfig__section { + transform: translateX(-100%); + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx new file mode 100644 index 000000000000..be0ec12bbff2 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiForm } from '@elastic/eui'; +import React from 'react'; +import { useVisualizationType } from '../../utils/use'; +import { useTypedSelector } from '../../utils/state_management'; +import './config_panel.scss'; +import { mapSchemaToAggPanel } from './schema_to_dropbox'; +import { SecondaryPanel } from './secondary_panel'; + +export function ConfigPanel() { + const vizType = useVisualizationType(); + const editingState = useTypedSelector( + (state) => state.visualization.activeVisualization?.draftAgg + ); + const schemas = vizType.ui.containerConfig.data.schemas; + + if (!schemas) return null; + + const mainPanel = mapSchemaToAggPanel(schemas); + + return ( + +

{mainPanel}
+ + + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.scss b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss new file mode 100644 index 000000000000..a2013e4a02e3 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss @@ -0,0 +1,106 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.dropBox { + margin-top: $euiSize; + border-bottom: $euiBorderThin; + padding-bottom: $euiSize; + + &:first-child { + margin-top: 0; + } + + &:last-child { + border-bottom: none; + } + + &__container { + display: grid; + grid-gap: $euiSizeXS / 2; + padding: $euiSizeS - ($euiSizeXS / 2) $euiSizeS $euiSizeS $euiSizeS; + background-color: $euiColorLightShade; + border-radius: $euiBorderRadius; + } + + &__field { + display: grid; + grid-template-columns: 1fr auto; + grid-gap: $euiSizeS; + padding: $euiSizeS $euiSizeM; + align-items: center; + } + + &__draggable { + padding: $euiSizeXS / 2 0; + animation: pop-in $euiAnimSpeedSlow $euiAnimSlightResistance forwards; + transform-origin: bottom; + + &.closing { + animation: pop-out $euiAnimSpeedSlow $euiAnimSlightResistance forwards; // Also update speed in dropbox.tsx + } + } + + &__field_text { + text-overflow: ellipsis; + overflow: hidden; + } + + &__dropTarget { + color: $euiColorDarkShade; + grid-template-columns: 1fr auto; + transform-origin: top; + animation: pop-in $euiAnimSpeedFast $euiAnimSlightResistance forwards; + + &.validField { + background-color: tintOrShade($euiColorPrimary, 80%, 70%); + border-color: tintOrShade($euiColorPrimary, 80%, 70%); + + &.canDrop { + background-color: tintOrShade($euiColorPrimary, 60%, 40%); + border-color: tintOrShade($euiColorPrimary, 30%, 20%); + border-style: dashed; + } + } + } +} + +@keyframes pop-in { + from { + max-height: 0; + opacity: 0; + } + + to { + max-height: 1000px; + opacity: 1; + } +} + +@keyframes pop-out { + from { + max-height: 1000px; + opacity: 1; + } + + to { + max-height: 0; + opacity: 0; + } +} + +@media (prefers-reduced-motion) { + .dropBox { + &__draggable { + animation: none; + + &.closing { + animation: none; + } + } + + &__dropTarget { + animation: none; + } + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx new file mode 100644 index 000000000000..f6b7a6ca221b --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFormRow, + EuiPanel, + EuiText, + DropResult, +} from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { IDropAttributes, IDropState } from '../../utils/drag_drop'; +import './dropbox.scss'; +import { useDropbox } from './use'; +import { UseDropboxProps } from './use/use_dropbox'; +import { usePrefersReducedMotion } from './use/use_prefers_reduced_motion'; + +export interface DropboxDisplay { + label: string; + id: string; +} + +interface DropboxProps extends IDropState { + id: string; + label: string; + fields: DropboxDisplay[]; + limit?: number; + onAddField: () => void; + onEditField: (id: string) => void; + onDeleteField: (id: string) => void; + onReorderField: ({ + sourceAggId, + destinationAggId, + }: { + sourceAggId: string; + destinationAggId: string; + }) => void; + dropProps: IDropAttributes; +} + +const DropboxComponent = ({ + id: dropboxId, + label: boxLabel, + fields, + onAddField, + onDeleteField, + onEditField, + onReorderField, + limit = 1, + isValidDropTarget, + canDrop, + dropProps, +}: DropboxProps) => { + const prefersReducedMotion = usePrefersReducedMotion(); + const [closing, setClosing] = useState(false); + const handleDragEnd = useCallback( + ({ source, destination }: DropResult) => { + if (!destination) return; + + onReorderField({ + sourceAggId: fields[source.index].id, + destinationAggId: fields[destination.index].id, + }); + }, + [fields, onReorderField] + ); + + const animateDelete = useCallback( + (id: string) => { + setClosing(id); + setTimeout( + () => { + onDeleteField(id); + setClosing(false); + }, + prefersReducedMotion ? 0 : 350 // Also update speed in dropbox.scss + ); + }, + [onDeleteField, prefersReducedMotion] + ); + + return ( + + +
+ + {fields.map(({ id, label }, index) => ( + + + onEditField(id)}> + + {label} + + + animateDelete(id)} + data-test-subj="dropBoxRemoveBtn" + /> + + + ))} + + {fields.length < limit && ( + + Click or drop to add + onAddField()} + data-test-subj="dropBoxAddBtn" + /> + + )} +
+
+
+ ); +}; + +const Dropbox = React.memo((dropBox: UseDropboxProps) => { + const props = useDropbox(dropBox); + + return ; +}); + +export { Dropbox, DropboxComponent, DropboxProps }; diff --git a/src/plugins/wizard/public/application/components/data_tab/field_search.tsx b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx new file mode 100644 index 000000000000..62dcf2c2b953 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { setSearchField } from '../../utils/state_management/visualization_slice'; +import { useTypedDispatch } from '../../utils/state_management'; + +export interface Props { + /** + * the input value of the user + */ + value?: string; +} + +/** + * Component is Wizard's side bar to search of available fields + * Additionally there's a button displayed that allows the user to show/hide more filter fields + */ +export function FieldSearch({ value }: Props) { + const searchPlaceholder = i18n.translate('wizard.fieldChooser.searchPlaceHolder', { + defaultMessage: 'Search field names', + }); + + const dispatch = useTypedDispatch(); + + return ( + + + + dispatch(setSearchField(event.currentTarget.value))} + placeholder={searchPlaceholder} + value={value} + /> + + + + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss new file mode 100644 index 000000000000..b9b75a8a260d --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "../../util"; + +.wizFieldSelector { + @include scrollNavParent(auto 1fr); + + padding: $euiSizeS; + + &__fieldGroups { + @include euiYScrollWithShadows; + + overflow-y: auto; + margin-right: -$euiSizeS; + padding-right: $euiSizeS; + margin-left: -$euiSizeS; + padding-left: $euiSizeS; + margin-top: $euiSizeS; + } + + &__fieldGroup { + margin-top: $euiSizeS; + + &:first-child { + margin-top: 0; + } + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx new file mode 100644 index 000000000000..da1020413b26 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -0,0 +1,131 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; +import { FieldSearch } from './field_search'; + +import { + IndexPatternField, + OPENSEARCH_FIELD_TYPES, + OSD_FIELD_TYPES, +} from '../../../../../data/public'; +import { FieldSelectorField } from './field_selector_field'; + +import './field_selector.scss'; +import { useTypedSelector } from '../../utils/state_management'; +import { useIndexPatterns } from '../../utils/use'; +import { getAvailableFields } from './utils'; + +interface IFieldCategories { + categorical: IndexPatternField[]; + numerical: IndexPatternField[]; + meta: IndexPatternField[]; +} + +const META_FIELDS: string[] = [ + OPENSEARCH_FIELD_TYPES._ID, + OPENSEARCH_FIELD_TYPES._INDEX, + OPENSEARCH_FIELD_TYPES._SOURCE, + OPENSEARCH_FIELD_TYPES._TYPE, +]; + +export const FieldSelector = () => { + const indexPattern = useIndexPatterns().selected; + const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + const [filteredFields, setFilteredFields] = useState([]); + + useEffect(() => { + const indexFields = indexPattern?.fields ?? []; + const filteredSubset = getAvailableFields(indexFields).filter((field) => + field.displayName.includes(fieldSearchValue) + ); + + setFilteredFields(filteredSubset); + return; + }, [fieldSearchValue, indexPattern?.fields]); + + const fields = useMemo( + () => + filteredFields?.reduce( + (fieldGroups, currentField) => { + const category = getFieldCategory(currentField); + fieldGroups[category].push(currentField); + + return fieldGroups; + }, + { + categorical: [], + numerical: [], + meta: [], + } + ), + [filteredFields] + ); + + return ( +
+
+
+ + +
+
+ {/* Count Field */} + + + + +
+
+ ); +}; + +interface FieldGroupProps { + fields?: IndexPatternField[]; + header: string; + id: string; +} + +const FieldGroup = ({ fields, header, id }: FieldGroupProps) => ( + + {header} + + } + extraAction={ + + {fields?.length || 0} + + } + initialIsOpen + > + {fields?.map((field, i) => ( + + + + ))} + +); + +function getFieldCategory(field: IndexPatternField): keyof IFieldCategories { + if (META_FIELDS.includes(field.name)) return 'meta'; + if (field.type === OSD_FIELD_TYPES.NUMBER) return 'numerical'; + + return 'categorical'; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss new file mode 100644 index 000000000000..a7f093ad22a1 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizFieldSelectorField { + @include euiBottomShadowSmall; + + background-color: $euiColorEmptyShade; + border: $euiBorderThin; + margin-top: $euiSizeXS; + + & > .osdFieldButton__button { + padding: 0; + } + + & .osdFieldButton__name { + padding: $euiSizeS $euiSizeS $euiSizeS 0; + } + + & > button { + align-items: stretch; + } + + & .osdFieldIcon { + box-shadow: none; + height: 100%; + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx new file mode 100644 index 000000000000..10acd5aa62f3 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { IndexPatternField } from '../../../../../data/public'; +import { FieldButton, FieldIcon } from '../../../../../opensearch_dashboards_react/public'; +import { useDrag } from '../../utils/drag_drop/drag_drop_context'; +import { COUNT_FIELD } from '../../utils/drag_drop/types'; + +import './field_selector_field.scss'; + +export interface FieldSelectorFieldProps { + field: Partial & Pick; +} + +// TODO: +// 1. Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) +// 2. Add popover for fields stats from discover as well +export const FieldSelectorField = ({ field }: FieldSelectorFieldProps) => { + const [infoIsOpen, setOpen] = useState(false); + const [dragProps] = useDrag({ + namespace: 'field-data', + value: field.name || COUNT_FIELD, + }); + + function togglePopover() { + setOpen(!infoIsOpen); + } + + function wrapOnDot(str?: string) { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str ? str.replace(/\./g, '.\u200B') : ''; + } + + const fieldName = ( + + {wrapOnDot(field.displayName)} + + ); + + return ( + } + // fieldAction={actionButton} + fieldName={fieldName} + {...dragProps} + /> + ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/index.scss b/src/plugins/wizard/public/application/components/data_tab/index.scss new file mode 100644 index 000000000000..d242eac4ee2c --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/index.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "../../util"; + +.wizDataTab { + @include scrollNavParent; + + display: grid; + grid-template-columns: 50% 50%; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/index.tsx b/src/plugins/wizard/public/application/components/data_tab/index.tsx new file mode 100644 index 000000000000..1f880f20698d --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { FieldSelector } from './field_selector'; + +import './index.scss'; +import { ConfigPanel } from './config_panel'; + +export const DATA_TAB_ID = 'data_tab'; + +export const DataTab = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx new file mode 100644 index 000000000000..37c55e25be99 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Dropbox } from './dropbox'; +import { Title } from './title'; + +export const mapSchemaToAggPanel = (schemas: Schemas) => { + const panelComponents = schemas.all.map((schema) => { + return ; + }); + + return ( + <> + + <div className="wizConfig__content">{panelComponents}</div> + </> + ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx new file mode 100644 index 000000000000..6fa6789d508e --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx @@ -0,0 +1,132 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { cloneDeep, get } from 'lodash'; +import { useDebounce } from 'react-use'; +import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; +import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public'; +import { Title } from './title'; +import { useIndexPatterns, useVisualizationType } from '../../utils/use'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../types'; +import { AggParam, IAggType, IFieldParamType } from '../../../../../data/public'; +import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_slice'; +import { setValidity } from '../../utils/state_management/metadata_slice'; + +const EDITOR_KEY = 'CONFIG_PANEL'; + +export function SecondaryPanel() { + const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization!.draftAgg); + const isEditorValid = useTypedSelector((state) => state.metadata.editor.validity[EDITOR_KEY]); + const [touched, setTouched] = useState(false); + const dispatch = useTypedDispatch(); + const vizType = useVisualizationType(); + const indexPattern = useIndexPatterns().selected; + const { + services: { + data: { + search: { aggs: aggService }, + }, + }, + } = useOpenSearchDashboards<WizardServices>(); + const schemas = vizType.ui.containerConfig.data.schemas.all; + + const aggConfigs = useMemo(() => { + return ( + indexPattern && draftAgg && aggService.createAggConfigs(indexPattern, [cloneDeep(draftAgg)]) + ); + }, [draftAgg, aggService, indexPattern]); + + const aggConfig = aggConfigs?.aggs[0]; + + const selectedSchema = useMemo( + () => schemas.find((schema) => schema.name === aggConfig?.schema), + [aggConfig?.schema, schemas] + ); + + const showAggParamEditor = !!(aggConfig && indexPattern); + + const closeMenu = useCallback(() => { + dispatch(editDraftAgg(undefined)); + }, [dispatch]); + + const handleSetValid = useCallback( + (isValid: boolean) => { + // Set validity state globally + dispatch( + setValidity({ + key: EDITOR_KEY, + valid: isValid, + }) + ); + }, + [dispatch] + ); + + // Autosave is agg value has changed and edits are valid + useDebounce( + () => { + if (isEditorValid) { + dispatch(saveDraftAgg()); + } else { + // To indicate that an invalid edit was made + setTouched(true); + } + }, + 200, + [draftAgg, isEditorValid] + ); + + return ( + <div className="wizConfig__section wizConfig--secondary"> + <Title title={selectedSchema?.title ?? 'Edit'} isSecondary closeMenu={closeMenu} /> + {showAggParamEditor && ( + <DefaultEditorAggParams + className="wizConfig__aggEditor" + agg={aggConfig!} + indexPattern={indexPattern!} + setValidity={handleSetValid} + setTouched={setTouched} + schemas={schemas} + formIsTouched={touched} + groupName={selectedSchema?.group ?? 'none'} + metricAggs={[]} + state={{ + data: {}, + description: '', + title: '', + }} + setAggParamValue={function <T extends string | number | symbol>( + aggId: string, + paramName: T, + value: any + ): void { + aggConfig.params[paramName] = value; + dispatch(editDraftAgg(aggConfig.serialize())); + }} + onAggTypeChange={function (aggId: string, aggType: IAggType): void { + aggConfig.type = aggType; + + // Persist field if the new agg type supports the existing field + const fieldParam = (aggType.params as AggParam[]).find(({ type }) => type === 'field'); + if (fieldParam) { + const availableFields = (fieldParam as IFieldParamType).getAvailableFields(aggConfig); + const indexField = availableFields.find( + ({ name }) => name === get(draftAgg, 'params.field') + ); + + if (indexField) { + aggConfig.params.field = indexField; + } + } + + dispatch(editDraftAgg(aggConfig.serialize())); + }} + /> + )} + </div> + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/title.tsx b/src/plugins/wizard/public/application/components/data_tab/title.tsx new file mode 100644 index 000000000000..8083efd05b0b --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/title.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +export interface TitleProps { + title: string; + isSecondary?: boolean; + closeMenu?: () => void; +} + +export const Title = ({ title, isSecondary, closeMenu }: TitleProps) => { + const icon = isSecondary && ( + <EuiIcon type="arrowLeft" onClick={closeMenu} data-test-subj="panelCloseBtn" /> + ); + return ( + <> + <div className="wizConfig__title"> + <EuiFlexGroup gutterSize="s" alignItems="center"> + {icon && <EuiFlexItem grow={false}>{icon}</EuiFlexItem>} + <EuiFlexItem> + <EuiTitle size="xxs"> + <h2>{title}</h2> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </div> + {isSecondary ? <EuiHorizontalRule margin="s" /> : <EuiSpacer size="s" />} + </> + ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/use/index.ts b/src/plugins/wizard/public/application/components/data_tab/use/index.ts new file mode 100644 index 000000000000..64265e655fa7 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/use/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useDropbox } from './use_dropbox'; diff --git a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx new file mode 100644 index 000000000000..8229073c1b4c --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx @@ -0,0 +1,217 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { cloneDeep } from 'lodash'; +import { BucketAggType, IndexPatternField, propFilter } from '../../../../../../data/common'; +import { Schema } from '../../../../../../vis_default_editor/public'; +import { COUNT_FIELD, FieldDragDataType } from '../../../utils/drag_drop/types'; +import { useTypedDispatch, useTypedSelector } from '../../../utils/state_management'; +import { DropboxDisplay, DropboxProps } from '../dropbox'; +import { useDrop } from '../../../utils/drag_drop'; +import { + editDraftAgg, + reorderAgg, + updateAggConfigParams, +} from '../../../utils/state_management/visualization_slice'; +import { useIndexPatterns } from '../../../utils/use/use_index_pattern'; +import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../../types'; + +const filterByName = propFilter('name'); +const filterByType = propFilter('type'); + +export interface UseDropboxProps extends Pick<DropboxProps, 'id' | 'label'> { + schema: Schema; +} + +export const useDropbox = (props: UseDropboxProps): DropboxProps => { + const { id: dropboxId, label, schema } = props; + const [validAggTypes, setValidAggTypes] = useState<string[]>([]); + const dispatch = useTypedDispatch(); + const indexPattern = useIndexPatterns().selected; + const { + services: { + data: { + search: { aggs: aggService }, + }, + }, + } = useOpenSearchDashboards<WizardServices>(); + const aggConfigParams = useTypedSelector( + (state) => state.visualization.activeVisualization?.aggConfigParams + ); + + const aggConfigs = useMemo(() => { + return indexPattern && aggService.createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + }, [aggConfigParams, aggService, indexPattern]); + + const aggs = useMemo(() => aggConfigs?.aggs ?? [], [aggConfigs?.aggs]); + + const dropboxAggs = aggs.filter((agg) => agg.schema === schema.name); + + const displayFields: DropboxDisplay[] = useMemo( + () => + dropboxAggs?.map( + (agg): DropboxDisplay => ({ + id: agg.id, + label: agg.makeLabel(), + }) + ) || [], + [dropboxAggs] + ); + + // Event handlers for each dropbox action type + const onAddField = useCallback(() => { + if (!aggConfigs || !indexPattern) { + throw new Error('Cannot create new field, missing aggConfigs or indexPattern'); + } + + const aggConfig = aggConfigs.createAggConfig( + { + schema: schema.name, + // using any since createAggConfig requires the type property but when an agg is brandNew, this has to be skipped. + // TODO: Update createAggConfig typing to correctly handle missing type field + } as any, + { + addToAggConfigs: false, + } + ); + + aggConfig.brandNew = true; + const newAggs = [...aggs, aggConfig]; + const newAggConfigs = aggService.createAggConfigs(indexPattern, cloneDeep(newAggs)); + const newAggConfig = newAggConfigs.aggs.find((agg) => agg.brandNew); + + if (!newAggConfig) { + throw new Error('Missing new aggConfig'); + } + + dispatch(editDraftAgg(newAggConfig.serialize())); + }, [aggConfigs, aggService, aggs, dispatch, indexPattern, schema.name]); + + const onEditField = useCallback( + (aggId: string) => { + const aggConfig = aggConfigs?.aggs.find((agg) => agg.id === aggId); + + if (!aggConfig) { + throw new Error('Could not find agg in aggConfigs'); + } + + dispatch(editDraftAgg(aggConfig.serialize())); + }, + [aggConfigs?.aggs, dispatch] + ); + + const onDeleteField = useCallback( + (aggId: string) => { + const newAggs = aggConfigs?.aggs.filter((agg) => agg.id !== aggId); + + if (newAggs) { + dispatch(updateAggConfigParams(newAggs.map((agg) => agg.serialize()))); + } + }, + [aggConfigs?.aggs, dispatch] + ); + + const onDropField = useCallback( + (data: FieldDragDataType['value']) => { + if (!data || !validAggTypes.length) return; + + const fieldName = data; + const schemaAggTypes = (schema.defaults as any).aggTypes; + const allowedAggTypes = schemaAggTypes + ? schemaAggTypes.filter((type: string) => validAggTypes.includes(type)) + : []; + + aggConfigs?.createAggConfig({ + type: allowedAggTypes[0] || validAggTypes[0], + schema: schema.name, + params: { + field: fieldName, + }, + }); + + if (aggConfigs) { + dispatch(updateAggConfigParams(aggConfigs.aggs.map((agg) => agg.serialize()))); + } + }, + [aggConfigs, dispatch, schema.defaults, schema.name, validAggTypes] + ); + + const onReorderField = useCallback( + ({ sourceAggId, destinationAggId }) => { + dispatch( + reorderAgg({ + sourceId: sourceAggId, + destinationId: destinationAggId, + }) + ); + }, + [dispatch] + ); + + const [dropProps, { isValidDropTarget, dragData, ...dropState }] = useDrop( + 'field-data', + onDropField + ); + + useEffect(() => { + const getValidAggTypes = () => { + if (!dragData || schema.group === 'none') return []; + const isCountField = dragData === COUNT_FIELD; + + const indexField = isCountField + ? { type: 'count' } + : getIndexPatternField(dragData, indexPattern?.fields ?? []); + + if (!indexField) return []; + + // Get all aggTypes allowed by the schema and get a list of all the aggTypes that the dragged index field can use + const aggTypes = aggService.types.getAll(); + // `types` can be either a Bucket or Metric aggType, but both types have the name property. + const allowedAggTypes = filterByName( + aggTypes[schema.group] as BucketAggType[], + schema.aggFilter + ); + + return ( + allowedAggTypes + .filter((aggType) => { + const allowedFieldTypes = aggType.paramByName('field')?.filterFieldTypes; + return filterByType([indexField], allowedFieldTypes).length !== 0; + }) + .filter((aggType) => (isCountField ? true : aggType.name !== 'count')) + // `types` can be either a Bucket or Metric aggType, but both types have the name property. + .map((aggType) => (aggType as BucketAggType).name) + ); + }; + + setValidAggTypes(getValidAggTypes()); + + return () => { + setValidAggTypes([]); + }; + }, [aggService.types, dragData, indexPattern?.fields, schema.aggFilter, schema.group]); + + const canDrop = validAggTypes.length > 0 && schema.max > dropboxAggs.length; + + return { + id: dropboxId, + label, + limit: schema.max, + fields: displayFields, + onAddField, + onEditField, + onDeleteField, + onReorderField, + ...dropState, + dragData, + isValidDropTarget: canDrop, + dropProps, + }; +}; + +const getIndexPatternField = (indexFieldName: string, availableFields: IndexPatternField[]) => + availableFields.find(({ name }) => name === indexFieldName); diff --git a/src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts b/src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts new file mode 100644 index 000000000000..b88c3bc444d0 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; + +const QUERY = '(prefers-reduced-motion: no-preference)'; + +export function usePrefersReducedMotion() { + const [prefersReducedMotion, setPrefersReducedMotion] = useState( + !window.matchMedia(QUERY).matches + ); + + useEffect(() => { + const mediaQueryList = window.matchMedia(QUERY); + const listener = (event) => { + setPrefersReducedMotion(!event.matches); + }; + + if (mediaQueryList.addEventListener) { + mediaQueryList.addEventListener('change', listener); + } + + return () => { + mediaQueryList.removeEventListener('change', listener); + }; + }, []); + + return prefersReducedMotion; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts b/src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts new file mode 100644 index 000000000000..d938e92b6978 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + FieldTypes, + IndexPatternField, + isNestedField, + propFilter, +} from '../../../../../../data/common'; + +const filterByType = propFilter('type'); + +export const getAvailableFields = ( + fields: IndexPatternField[], + filterFieldTypes: FieldTypes = '*' +) => { + const filteredFields = fields.filter((field: IndexPatternField) => { + if (!field.aggregatable || isNestedField(field) || field.scripted) { + return false; + } + + return filterByType([field], filterFieldTypes).length !== 0; + }); + + return filteredFields; +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/utils/index.ts b/src/plugins/wizard/public/application/components/data_tab/utils/index.ts new file mode 100644 index 000000000000..dd0cdea3e23e --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/utils/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { getAvailableFields } from './get_available_fields'; diff --git a/src/plugins/wizard/public/application/components/experimental_info.tsx b/src/plugins/wizard/public/application/components/experimental_info.tsx new file mode 100644 index 000000000000..35ea235c1ba3 --- /dev/null +++ b/src/plugins/wizard/public/application/components/experimental_info.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; + +export const InfoComponent = () => { + return ( + <EuiCallOut + className="hide-for-sharing" + data-test-subj="experimentalVisInfo" + size="s" + title={i18n.translate('wizard.experimentalInfoTitle', { + defaultMessage: 'This editor is experimental and should not be used in production', + })} + iconType="beaker" + > + <FormattedMessage + id="wizard.experimentalInfoText" + defaultMessage="We want to hear from you about how we can improve your experience. Leave feedback in {githubLink}." + values={{ + githubLink: ( + <EuiLink + external + href="/opensearch-project/OpenSearch-Dashboards/issues/2280" + target="_blank" + > + the GitHub issue + </EuiLink> + ), + }} + /> + </EuiCallOut> + ); +}; + +export const ExperimentalInfo = memo(InfoComponent); diff --git a/src/plugins/wizard/public/application/components/left_nav.tsx b/src/plugins/wizard/public/application/components/left_nav.tsx new file mode 100644 index 000000000000..2825d9778744 --- /dev/null +++ b/src/plugins/wizard/public/application/components/left_nav.tsx @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import './side_nav.scss'; +import { DataSourceSelect } from './data_source_select'; +import { DataTab } from './data_tab'; + +export const LeftNav = () => { + return ( + <section className="wizSidenav left"> + <div className="wizDatasourceSelect wizSidenav__header"> + <DataSourceSelect /> + </div> + <DataTab key="containerName" /> + </section> + ); +}; diff --git a/src/plugins/wizard/public/application/components/option.scss b/src/plugins/wizard/public/application/components/option.scss new file mode 100644 index 000000000000..8bb394c260e8 --- /dev/null +++ b/src/plugins/wizard/public/application/components/option.scss @@ -0,0 +1,8 @@ +.wizOption { + background-color: $euiColorEmptyShade; + padding: $euiSizeM; + + & &__panel { + background-color: $euiColorLightestShade; + } +} diff --git a/src/plugins/wizard/public/application/components/option.tsx b/src/plugins/wizard/public/application/components/option.tsx new file mode 100644 index 000000000000..7440bb0825c0 --- /dev/null +++ b/src/plugins/wizard/public/application/components/option.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { FC } from 'react'; +import './option.scss'; + +interface Props { + title: string; + initialIsOpen?: boolean; +} + +export const Option: FC<Props> = ({ title, children, initialIsOpen = false }) => { + return ( + <> + <EuiAccordion + id={title} + buttonContent={title} + className="wizOption" + initialIsOpen={initialIsOpen} + > + <EuiSpacer size="s" /> + <EuiPanel color="subdued" className="wizOption__panel"> + {children} + </EuiPanel> + </EuiAccordion> + <EuiHorizontalRule margin="none" /> + </> + ); +}; diff --git a/src/plugins/wizard/public/application/components/right_nav.tsx b/src/plugins/wizard/public/application/components/right_nav.tsx new file mode 100644 index 000000000000..ea7c3e17eab6 --- /dev/null +++ b/src/plugins/wizard/public/application/components/right_nav.tsx @@ -0,0 +1,96 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiSuperSelect, + EuiSuperSelectOption, + EuiIcon, + IconType, + EuiConfirmModal, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { useVisualizationType } from '../utils/use'; +import './side_nav.scss'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../types'; +import { setActiveVisualization, useTypedDispatch } from '../utils/state_management'; + +export const RightNav = () => { + const [newVisType, setNewVisType] = useState<string>(); + const { + services: { types }, + } = useOpenSearchDashboards<WizardServices>(); + const { ui, name: activeVisName } = useVisualizationType(); + const dispatch = useTypedDispatch(); + const StyleSection = ui.containerConfig.style.render; + + const options: Array<EuiSuperSelectOption<string>> = types.all().map(({ name, icon, title }) => ({ + value: name, + inputDisplay: <OptionItem icon={icon} title={title} />, + dropdownDisplay: <OptionItem icon={icon} title={title} />, + 'data-test-subj': `visType-${name}`, + })); + + return ( + <section className="wizSidenav right"> + <div className="wizSidenav__header"> + <EuiSuperSelect + options={options} + valueOfSelected={activeVisName} + onChange={(name) => { + setNewVisType(name); + }} + fullWidth + data-test-subj="chartPicker" + /> + </div> + <div className="wizSidenav__style"> + <StyleSection /> + </div> + {newVisType && ( + <EuiConfirmModal + title={i18n.translate('wizard.rightNav.changeVisType.modalTitle', { + defaultMessage: 'Change visualization type', + })} + confirmButtonText={i18n.translate('wizard.rightNav.changeVisType.confirmText', { + defaultMessage: 'Change type', + })} + cancelButtonText={i18n.translate('wizard.rightNav.changeVisType.cancelText', { + defaultMessage: 'Cancel', + })} + onCancel={() => setNewVisType(undefined)} + onConfirm={() => { + dispatch( + setActiveVisualization({ + name: newVisType, + style: types.get(newVisType)?.ui.containerConfig.style.defaults, + }) + ); + + setNewVisType(undefined); + }} + maxWidth="300px" + data-test-subj="confirmVisChangeModal" + > + <p> + <FormattedMessage + id="wizard.rightNav.changeVisType.modalDescription" + defaultMessage="Changing the visualization type will reset all field selections. Do you want to continue?" + /> + </p> + </EuiConfirmModal> + )} + </section> + ); +}; + +const OptionItem = ({ icon, title }: { icon: IconType; title: string }) => ( + <> + <EuiIcon type={icon} className="wizTypeSelector__icon" /> + <span>{title}</span> + </> +); diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss new file mode 100644 index 000000000000..be8d6144df3a --- /dev/null +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "../variables"; + +.searchableDropdown { + overflow: "hidden"; + + .euiFormControlLayout__childrenWrapper { + display: flex; + } + + &--topDisplay { + padding-right: $euiSizeL; + font-size: $euiFontSizeS; + flex-grow: 1; + + .euiButtonEmpty__content { + justify-content: flex-start; + } + } + + &--fixedWidthChild { + width: calc(#{$wizLeftNavWidth} - #{$euiSizeXL} * 2); + } + + &--selectableWrapper .euiSelectableList { + // When clicking on the selectable content it will "highlight" itself with a box shadow + // This turns that off + box-shadow: none !important; + margin: ($euiFormControlPadding * -1) - 4; + } + + .euiPopover, + .euiPopover__anchor { + width: 100%; + } +} diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx new file mode 100644 index 000000000000..3ff8300e8d48 --- /dev/null +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx @@ -0,0 +1,177 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiLoadingSpinner, + EuiFormControlLayout, + EuiPopoverTitle, + EuiButtonEmpty, + EuiPopover, + EuiSelectable, + EuiTextColor, +} from '@elastic/eui'; +import './searchable_dropdown.scss'; + +export interface SearchableDropdownOption { + id: string; + label: string; + searchableLabel: string; + prepend: any; +} + +interface SearchableDropdownProps { + selected?: SearchableDropdownOption; + onChange: (selection) => void; + options: SearchableDropdownOption[]; + loading: boolean; + error?: Error; + prepend: string; + // not just the first time! + onOpen?: () => void; + equality: (A, B) => boolean; +} + +type DisplayError = any; + +function displayError(error: DisplayError) { + return typeof error === 'object' ? error.toString() : <>{error}</>; +} + +export const SearchableDropdown = ({ + onChange, + equality, + selected, + options, + error, + loading, + prepend, + onOpen, +}: SearchableDropdownProps) => { + const [localOptions, setLocalOptions] = useState<any[] | undefined>(undefined); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onButtonClick = () => { + if (!isPopoverOpen && typeof onOpen === 'function') { + onOpen(); + } + setIsPopoverOpen(!isPopoverOpen); + }; + const closePopover = () => setIsPopoverOpen(false); + + function selectNewOption(newOptions) { + // alright, the EUI Selectable is pretty ratchet + // this is as smarmy as it is because it needs to be + + // first go through and count all the "checked" options + const selectedCount = newOptions.filter((o) => o.checked === 'on').length; + + // if the count is 0, the user just "unchecked" our selection and we can just do nothing + if (selectedCount === 0) { + setIsPopoverOpen(false); + return; + } + + // then, if there's more than two selections, the Selectable left the previous selection as "checked" + // so we need to go and "uncheck" it + for (let i = 0; i < newOptions.length; i++) { + if (equality(newOptions[i], selected) && selectedCount > 1) { + delete newOptions[i].checked; + } + } + + // finally, we can pick the checked option as the actual selection + const newSelection = newOptions.filter((o) => o.checked === 'on')[0]; + + setLocalOptions(newOptions); + setIsPopoverOpen(false); + onChange(newSelection); + } + + useEffect(() => { + setLocalOptions( + options.map((o) => ({ + ...o, + checked: equality(o, selected) ? 'on' : undefined, + })) + ); + }, [selected, options, equality]); + + const listDisplay = (list, search) => + loading ? ( + <div style={{ textAlign: 'center' }}> + <EuiLoadingSpinner /> + </div> + ) : error !== undefined ? ( + displayError(error) + ) : ( + <> + <EuiPopoverTitle paddingSize="s" className="wizPopoverTitle"> + {search} + </EuiPopoverTitle> + {list} + </> + ); + + const selectable = ( + <div className="searchableDropdown--selectableWrapper"> + <EuiSelectable + aria-label="Selectable options" + data-test-subj="searchableDropdownList" + searchable + options={localOptions} + onChange={selectNewOption} + listProps={{ + showIcons: false, + }} + > + {listDisplay} + </EuiSelectable> + </div> + ); + + const selectedText = + selected === undefined ? ( + <EuiTextColor color="subdued">{loading ? 'Loading' : 'Select an option'}</EuiTextColor> + ) : ( + <> + {selected.prepend} {selected.label} + </> + ); + + const selectedView = ( + <EuiButtonEmpty + color="text" + size="s" + style={{ textAlign: 'left' }} + className="searchableDropdown--topDisplay" + data-test-subj="searchableDropdownValue" + onClick={onButtonClick} + > + {selectedText} + </EuiButtonEmpty> + ); + + const formControl = ( + <EuiFormControlLayout + title={selected === undefined ? 'Select an option' : selected.label} + isLoading={loading} + fullWidth={true} + style={{ cursor: 'pointer' }} + prepend={prepend} + icon={{ type: 'arrowDown', side: 'right' }} + readOnly={true} + > + {selectedView} + </EuiFormControlLayout> + ); + + return ( + <div className="searchableDropdown"> + <EuiPopover button={formControl} isOpen={isPopoverOpen} closePopover={closePopover}> + <div className="searchableDropdown--fixedWidthChild">{selectable}</div> + </EuiPopover> + </div> + ); +}; diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss new file mode 100644 index 000000000000..025e42d08170 --- /dev/null +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "../util"; +@import "../variables"; + +.wizSidenav { + @include scrollNavParent(auto 1fr); + + &.left { + border-right: $euiBorderThin; + grid-area: leftNav; + } + + &.right { + border-left: $euiBorderThin; + grid-area: rightNav; + height: 100%; + } + + &__header { + padding: $euiSizeS; + border-bottom: $euiBorderThin; + background-color: $euiColorEmptyShade; + } + + &__style { + @include euiYScrollWithShadows; + } +} + +.wizTypeSelector__icon { + margin-right: $euiSizeS; +} + +.wizSidenavTabs { + .euiTab__content { + text-transform: capitalize; + } + + @include scrollNavParent(min-content 1fr); + + & > [role="tabpanel"] { + @include scrollNavParent; + } +} + +.wizDatasourceSelect { + max-width: calc(#{$wizLeftNavWidth} - 1px); +} diff --git a/src/plugins/wizard/public/application/components/top_nav.scss b/src/plugins/wizard/public/application/components/top_nav.scss new file mode 100644 index 000000000000..cad0e3eebee2 --- /dev/null +++ b/src/plugins/wizard/public/application/components/top_nav.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizTopNav { + grid-area: topNav; + border-bottom: $euiBorderThin; +} diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx new file mode 100644 index 000000000000..4e45ba2f05ee --- /dev/null +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { useUnmount } from 'react-use'; +import { PLUGIN_ID } from '../../../common'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { getTopNavConfig } from '../utils/get_top_nav_config'; +import { WizardServices } from '../../types'; + +import './top_nav.scss'; +import { useIndexPatterns, useSavedWizardVis } from '../utils/use'; +import { useTypedSelector, useTypedDispatch } from '../utils/state_management'; +import { setEditorState } from '../utils/state_management/metadata_slice'; +import { useCanSave } from '../utils/use/use_can_save'; + +export const TopNav = () => { + // id will only be set for the edit route + const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); + const { services } = useOpenSearchDashboards<WizardServices>(); + const { + setHeaderActionMenu, + navigation: { + ui: { TopNavMenu }, + }, + } = services; + const rootState = useTypedSelector((state) => state); + const dispatch = useTypedDispatch(); + + const saveDisabledReason = useCanSave(); + const savedWizardVis = useSavedWizardVis(visualizationIdFromUrl); + + const config = useMemo(() => { + if (savedWizardVis === undefined) return; + + const { visualization: visualizationState, style: styleState } = rootState; + + return getTopNavConfig( + { + visualizationIdFromUrl, + savedWizardVis, + visualizationState, + styleState, + saveDisabledReason, + dispatch, + }, + services + ); + }, [rootState, savedWizardVis, services, visualizationIdFromUrl, saveDisabledReason, dispatch]); + + const indexPattern = useIndexPatterns().selected; + + // reset validity before component destroyed + useUnmount(() => { + dispatch(setEditorState({ state: 'loading' })); + }); + + return ( + <div className="wizTopNav"> + <TopNavMenu + appName={PLUGIN_ID} + config={config} + setMenuMountPoint={setHeaderActionMenu} + indexPatterns={indexPattern ? [indexPattern] : []} + showSearchBar + showSaveQuery + useDefaultBehaviors + /> + </div> + ); +}; diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss new file mode 100644 index 000000000000..1a57a891262e --- /dev/null +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizWorkspace { + display: grid; + -ms-grid-rows: auto $euiSizeM 1fr; + grid-template-rows: auto 1fr; + grid-area: workspace; + grid-gap: $euiSizeM; + padding: $euiSizeM; + background-color: $euiColorEmptyShade; + height: 100%; + + &__empty { + height: 100%; + } + + &__container { + position: relative; + } + + &__handFieldSvg { + animation: wizDragAnimation 6s ease-in-out infinite forwards; + position: absolute; + top: 34.5%; + } +} + +@keyframes wizDragAnimation { + 0% { + transform: none; + } + + 30% { + transform: translate(116%, -80%); + } + + 60% { + transform: none; + } + + 100% { + transform: none; + } +} diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx new file mode 100644 index 000000000000..1aa58a272dff --- /dev/null +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -0,0 +1,111 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel } from '@elastic/eui'; +import React, { FC, useState, useMemo, useEffect, useLayoutEffect } from 'react'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { IExpressionLoaderParams } from '../../../../expressions/public'; +import { WizardServices } from '../../types'; +import { validateSchemaState } from '../utils/validate_schema_state'; +import { useTypedSelector } from '../utils/state_management'; +import { useVisualizationType } from '../utils/use'; +import { PersistedState } from '../../../../visualizations/public'; + +import hand_field from '../../assets/hand_field.svg'; +import fields_bg from '../../assets/fields_bg.svg'; + +import './workspace.scss'; +import { ExperimentalInfo } from './experimental_info'; + +export const Workspace: FC = ({ children }) => { + const { + services: { + expressions: { ReactExpressionRenderer }, + notifications: { toasts }, + data, + }, + } = useOpenSearchDashboards<WizardServices>(); + const { toExpression, ui } = useVisualizationType(); + const [expression, setExpression] = useState<string>(); + const [searchContext, setSearchContext] = useState<IExpressionLoaderParams['searchContext']>({ + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getFilters(), + 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(), []); + + useEffect(() => { + async function loadExpression() { + const schemas = ui.containerConfig.data.schemas; + const [valid, errorMsg] = validateSchemaState(schemas, rootState); + + if (!valid) { + if (errorMsg) { + toasts.addWarning(errorMsg); + } + setExpression(undefined); + return; + } + const exp = await toExpression(rootState); + setExpression(exp); + } + + loadExpression(); + }, [rootState, toExpression, toasts, ui.containerConfig.data.schemas]); + + useLayoutEffect(() => { + const subscription = data.query.state$.subscribe(({ state }) => { + setSearchContext({ + query: state.query, + timeRange: state.time, + filters: state.filters, + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [data.query.state$]); + + return ( + <section className="wizWorkspace"> + <EuiFlexGroup className="wizCanvasControls"> + <EuiFlexItem> + <ExperimentalInfo /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiPanel className="wizCanvas" data-test-subj="visualizationLoader"> + {expression ? ( + <ReactExpressionRenderer + expression={expression} + searchContext={searchContext} + uiState={uiState} + /> + ) : ( + <EuiFlexItem className="wizWorkspace__empty" data-test-subj="emptyWorkspace"> + <EuiEmptyPrompt + title={<h2>Add a field to start</h2>} + body={ + <> + <p>Drag a field to the configuration panel to generate a visualization.</p> + <span className="wizWorkspace__container"> + <EuiIcon className="wizWorkspace__fieldSvg" type={fields_bg} size="original" /> + <EuiIcon + className="wizWorkspace__handFieldSvg" + type={hand_field} + size="original" + /> + </span> + </> + } + /> + </EuiFlexItem> + )} + </EuiPanel> + </section> + ); +}; diff --git a/src/plugins/wizard/public/application/index.tsx b/src/plugins/wizard/public/application/index.tsx new file mode 100644 index 000000000000..28ee13a80bf6 --- /dev/null +++ b/src/plugins/wizard/public/application/index.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router, Route, Switch } from 'react-router-dom'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Store } from 'redux'; +import { AppMountParameters } from '../../../../core/public'; +import { WizardServices } from '../types'; +import { WizardApp } from './app'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { EDIT_PATH } from '../../common'; + +export const renderApp = ( + { element, history }: AppMountParameters, + services: WizardServices, + store: Store +) => { + ReactDOM.render( + <Router history={history}> + <OpenSearchDashboardsContextProvider services={services}> + <ReduxProvider store={store}> + <services.i18n.Context> + <Switch> + <Route path={[`${EDIT_PATH}/:id`, '/']} exact={false}> + <WizardApp /> + </Route> + </Switch> + </services.i18n.Context> + </ReduxProvider> + </OpenSearchDashboardsContextProvider> + </Router>, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/wizard/public/application/utils/breadcrumbs.ts b/src/plugins/wizard/public/application/utils/breadcrumbs.ts new file mode 100644 index 000000000000..7bdb41075f35 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/breadcrumbs.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { VISUALIZE_ID } from '../../../common'; + +const defaultEditText = i18n.translate('wizard.editor.defaultEditBreadcrumbText', { + defaultMessage: 'Edit', +}); + +export function getVisualizeLandingBreadcrumbs(navigateToApp) { + return [ + { + text: i18n.translate('wizard.listing.breadcrumb', { + defaultMessage: 'Visualize', + }), + onClick: () => navigateToApp(VISUALIZE_ID), + }, + ]; +} + +export function getCreateBreadcrumbs(navigateToApp) { + return [ + ...getVisualizeLandingBreadcrumbs(navigateToApp), + { + text: i18n.translate('wizard.editor.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]; +} + +export function getEditBreadcrumbs(text: string = defaultEditText, navigateToApp) { + return [ + ...getVisualizeLandingBreadcrumbs(navigateToApp), + { + text, + }, + ]; +} diff --git a/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx new file mode 100644 index 000000000000..c0f8725a501a --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx @@ -0,0 +1,135 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { + createContext, + DragEvent, + FC, + ReactNode, + useContext, + useEffect, + useState, +} from 'react'; +import { DragDataType } from './types'; + +// TODO: Replace any with corret type +// TODO: Split into separate files +interface IDragDropContext { + data: DragDataType; + setData?: (data: DragDataType) => void; + isDragging: boolean; + setIsDragging?: any; +} + +const EMPTY_DATA: DragDataType = { + namespace: null, + value: null, +}; + +const defaultContextProps: IDragDropContext = { + isDragging: false, + data: EMPTY_DATA, +}; + +const DragDropContext = createContext<IDragDropContext>(defaultContextProps); + +const DragDropProvider: FC<ReactNode> = ({ children }) => { + const [isDragging, setIsDragging] = useState(false); + const [data, setData] = useState<DragDataType>(EMPTY_DATA); + return ( + <DragDropContext.Provider + value={{ + data, + setData, + + isDragging, + setIsDragging, + }} + > + {children} + </DragDropContext.Provider> + ); +}; + +const useDragDropContext = () => useContext(DragDropContext); + +function useDrag(dragData: DragDataType) { + const { setData, setIsDragging } = useDragDropContext(); + const dragElementProps = { + draggable: true, + onDragStart: (event: DragEvent) => { + setIsDragging(true); + setData!(dragData); + }, + onDragEnd: (event: DragEvent) => { + setIsDragging(false); + setData!({ + namespace: null, + value: null, + }); + }, + }; + return [dragElementProps]; +} + +export interface IDropAttributes { + onDragOver: (event: DragEvent) => void; + onDrop: (event: DragEvent) => void; + onDragEnter: (event: DragEvent) => void; + onDragLeave: (event: DragEvent) => void; +} + +export interface IDropState { + isDragging: boolean; + canDrop: boolean; + isValidDropTarget: boolean; + dragData: DragDataType['value']; +} +const useDrop = ( + namespace: DragDataType['namespace'], + onDropCallback: (data: DragDataType['value']) => void +): [IDropAttributes, IDropState] => { + const { data, isDragging, setIsDragging, setData } = useDragDropContext(); + const [canDrop, setCanDrop] = useState(0); + + const dropAttributes: IDropAttributes = { + onDragOver: (event) => { + event.preventDefault(); + }, + onDrop: (event) => { + setIsDragging(false); + setCanDrop(0); + onDropCallback(data.value); + setData!({ + namespace: null, + value: null, + }); + }, + onDragEnter: (event) => { + if (data?.namespace === namespace) { + setCanDrop((state) => state + 1); + } + }, + onDragLeave: (event) => { + setCanDrop((state) => state - 1); + }, + }; + + useEffect(() => { + if (!isDragging) setCanDrop(0); + }, [isDragging]); + + return [ + dropAttributes, + { + isDragging, + canDrop: canDrop > 0, + isValidDropTarget: isDragging && data?.namespace === namespace, + dragData: data.value, + }, + ]; +}; + +export { DragDropContext, DragDropProvider, useDragDropContext, useDrag, useDrop }; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/index.ts b/src/plugins/wizard/public/application/utils/drag_drop/index.ts new file mode 100644 index 000000000000..3799a2eb6052 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './drag_drop_context'; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/types.ts b/src/plugins/wizard/public/application/utils/drag_drop/types.ts new file mode 100644 index 000000000000..18fa226bb252 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPatternField, METRIC_TYPES } from '../../../../../data/common'; + +export const COUNT_FIELD = Symbol.for(METRIC_TYPES.COUNT); + +export interface EmptyDragDataType { + namespace: null; + value: null; +} +export interface FieldDragDataType { + namespace: 'field-data'; + value: IndexPatternField['name'] | null | typeof COUNT_FIELD; +} + +export type DragDataType = EmptyDragDataType | FieldDragDataType; diff --git a/src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts b/src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts new file mode 100644 index 000000000000..f56169114f60 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WizardServices } from '../..'; + +export const getSavedWizardVis = async (services: WizardServices, wizardVisId?: string) => { + const { savedWizardLoader } = services; + if (!savedWizardLoader) { + return {}; + } + const savedWizardVis = await savedWizardLoader.get(wizardVisId); + + return savedWizardVis; +}; diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx new file mode 100644 index 000000000000..75ebd87fbd7c --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -0,0 +1,184 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { TopNavMenuData } from '../../../../navigation/public'; +import { + OnSaveProps, + SavedObjectSaveModalOrigin, + showSaveModal, +} from '../../../../saved_objects/public'; +import { WizardServices } from '../..'; +import { WizardVisSavedObject } from '../../types'; +import { StyleState, VisualizationState, AppDispatch } from './state_management'; +import { EDIT_PATH } from '../../../common'; +import { setEditorState } from './state_management/metadata_slice'; +interface TopNavConfigParams { + visualizationIdFromUrl: string; + savedWizardVis: WizardVisSavedObject; + visualizationState: VisualizationState; + styleState: StyleState; + saveDisabledReason?: string; + dispatch: AppDispatch; +} + +export const getTopNavConfig = ( + { + visualizationIdFromUrl, + savedWizardVis, + visualizationState, + styleState, + saveDisabledReason, + dispatch, + }: TopNavConfigParams, + { + history, + toastNotifications, + i18n: { Context: I18nContext }, + data: { indexPatterns }, + }: WizardServices +) => { + const topNavConfig: TopNavMenuData[] = [ + { + id: 'save', + iconType: 'save', + emphasize: savedWizardVis && !savedWizardVis.id, + description: i18n.translate('wizard.topNavMenu.saveVisualizationButtonAriaLabel', { + defaultMessage: 'Save Visualization', + }), + className: 'saveButton', + label: i18n.translate('wizard.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'save', + }), + testId: 'wizardSaveButton', + disableButton: !!saveDisabledReason, + tooltip: saveDisabledReason, + run: (_anchorElement) => { + const onSave = async ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newDescription, + returnToOrigin, + }: OnSaveProps & { returnToOrigin: boolean }) => { + if (!savedWizardVis) { + return; + } + const currentTitle = savedWizardVis.title; + const indexPattern = await indexPatterns.get(visualizationState.indexPattern || ''); + savedWizardVis.searchSourceFields = { + index: indexPattern, + }; + const vizStateWithoutIndex = { + searchField: visualizationState.searchField, + activeVisualization: visualizationState.activeVisualization, + }; + savedWizardVis.visualizationState = JSON.stringify(vizStateWithoutIndex); + savedWizardVis.styleState = JSON.stringify(styleState); + savedWizardVis.title = newTitle; + savedWizardVis.description = newDescription; + savedWizardVis.copyOnSave = newCopyOnSave; + + try { + const id = await savedWizardVis.save({ + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + returnToOrigin, + }); + + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate( + 'wizard.topNavMenu.saveVisualization.successNotificationText', + { + defaultMessage: `Saved '{visTitle}'`, + values: { + visTitle: savedWizardVis.title, + }, + } + ), + 'data-test-subj': 'saveVisualizationSuccess', + }); + + // Update URL + if (id !== visualizationIdFromUrl) { + history.push({ + ...history.location, + pathname: `${EDIT_PATH}/${id}`, + }); + } + dispatch(setEditorState({ state: 'clean' })); + } else { + // reset title if save not successful + savedWizardVis.title = currentTitle; + } + + // Even if id='', which it will be for a duplicate title warning, we still want to return it, to avoid closing the modal + return { id }; + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(error); + + toastNotifications.addDanger({ + title: i18n.translate('wizard.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: newTitle, + }, + }), + text: error.message, + 'data-test-subj': 'saveVisualizationError', + }); + + // reset title if save not successful + savedWizardVis.title = currentTitle; + return { error }; + } + }; + + const saveModal = ( + <SavedObjectSaveModalOrigin + documentInfo={savedWizardVis} + onSave={onSave} + objectType={'wizard'} + onClose={() => {}} + /> + ); + + showSaveModal(saveModal, I18nContext); + }, + }, + ]; + + return topNavConfig; +}; diff --git a/src/plugins/wizard/public/application/utils/state_management/hooks.ts b/src/plugins/wizard/public/application/utils/state_management/hooks.ts new file mode 100644 index 000000000000..607fe05b1623 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/hooks.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// Use throughout the app instead of plain `useDispatch` and `useSelector` +export const useTypedDispatch = () => useDispatch<AppDispatch>(); +export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector; diff --git a/src/plugins/wizard/public/application/utils/state_management/index.ts b/src/plugins/wizard/public/application/utils/state_management/index.ts new file mode 100644 index 000000000000..5a3e34c8da69 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './store'; +export * from './hooks'; +export * from './shared_actions'; diff --git a/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts new file mode 100644 index 000000000000..a7be76061620 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { WizardServices } from '../../../types'; + +/* + * Initial state: default state when opening wizard plugin + * 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'; + +export interface MetadataState { + editor: { + validity: { + // Validity for each section in the editor + [key: string]: boolean; + }; + state: EditorState; + }; +} + +const initialState: MetadataState = { + editor: { + validity: {}, + state: 'loading', + }, +}; + +export const getPreloadedState = async ({ + types, + data, +}: WizardServices): Promise<MetadataState> => { + const preloadedState = { ...initialState }; + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'metadata', + initialState, + reducers: { + setValidity: (state, action: PayloadAction<{ key: string; valid: boolean }>) => { + const { key, valid } = action.payload; + state.editor.validity[key] = valid; + }, + setEditorState: (state, action: PayloadAction<{ state: EditorState }>) => { + state.editor.state = action.payload.state; + }, + setState: (_state, action: PayloadAction<MetadataState>) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setValidity, setEditorState, setState } = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/state_management/preload.ts b/src/plugins/wizard/public/application/utils/state_management/preload.ts new file mode 100644 index 000000000000..1d96608e33cc --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PreloadedState } from '@reduxjs/toolkit'; +import { WizardServices } from '../../..'; +import { getPreloadedState as getPreloadedStyleState } from './style_slice'; +import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; +import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; +import { RootState } from './store'; + +export const getPreloadedState = async ( + services: WizardServices +): Promise<PreloadedState<RootState>> => { + const styleState = await getPreloadedStyleState(services); + const visualizationState = await getPreloadedVisualizationState(services); + const metadataState = await getPreloadedMetadataState(services); + + return { + style: styleState, + visualization: visualizationState, + metadata: metadataState, + }; +}; diff --git a/src/plugins/wizard/public/application/utils/state_management/shared_actions.ts b/src/plugins/wizard/public/application/utils/state_management/shared_actions.ts new file mode 100644 index 000000000000..f1dc6ee027b8 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/shared_actions.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createAction } from '@reduxjs/toolkit'; +import { VisualizationType } from '../../../services/type_service/visualization_type'; + +interface ActiveVisPayload { + name: VisualizationType['name']; + style: VisualizationType['ui']['containerConfig']['style']['defaults']; +} + +export const setActiveVisualization = createAction<ActiveVisPayload>('setActiveVisualzation'); diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts new file mode 100644 index 000000000000..1610b3333062 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; +import { reducer as styleReducer } from './style_slice'; +import { reducer as visualizationReducer } from './visualization_slice'; +import { reducer as metadataReducer } from './metadata_slice'; +import { WizardServices } from '../../..'; +import { getPreloadedState } from './preload'; +import { setEditorState } from './metadata_slice'; + +const rootReducer = combineReducers({ + style: styleReducer, + visualization: visualizationReducer, + metadata: metadataReducer, +}); + +export const configurePreloadedStore = (preloadedState: PreloadedState<RootState>) => { + return configureStore({ + reducer: rootReducer, + preloadedState, + }); +}; + +export const getPreloadedStore = async (services: WizardServices) => { + const preloadedState = await getPreloadedState(services); + const store = configurePreloadedStore(preloadedState); + + const { metadata: metadataState, style: styleState, visualization: vizState } = store.getState(); + let previousStore = { + viz: vizState, + style: styleState, + }; + let previousMetadata = metadataState; + + // Listen to changes + const handleChange = () => { + const { + metadata: currentMetadataState, + style: currentStyleState, + visualization: currentVizState, + } = store.getState(); + const currentStore = { + viz: currentVizState, + style: currentStyleState, + }; + const currentMetadata = currentMetadataState; + + // Need to make sure the editorStates are in the clean states(not the initial states) to indicate the viz finished loading + // Because when loading a saved viz from saved object, the previousStore will differ from + // the currentStore even tho there is no changes applied ( aggParams will + // first be empty, and it then will change to not empty once the viz finished loading) + if ( + previousMetadata.editor.state === 'clean' && + currentMetadata.editor.state === 'clean' && + JSON.stringify(currentStore) !== JSON.stringify(previousStore) + ) { + store.dispatch(setEditorState({ state: 'dirty' })); + } + + previousStore = currentStore; + previousMetadata = currentMetadata; + }; + + // the store subscriber will automatically detect changes and call handleChange function + store.subscribe(handleChange); + + return store; +}; + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType<typeof rootReducer>; +type Store = ReturnType<typeof configurePreloadedStore>; +export type AppDispatch = Store['dispatch']; + +export { setState as setStyleState, StyleState } from './style_slice'; +export { setState as setVisualizationState, VisualizationState } from './visualization_slice'; diff --git a/src/plugins/wizard/public/application/utils/state_management/style_slice.ts b/src/plugins/wizard/public/application/utils/state_management/style_slice.ts new file mode 100644 index 000000000000..9fb0cdedb763 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/style_slice.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { WizardServices } from '../../../types'; +import { setActiveVisualization } from './shared_actions'; + +export type StyleState<T = any> = T; + +const initialState = {} as StyleState; + +export const getPreloadedState = async ({ types, data }: WizardServices): Promise<StyleState> => { + let preloadedState = initialState; + + const defaultVisualization = types.all()[0]; + const defaultState = defaultVisualization.ui.containerConfig.style.defaults; + if (defaultState) { + preloadedState = defaultState; + } + + return preloadedState; +}; + +export const styleSlice = createSlice({ + name: 'style', + initialState, + reducers: { + setState<T>(state: T, action: PayloadAction<StyleState<T>>) { + return action.payload; + }, + updateState<T>(state: T, action: PayloadAction<Partial<StyleState<T>>>) { + state = { + ...state, + ...action.payload, + }; + }, + }, + extraReducers(builder) { + builder.addCase(setActiveVisualization, (state, action) => { + return action.payload.style; + }); + }, +}); + +// Exposing the state functions as generics +export const setState = styleSlice.actions.setState as <T>(payload: T) => PayloadAction<T>; +export const updateState = styleSlice.actions.updateState as <T>( + payload: Partial<T> +) => PayloadAction<Partial<T>>; + +export const { reducer } = styleSlice; diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts new file mode 100644 index 000000000000..cbb6c76a0758 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { CreateAggConfigParams } from '../../../../../data/common'; +import { WizardServices } from '../../../types'; +import { setActiveVisualization } from './shared_actions'; + +export interface VisualizationState { + indexPattern?: string; + searchField: string; + activeVisualization?: { + name: string; + aggConfigParams: CreateAggConfigParams[]; + draftAgg?: CreateAggConfigParams; + }; +} + +const initialState: VisualizationState = { + searchField: '', +}; + +export const getPreloadedState = async ({ + types, + data, +}: WizardServices): Promise<VisualizationState> => { + const preloadedState = { ...initialState }; + + const defaultVisualization = types.all()[0]; + const defaultIndexPattern = await data.indexPatterns.getDefault(); + const name = defaultVisualization.name; + if (name && defaultIndexPattern) { + preloadedState.activeVisualization = { + name, + aggConfigParams: [], + }; + + preloadedState.indexPattern = defaultIndexPattern.id; + } + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'visualization', + initialState, + reducers: { + setIndexPattern: (state, action: PayloadAction<string>) => { + state.indexPattern = action.payload; + state.activeVisualization!.aggConfigParams = []; + }, + setSearchField: (state, action: PayloadAction<string>) => { + state.searchField = action.payload; + }, + editDraftAgg: (state, action: PayloadAction<CreateAggConfigParams | undefined>) => { + state.activeVisualization!.draftAgg = action.payload; + }, + saveDraftAgg: (state, action: PayloadAction<undefined>) => { + const draftAgg = state.activeVisualization!.draftAgg; + + if (draftAgg) { + const aggIndex = state.activeVisualization!.aggConfigParams.findIndex( + (agg) => agg.id === draftAgg.id + ); + + if (aggIndex === -1) { + state.activeVisualization!.aggConfigParams.push(draftAgg); + } else { + state.activeVisualization!.aggConfigParams.splice(aggIndex, 1, draftAgg); + } + } + }, + reorderAgg: ( + state, + action: PayloadAction<{ + sourceId: string; + destinationId: string; + }> + ) => { + const { sourceId, destinationId } = action.payload; + const aggParams = state.activeVisualization!.aggConfigParams; + const newAggs = [...aggParams]; + const destinationIndex = newAggs.findIndex((agg) => agg.id === destinationId); + newAggs.splice( + destinationIndex, + 0, + newAggs.splice( + aggParams.findIndex((agg) => agg.id === sourceId), + 1 + )[0] + ); + + state.activeVisualization!.aggConfigParams = newAggs; + }, + updateAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams[]>) => { + state.activeVisualization!.aggConfigParams = action.payload; + }, + setState: (_state, action: PayloadAction<VisualizationState>) => { + return action.payload; + }, + }, + extraReducers(builder) { + builder.addCase(setActiveVisualization, (state, action) => { + state.activeVisualization = { + name: action.payload.name, + aggConfigParams: [], + }; + }); + }, +}); + +export const { reducer } = slice; +export const { + setIndexPattern, + setSearchField, + editDraftAgg, + saveDraftAgg, + updateAggConfigParams, + reorderAgg, + setState, +} = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts new file mode 100644 index 000000000000..e8d1087ce0a0 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useVisualizationType } from './use_visualization_type'; +export { useIndexPatterns } from './use_index_pattern'; +export { useSavedWizardVis } from './use_saved_wizard_vis'; diff --git a/src/plugins/wizard/public/application/utils/use/use_can_save.ts b/src/plugins/wizard/public/application/utils/use/use_can_save.ts new file mode 100644 index 000000000000..adfb15c03e48 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_can_save.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { useTypedSelector } from '../state_management'; + +export const useCanSave = () => { + const isEmpty = useTypedSelector( + (state) => state.visualization.activeVisualization?.aggConfigParams?.length === 0 + ); + const hasNoChange = useTypedSelector((state) => state.metadata.editor.state !== 'dirty'); + const hasDraftAgg = useTypedSelector( + (state) => !!state.visualization.activeVisualization?.draftAgg + ); + const errorMsg = getErrorMsg(isEmpty, hasNoChange, hasDraftAgg); + + return errorMsg; +}; + +// TODO: Need to finalize the error messages +const getErrorMsg = (isEmpty, hasNoChange, hasDraftAgg) => { + const i18nTranslate = (key: string, defaultMessage: string) => + i18n.translate(`wizard.saveVisualizationTooltip.${key}`, { + defaultMessage, + }); + + if (isEmpty) { + return i18nTranslate('empty', 'The canvas is empty. Add some aggregations before saving.'); + } else if (hasNoChange) { + return i18nTranslate('noChange', 'Add some changes before saving.'); + } else if (hasDraftAgg) { + return i18nTranslate( + 'hasDraftAgg', + 'Has unapplied aggregations changes, update them before saving.' + ); + } else { + return undefined; + } +}; diff --git a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx new file mode 100644 index 000000000000..b5c60ee20944 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { useEffect, useState } from 'react'; +import { IndexPattern } from '../../../../../data/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../types'; +import { useTypedSelector } from '../state_management'; + +export const useIndexPatterns = () => { + const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization); + const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>([]); + const [loading, setLoading] = useState<boolean>(true); + const [error, setError] = useState<Error | undefined>(undefined); + const { + services: { data }, + } = useOpenSearchDashboards<WizardServices>(); + + let foundSelected: IndexPattern; + if (!loading && !error) { + foundSelected = indexPatterns.filter((p) => p.id === indexId)[0]; + if (foundSelected === undefined) { + setError( + new Error("Attempted to select an index pattern that wasn't in the index pattern list") + ); + } + } + + useEffect(() => { + const handleUpdate = async () => { + try { + const ids = await data.indexPatterns.getIds(true); + const patterns = await Promise.all(ids.map((id) => data.indexPatterns.get(id))); + setIndexPatterns(patterns); + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + }; + + handleUpdate(); + }, [data.indexPatterns]); + + return { + indexPatterns, + error, + loading, + selected: foundSelected!, + }; +}; diff --git a/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts new file mode 100644 index 000000000000..db17e478a41c --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { useEffect, useState } from 'react'; +import { SavedObject } from '../../../../../saved_objects/public'; +import { + redirectWhenMissing, + SavedObjectNotFound, +} from '../../../../../opensearch_dashboards_utils/public'; +import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; +import { WizardServices } from '../../../types'; +import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz_type'; +import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; +import { getSavedWizardVis } from '../get_saved_wizard_vis'; +import { + useTypedDispatch, + setStyleState, + setVisualizationState, + VisualizationState, +} from '../state_management'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { setEditorState } from '../state_management/metadata_slice'; + +// This function can be used when instantiating a saved vis or creating a new one +// using url parameters, embedding and destroying it in DOM +export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => { + const { services } = useOpenSearchDashboards<WizardServices>(); + const [savedVisState, setSavedVisState] = useState<SavedObject | undefined>(undefined); + const dispatch = useTypedDispatch(); + + useEffect(() => { + const { + application: { navigateToApp }, + chrome, + history, + http: { basePath }, + toastNotifications, + } = services; + const loadSavedWizardVis = async () => { + try { + const savedWizardVis = await getSavedWizardVis(services, visualizationIdFromUrl); + + if (savedWizardVis.id) { + chrome.setBreadcrumbs(getEditBreadcrumbs(savedWizardVis.title, navigateToApp)); + chrome.docTitle.change(savedWizardVis.title); + } else { + chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp)); + } + + if (savedWizardVis.styleState !== '{}' && savedWizardVis.visualizationState !== '{}') { + const styleState = JSON.parse(savedWizardVis.styleState); + const vizStateWithoutIndex = JSON.parse(savedWizardVis.visualizationState); + const visualizationState: VisualizationState = { + searchField: vizStateWithoutIndex.searchField, + activeVisualization: vizStateWithoutIndex.activeVisualization, + indexPattern: savedWizardVis.searchSourceFields.index, + }; + // TODO: Add validation and transformation, throw/handle errors + dispatch(setStyleState<MetricOptionsDefaults>(styleState)); + dispatch(setVisualizationState(visualizationState)); + } + + setSavedVisState(savedWizardVis); + dispatch(setEditorState({ state: 'clean' })); + } catch (error) { + const managementRedirectTarget = { + [PLUGIN_ID]: { + app: 'management', + path: `opensearch-dashboards/objects/savedWizard/${visualizationIdFromUrl}`, + }, + }; + + try { + if (error instanceof SavedObjectNotFound) { + redirectWhenMissing({ + history, + navigateToApp, + toastNotifications, + basePath, + mapping: managementRedirectTarget, + })(error); + } + } catch (e) { + const message = e instanceof Error ? e.message : ''; + toastNotifications.addWarning({ + title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { + defaultMessage: 'Failed to load the visualization', + }), + text: message, + }); + history.replace(EDIT_PATH); + } + } + }; + + loadSavedWizardVis(); + }, [dispatch, services, visualizationIdFromUrl]); + + return savedVisState; +}; diff --git a/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts b/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts new file mode 100644 index 000000000000..002c83759b3c --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisualizationType } from '../../../services/type_service/visualization_type'; +import { WizardServices } from '../../../types'; +import { useTypedSelector } from '../state_management'; + +export const useVisualizationType = (): VisualizationType => { + const { activeVisualization } = useTypedSelector((state) => state.visualization); + const { + services: { types }, + } = useOpenSearchDashboards<WizardServices>(); + + const visualizationType = types.get(activeVisualization?.name ?? ''); + + if (!visualizationType) { + throw new Error(`Invalid visualization type ${activeVisualization}`); + } + + return visualizationType; +}; diff --git a/src/plugins/wizard/public/application/utils/validate_schema_state.ts b/src/plugins/wizard/public/application/utils/validate_schema_state.ts new file mode 100644 index 000000000000..2db3653e2ce3 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/validate_schema_state.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { countBy } from 'lodash'; +import { Schemas } from '../../../../vis_default_editor/public'; +import { RootState } from './state_management'; + +export const validateSchemaState = (schemas: Schemas, state: RootState): [boolean, string?] => { + const activeViz = state.visualization.activeVisualization; + const vizName = activeViz?.name; + const aggs = activeViz?.aggConfigParams; + + // Check if any aggreagations exist + if (aggs?.length === 0) { + return [false]; + } + + // Check if each schema's min agg requirement is met + const aggSchemaCount = countBy(aggs, (agg) => agg.schema); + const invalidsSchemas = schemas.all.filter((schema) => { + if (!schema.min) return false; + if (!aggSchemaCount[schema.name] || aggSchemaCount[schema.name] < schema.min) return true; + + return false; + }); + + if (invalidsSchemas.length > 0) { + return [ + false, + `The ${vizName} visualization needs at least ${invalidsSchemas[0].min} field(s) in the agg type "${invalidsSchemas[0].name}"`, + ]; + } + + return [true, '']; +}; diff --git a/src/plugins/wizard/public/assets/fields_bg.svg b/src/plugins/wizard/public/assets/fields_bg.svg new file mode 100644 index 000000000000..d7ac9e455f0c --- /dev/null +++ b/src/plugins/wizard/public/assets/fields_bg.svg @@ -0,0 +1,61 @@ +<svg width="243" height="186" viewBox="0 0 243 186" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M0 98.6C0 97.1641 1.14799 96 2.5641 96H97.4359C98.852 96 100 97.1641 100 98.6V119.4C100 120.836 98.852 122 97.4359 122H2.5641C1.14799 122 0 120.836 0 119.4V98.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 98.6H2.5641L2.5641 119.4H97.4359V98.6ZM2.5641 96C1.14799 96 0 97.1641 0 98.6V119.4C0 120.836 1.14799 122 2.5641 122H97.4359C98.852 122 100 120.836 100 119.4V98.6C100 97.1641 98.852 96 97.4359 96H2.5641Z" fill="#343741"/> +<path d="M15.155 103.091L14.6642 105.413H16.4707L16.0426 107.433H14.2361L13.359 111.723C13.3102 112.016 13.3329 112.238 13.4268 112.387C13.5208 112.537 13.7558 112.617 14.1317 112.629C14.2779 112.635 14.5772 112.62 15.0297 112.586L14.7791 114.692C14.2013 114.847 13.5852 114.919 12.9308 114.908C11.8657 114.896 11.0687 114.637 10.5396 114.131C10.0105 113.625 9.79474 112.937 9.8922 112.068L10.8111 107.433H9.41187L9.82955 105.413H11.2288L11.7196 103.091H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 109C22.353 108.347 22.9324 107.818 23.6471 107.818H34.0001C34.7148 107.818 35.2942 108.347 35.2942 109C35.2942 109.653 34.7148 110.182 34.0001 110.182H23.6471C22.9324 110.182 22.353 109.653 22.353 109Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 109C43.5293 108.347 44.0999 107.818 44.8038 107.818L88.1371 107.818C88.841 107.818 89.4117 108.347 89.4117 109C89.4117 109.653 88.841 110.182 88.1371 110.182L44.8038 110.182C44.0999 110.182 43.5293 109.653 43.5293 109Z" fill="#343741"/> +<path d="M0 66.6C0 65.1641 1.14799 64 2.5641 64H97.4359C98.852 64 100 65.1641 100 66.6V87.4C100 88.8359 98.852 90 97.4359 90H2.5641C1.14799 90 0 88.8359 0 87.4V66.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 66.6H2.5641L2.5641 87.4H97.4359V66.6ZM2.5641 64C1.14799 64 0 65.1641 0 66.6V87.4C0 88.8359 1.14799 90 2.5641 90H97.4359C98.852 90 100 88.8359 100 87.4V66.6C100 65.1641 98.852 64 97.4359 64H2.5641Z" fill="#343741"/> +<path d="M15.155 69.9092L14.6642 72.4633H16.4707L16.0426 74.6852H14.2361L13.359 79.4042C13.3102 79.727 13.3329 79.9707 13.4268 80.1353C13.5208 80.2999 13.7558 80.3885 14.1317 80.4012C14.2779 80.4075 14.5772 80.3917 15.0297 80.3537L14.7791 82.6705C14.2013 82.8414 13.5852 82.9205 12.9308 82.9079C11.8657 82.8952 11.0687 82.6104 10.5396 82.0533C10.0105 81.4963 9.79474 80.7398 9.8922 79.784L10.8111 74.6852H9.41187L9.82955 72.4633H11.2288L11.7196 69.9092H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 76.9999C22.353 76.3472 22.9324 75.8181 23.6471 75.8181H34.0001C34.7148 75.8181 35.2942 76.3472 35.2942 76.9999C35.2942 77.6526 34.7148 78.1818 34.0001 78.1818H23.6471C22.9324 78.1818 22.353 77.6526 22.353 76.9999Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 76.9999C43.5293 76.3472 44.0999 75.8181 44.8038 75.8181L88.1371 75.8181C88.841 75.8181 89.4117 76.3472 89.4117 76.9999C89.4117 77.6526 88.841 78.1818 88.1371 78.1818L44.8038 78.1817C44.0999 78.1817 43.5293 77.6526 43.5293 76.9999Z" fill="#343741"/> +<path d="M0 34.6C0 33.1641 1.14799 32 2.5641 32H97.4359C98.852 32 100 33.1641 100 34.6V55.4C100 56.8359 98.852 58 97.4359 58H2.5641C1.14799 58 0 56.8359 0 55.4V34.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 34.6H2.5641L2.5641 55.4H97.4359V34.6ZM2.5641 32C1.14799 32 0 33.1641 0 34.6V55.4C0 56.8359 1.14799 58 2.5641 58H97.4359C98.852 58 100 56.8359 100 55.4V34.6C100 33.1641 98.852 32 97.4359 32H2.5641Z" fill="#343741"/> +<path d="M15.155 37.9092L14.6642 40.4633H16.4707L16.0426 42.6852H14.2361L13.359 47.4042C13.3102 47.727 13.3329 47.9707 13.4268 48.1353C13.5208 48.2999 13.7558 48.3885 14.1317 48.4012C14.2779 48.4075 14.5772 48.3917 15.0297 48.3537L14.7791 50.6705C14.2013 50.8414 13.5852 50.9205 12.9308 50.9079C11.8657 50.8952 11.0687 50.6104 10.5396 50.0533C10.0105 49.4963 9.79474 48.7398 9.8922 47.784L10.8111 42.6852H9.41187L9.82955 40.4633H11.2288L11.7196 37.9092H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 44.9999C22.353 44.3472 22.9324 43.8181 23.6471 43.8181H34.0001C34.7148 43.8181 35.2942 44.3472 35.2942 44.9999C35.2942 45.6526 34.7148 46.1818 34.0001 46.1818H23.6471C22.9324 46.1818 22.353 45.6526 22.353 44.9999Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 44.9999C43.5293 44.3472 44.0999 43.8181 44.8038 43.8181L88.1371 43.8181C88.841 43.8181 89.4117 44.3472 89.4117 44.9999C89.4117 45.6526 88.841 46.1818 88.1371 46.1818L44.8038 46.1817C44.0999 46.1817 43.5293 45.6526 43.5293 44.9999Z" fill="#343741"/> +<path d="M0 2.6C0 1.16406 1.14799 0 2.5641 0H97.4359C98.852 0 100 1.16406 100 2.6V23.4C100 24.8359 98.852 26 97.4359 26H2.5641C1.14799 26 0 24.8359 0 23.4V2.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 2.6H2.5641L2.5641 23.4H97.4359V2.6ZM2.5641 0C1.14799 0 0 1.16406 0 2.6V23.4C0 24.8359 1.14799 26 2.5641 26H97.4359C98.852 26 100 24.8359 100 23.4V2.6C100 1.16406 98.852 0 97.4359 0H2.5641Z" fill="#343741"/> +<path d="M15.155 5.90918L14.6642 8.46334H16.4707L16.0426 10.6852H14.2361L13.359 15.4042C13.3102 15.727 13.3329 15.9707 13.4268 16.1353C13.5208 16.2999 13.7558 16.3885 14.1317 16.4012C14.2779 16.4075 14.5772 16.3917 15.0297 16.3537L14.7791 18.6705C14.2013 18.8414 13.5852 18.9205 12.9308 18.9079C11.8657 18.8952 11.0687 18.6104 10.5396 18.0533C10.0105 17.4963 9.79474 16.7398 9.8922 15.784L10.8111 10.6852H9.41187L9.82955 8.46334H11.2288L11.7196 5.90918H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 12.9999C22.353 12.3472 22.9324 11.8181 23.6471 11.8181H34.0001C34.7148 11.8181 35.2942 12.3472 35.2942 12.9999C35.2942 13.6526 34.7148 14.1818 34.0001 14.1818H23.6471C22.9324 14.1818 22.353 13.6526 22.353 12.9999Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 12.9999C43.5293 12.3472 44.0999 11.8181 44.8038 11.8181L88.1371 11.8181C88.841 11.8181 89.4117 12.3472 89.4117 12.9999C89.4117 13.6526 88.841 14.1818 88.1371 14.1818L44.8038 14.1817C44.0999 14.1817 43.5293 13.6526 43.5293 12.9999Z" fill="#343741"/> +<path d="M0 130.6C0 129.164 1.14799 128 2.5641 128H97.4359C98.852 128 100 129.164 100 130.6V151.4C100 152.836 98.852 154 97.4359 154H2.5641C1.14799 154 0 152.836 0 151.4V130.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 130.6H2.5641L2.5641 151.4H97.4359V130.6ZM2.5641 128C1.14799 128 0 129.164 0 130.6V151.4C0 152.836 1.14799 154 2.5641 154H97.4359C98.852 154 100 152.836 100 151.4V130.6C100 129.164 98.852 128 97.4359 128H2.5641Z" fill="#343741"/> +<path d="M12.6653 143.581H11.4987L10.4188 146.909H8.83443L9.91433 143.581H8.23535L8.48759 142.096H10.3873L11.0888 139.953H9.41773L9.67786 138.468H11.5697L12.6653 135.091H14.2419L13.1462 138.468H14.3207L15.4164 135.091H17.0007L15.9051 138.468H17.6471L17.3949 139.953H15.4321L14.7306 142.096H16.4569L16.2046 143.581H14.2497L13.1777 146.909H11.5933L12.6653 143.581ZM11.9717 142.096H13.1383L13.8477 139.953H12.6732L11.9717 142.096Z" fill="#387765"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 141C22.353 140.347 22.9324 139.818 23.6471 139.818H34.0001C34.7148 139.818 35.2942 140.347 35.2942 141C35.2942 141.653 34.7148 142.182 34.0001 142.182H23.6471C22.9324 142.182 22.353 141.653 22.353 141Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 141C43.5293 140.347 44.0999 139.818 44.8038 139.818L88.1371 139.818C88.841 139.818 89.4117 140.347 89.4117 141C89.4117 141.653 88.841 142.182 88.1371 142.182L44.8038 142.182C44.0999 142.182 43.5293 141.653 43.5293 141Z" fill="#343741"/> +<path d="M0 162.6C0 161.164 1.14799 160 2.5641 160H97.4359C98.852 160 100 161.164 100 162.6V183.4C100 184.836 98.852 186 97.4359 186H2.5641C1.14799 186 0 184.836 0 183.4V162.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 162.6H2.5641L2.5641 183.4H97.4359V162.6ZM2.5641 160C1.14799 160 0 161.164 0 162.6V183.4C0 184.836 1.14799 186 2.5641 186H97.4359C98.852 186 100 184.836 100 183.4V162.6C100 161.164 98.852 160 97.4359 160H2.5641Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8236 178.398C18.8236 179.333 18.1451 180.091 17.3066 180.091H9.75239C8.91455 180.091 8.23535 179.334 8.23535 178.398V169.966C8.23535 169.031 8.9138 168.273 9.75239 168.273H11.4118V167.687C11.4118 167.395 11.6059 167.151 11.8483 167.101L11.9412 167.091C12.2336 167.091 12.4706 167.344 12.4706 167.687V168.273H14.5883V167.687C14.5883 167.395 14.7823 167.151 15.0248 167.101L15.1177 167.091C15.4101 167.091 15.6471 167.344 15.6471 167.687V168.273H17.3066C18.1444 168.273 18.8236 169.03 18.8236 169.966V178.398ZM9.29418 171.818V178.117C9.29418 178.555 9.61176 178.909 10.0042 178.909H17.0547C17.4473 178.909 17.7648 178.555 17.7648 178.117V171.818H9.29418ZM10.8824 176.545C11.1423 176.545 11.3585 176.762 11.4033 177.033L11.4118 177.136C11.4118 177.426 11.2178 177.668 10.9753 177.718L10.8824 177.727C10.59 177.727 10.353 177.453 10.353 177.136C10.353 176.846 10.547 176.605 10.7895 176.555L10.8824 176.545ZM13.5295 176.545C13.7894 176.545 14.0055 176.762 14.0504 177.033L14.0589 177.136C14.0589 177.426 13.8648 177.668 13.6224 177.718L13.5295 177.727C13.2371 177.727 13.0001 177.453 13.0001 177.136C13.0001 176.846 13.1941 176.605 13.4366 176.555L13.5295 176.545ZM16.1765 176.545C16.4364 176.545 16.6526 176.762 16.6974 177.033L16.7059 177.136C16.7059 177.426 16.5119 177.668 16.2694 177.718L16.1765 177.727C15.8841 177.727 15.6471 177.453 15.6471 177.136C15.6471 176.846 15.8412 176.605 16.0836 176.555L16.1765 176.545ZM10.8824 174.773C11.1423 174.773 11.3585 174.989 11.4033 175.26L11.4118 175.364C11.4118 175.654 11.2178 175.895 10.9753 175.945L10.8824 175.955C10.59 175.955 10.353 175.68 10.353 175.364C10.353 175.074 10.547 174.832 10.7895 174.782L10.8824 174.773ZM13.5295 174.773C13.7894 174.773 14.0055 174.989 14.0504 175.26L14.0589 175.364C14.0589 175.654 13.8648 175.895 13.6224 175.945L13.5295 175.955C13.2371 175.955 13.0001 175.68 13.0001 175.364C13.0001 175.074 13.1941 174.832 13.4366 174.782L13.5295 174.773ZM16.1765 174.773C16.4364 174.773 16.6526 174.989 16.6974 175.26L16.7059 175.364C16.7059 175.654 16.5119 175.895 16.2694 175.945L16.1765 175.955C15.8841 175.955 15.6471 175.68 15.6471 175.364C15.6471 175.074 15.8412 174.832 16.0836 174.782L16.1765 174.773ZM10.8824 173C11.1423 173 11.3585 173.217 11.4033 173.487L11.4118 173.591C11.4118 173.881 11.2178 174.122 10.9753 174.172L10.8824 174.182C10.59 174.182 10.353 173.908 10.353 173.591C10.353 173.301 10.547 173.06 10.7895 173.01L10.8824 173ZM13.5295 173C13.7894 173 14.0055 173.217 14.0504 173.487L14.0589 173.591C14.0589 173.881 13.8648 174.122 13.6224 174.172L13.5295 174.182C13.2371 174.182 13.0001 173.908 13.0001 173.591C13.0001 173.301 13.1941 173.06 13.4366 173.01L13.5295 173ZM16.1765 173C16.4364 173 16.6526 173.217 16.6974 173.487L16.7059 173.591C16.7059 173.881 16.5119 174.122 16.2694 174.172L16.1765 174.182C15.8841 174.182 15.6471 173.908 15.6471 173.591C15.6471 173.301 15.8412 173.06 16.0836 173.01L16.1765 173ZM9.29418 170.636H17.7648V170.247C17.7648 169.809 17.4472 169.455 17.0547 169.455H10.0042C9.61166 169.455 9.29418 169.809 9.29418 170.247V170.636Z" fill="#7B705A"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 173C22.353 172.347 22.9324 171.818 23.6471 171.818H34.0001C34.7148 171.818 35.2942 172.347 35.2942 173C35.2942 173.653 34.7148 174.182 34.0001 174.182H23.6471C22.9324 174.182 22.353 173.653 22.353 173Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 173C43.5293 172.347 44.0999 171.818 44.8038 171.818L88.1371 171.818C88.841 171.818 89.4117 172.347 89.4117 173C89.4117 173.653 88.841 174.182 88.1371 174.182L44.8038 174.182C44.0999 174.182 43.5293 173.653 43.5293 173Z" fill="#343741"/> +</g> +<g opacity="0.5"> +<path d="M143 66.6C143 65.1641 144.148 64 145.564 64H240.436C241.852 64 243 65.1641 243 66.6V87.4C243 88.8359 241.852 90 240.436 90H145.564C144.148 90 143 88.8359 143 87.4V66.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 66.6H145.564L145.564 87.4H240.436V66.6ZM145.564 64C144.148 64 143 65.1641 143 66.6V87.4C143 88.8359 144.148 90 145.564 90H240.436C241.852 90 243 88.8359 243 87.4V66.6C243 65.1641 241.852 64 240.436 64H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 77C220 76.4477 220.149 76 220.333 76L231.667 76C231.851 76 232 76.4477 232 77C232 77.5523 231.851 78 231.667 78L220.333 78C220.149 78 220 77.5523 220 77Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 71C226.552 71 227 71.1492 227 71.3333L227 82.6667C227 82.8508 226.552 83 226 83C225.448 83 225 82.8508 225 82.6667L225 71.3333C225 71.1492 225.448 71 226 71Z" fill="#343741"/> +<path d="M143 98.6C143 97.1641 144.148 96 145.564 96H240.436C241.852 96 243 97.1641 243 98.6V119.4C243 120.836 241.852 122 240.436 122H145.564C144.148 122 143 120.836 143 119.4V98.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 98.6H145.564L145.564 119.4H240.436V98.6ZM145.564 96C144.148 96 143 97.1641 143 98.6V119.4C143 120.836 144.148 122 145.564 122H240.436C241.852 122 243 120.836 243 119.4V98.6C243 97.1641 241.852 96 240.436 96H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 109C220 108.448 220.149 108 220.333 108L231.667 108C231.851 108 232 108.448 232 109C232 109.552 231.851 110 231.667 110L220.333 110C220.149 110 220 109.552 220 109Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 103C226.552 103 227 103.149 227 103.333L227 114.667C227 114.851 226.552 115 226 115C225.448 115 225 114.851 225 114.667L225 103.333C225 103.149 225.448 103 226 103Z" fill="#343741"/> +<path d="M143 130.6C143 129.164 144.148 128 145.564 128H240.436C241.852 128 243 129.164 243 130.6V151.4C243 152.836 241.852 154 240.436 154H145.564C144.148 154 143 152.836 143 151.4V130.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 130.6H145.564L145.564 151.4H240.436V130.6ZM145.564 128C144.148 128 143 129.164 143 130.6V151.4C143 152.836 144.148 154 145.564 154H240.436C241.852 154 243 152.836 243 151.4V130.6C243 129.164 241.852 128 240.436 128H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 141C220 140.448 220.149 140 220.333 140L231.667 140C231.851 140 232 140.448 232 141C232 141.552 231.851 142 231.667 142L220.333 142C220.149 142 220 141.552 220 141Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 135C226.552 135 227 135.149 227 135.333L227 146.667C227 146.851 226.552 147 226 147C225.448 147 225 146.851 225 146.667L225 135.333C225 135.149 225.448 135 226 135Z" fill="#343741"/> +<path d="M143 162.6C143 161.164 144.148 160 145.564 160H240.436C241.852 160 243 161.164 243 162.6V183.4C243 184.836 241.852 186 240.436 186H145.564C144.148 186 143 184.836 143 183.4V162.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 162.6H145.564L145.564 183.4H240.436V162.6ZM145.564 160C144.148 160 143 161.164 143 162.6V183.4C143 184.836 144.148 186 145.564 186H240.436C241.852 186 243 184.836 243 183.4V162.6C243 161.164 241.852 160 240.436 160H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 173C220 172.448 220.149 172 220.333 172L231.667 172C231.851 172 232 172.448 232 173C232 173.552 231.851 174 231.667 174L220.333 174C220.149 174 220 173.552 220 173Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 167C226.552 167 227 167.149 227 167.333L227 178.667C227 178.851 226.552 179 226 179C225.448 179 225 178.851 225 178.667L225 167.333C225 167.149 225.448 167 226 167Z" fill="#343741"/> +<path d="M143 34.6C143 33.1641 144.148 32 145.564 32H240.436C241.852 32 243 33.1641 243 34.6V55.4C243 56.8359 241.852 58 240.436 58H145.564C144.148 58 143 56.8359 143 55.4V34.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 34.6H145.564L145.564 55.4H240.436V34.6ZM145.564 32C144.148 32 143 33.1641 143 34.6V55.4C143 56.8359 144.148 58 145.564 58H240.436C241.852 58 243 56.8359 243 55.4V34.6C243 33.1641 241.852 32 240.436 32H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 45C220 44.4477 220.149 44 220.333 44L231.667 44C231.851 44 232 44.4477 232 45C232 45.5523 231.851 46 231.667 46L220.333 46C220.149 46 220 45.5523 220 45Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 39C226.552 39 227 39.1492 227 39.3333L227 50.6667C227 50.8508 226.552 51 226 51C225.448 51 225 50.8508 225 50.6667L225 39.3333C225 39.1492 225.448 39 226 39Z" fill="#343741"/> +<path d="M143 2.6C143 1.16406 144.148 0 145.564 0H240.436C241.852 0 243 1.16406 243 2.6V23.4C243 24.8359 241.852 26 240.436 26H145.564C144.148 26 143 24.8359 143 23.4V2.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 2.6H145.564L145.564 23.4H240.436V2.6ZM145.564 0C144.148 0 143 1.16406 143 2.6V23.4C143 24.8359 144.148 26 145.564 26H240.436C241.852 26 243 24.8359 243 23.4V2.6C243 1.16406 241.852 0 240.436 0H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 13C220 12.4477 220.149 12 220.333 12L231.667 12C231.851 12 232 12.4477 232 13C232 13.5523 231.851 14 231.667 14L220.333 14C220.149 14 220 13.5523 220 13Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 7C226.552 7 227 7.14924 227 7.33333L227 18.6667C227 18.8508 226.552 19 226 19C225.448 19 225 18.8508 225 18.6667L225 7.33333C225 7.14924 225.448 7 226 7Z" fill="#343741"/> +</g> +<line opacity="0.5" x1="121.5" y1="1.5" x2="121.5" y2="184.5" stroke="#343741" stroke-width="3" stroke-linecap="round"/> +</svg> diff --git a/src/plugins/wizard/public/assets/hand_field.svg b/src/plugins/wizard/public/assets/hand_field.svg new file mode 100644 index 000000000000..8c38e60edd59 --- /dev/null +++ b/src/plugins/wizard/public/assets/hand_field.svg @@ -0,0 +1,44 @@ +<svg width="206" height="68" viewBox="0 0 206 68" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g filter="url(#filter0_d_268_49515)"> +<rect x="2" y="2" width="176" height="40" rx="2" fill="#EFF4F9" stroke="#343741" stroke-width="4"/> +<path d="M23.6411 10L22.8172 14.7154H25.8498L25.1311 18.8172H22.0985L20.6261 27.5293C20.5443 28.1253 20.5822 28.5752 20.74 28.879C20.8978 29.1829 21.2922 29.3465 21.9232 29.3699C22.1686 29.3815 22.6711 29.3523 23.4308 29.2822L23.01 33.5593C22.0401 33.8749 21.0059 34.021 19.9074 33.9976C18.1194 33.9742 16.7813 33.4483 15.8932 32.4199C15.005 31.3916 14.6427 29.9951 14.8063 28.2304L16.3489 18.8172H14L14.7012 14.7154H17.0501L17.874 10H23.6411Z" fill="#4A7194"/> +<line x1="37" y1="22" x2="53" y2="22" stroke="#343741" stroke-width="4" stroke-linecap="round"/> +<line x1="69" y1="22" x2="162" y2="22" stroke="#343741" stroke-width="4" stroke-linecap="round"/> +</g> +<g filter="url(#filter1_d_268_49515)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M192.347 61.1429L189.898 56.2449L187.449 61.1429H177.653V58.6045C177.653 56.5994 169.816 51.0126 169.816 51.0126C168.734 50.2086 167.857 48.4761 167.857 47.1112V38.2857C167.857 35.4807 170.05 33.2064 172.755 33.2064V40.8254H175.204V28.974C175.204 27.8045 176.118 26.8572 177.245 26.8572C179.5 26.8572 181.327 28.7518 181.327 31.0896V30.2438C181.327 29.0743 182.24 28.127 183.368 28.127C185.622 28.127 187.449 30.0216 187.449 32.3594V31.5137C187.449 30.3442 188.362 29.3969 189.49 29.3062C191.745 29.3969 193.571 31.2915 193.571 33.6292V32.7835C193.571 31.614 194.485 30.6667 195.613 30.6667C197.867 30.6667 199.694 32.5613 199.694 34.8991V46.4596C199.694 47.8063 199.113 49.8316 198.407 50.9975C198.407 50.9975 194.796 56.5994 194.796 58.6045V61.1429H192.347ZM182.551 41.551H185V51.3469H182.551V41.551ZM187.449 41.551H189.898V51.3469H187.449V41.551ZM192.347 41.551H194.796V51.3469H192.347V41.551Z" fill="white"/> +</g> +<g filter="url(#filter2_d_268_49515)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M189.898 56.2449L192.347 61.1429H194.796V58.6044C194.796 57.6331 195.643 55.8178 196.517 54.2041C196.916 53.4667 197.321 52.7714 197.653 52.218C198.094 51.4824 198.407 50.9974 198.407 50.9974C199.113 49.8315 199.694 47.8062 199.694 46.4595V34.899C199.694 32.6255 197.966 30.7711 195.797 30.6709C195.736 30.6681 195.675 30.6667 195.613 30.6667C194.485 30.6667 193.571 31.614 193.571 32.7835V33.6292C193.571 33.3396 193.543 33.0569 193.49 32.7835C193.258 31.5969 192.548 30.587 191.578 29.9655C191.005 29.5985 190.342 29.3669 189.632 29.3142C189.585 29.3107 189.538 29.308 189.49 29.3061C188.362 29.3968 187.449 30.3441 187.449 31.5137V32.3594C187.449 32.0697 187.421 31.7869 187.368 31.5137C187.134 30.3206 186.417 29.3099 185.438 28.7111C184.88 28.3697 184.237 28.1622 183.549 28.1311C183.489 28.1284 183.429 28.127 183.368 28.127C182.24 28.127 181.327 29.0743 181.327 30.2438V31.0895C181.327 30.7999 181.298 30.5171 181.245 30.2438C181.012 29.0529 180.297 28.0436 179.321 27.4444C178.713 27.0713 178.003 26.8571 177.245 26.8571C176.118 26.8571 175.204 27.8044 175.204 28.974V40.8254H172.755V33.2063C172.618 33.2063 172.482 33.2122 172.347 33.2237C171.438 33.3016 170.598 33.6371 169.898 34.1597C168.662 35.0818 167.857 36.5864 167.857 38.2857V47.1112C167.857 48.4761 168.734 50.2086 169.816 51.0125C169.816 51.0125 177.653 56.5994 177.653 58.6044V61.1429H187.449L189.898 56.2449ZM187.449 41.551V51.3469H189.898V41.551H187.449ZM185 41.551H182.551V51.3469H185V41.551ZM192.347 41.551V51.3469H194.796V41.551H192.347ZM168.157 53.3389L168.134 53.3226L168.112 53.3057C167.123 52.5713 166.376 51.5446 165.879 50.5568C165.381 49.5683 165 48.3521 165 47.1112V38.2857C165 34.1356 168.167 30.5784 172.347 30.3598V28.974C172.347 26.3253 174.443 24 177.245 24C178.93 24 180.445 24.6015 181.627 25.5894C182.163 25.3836 182.748 25.2698 183.368 25.2698C185.037 25.2698 186.541 25.8607 187.718 26.833C188.201 26.6309 188.721 26.5017 189.261 26.4582L189.433 26.4444L189.605 26.4513C191.236 26.5169 192.72 27.1363 193.884 28.1243C194.417 27.9215 194.998 27.8095 195.613 27.8095C199.542 27.8095 202.551 31.0821 202.551 34.899V46.4595C202.551 47.484 202.341 48.6031 202.063 49.5784C201.784 50.5573 201.376 51.6108 200.851 52.477L200.83 52.5115M200.809 52.5451L200.802 52.5553L200.776 52.5959L200.671 52.7625C200.578 52.9094 200.445 53.1241 200.284 53.389C199.96 53.921 199.532 54.6442 199.108 55.4197C198.679 56.2043 198.28 56.9962 197.995 57.6751C197.777 58.1941 197.698 58.4841 197.669 58.5894C197.654 58.6466 197.653 58.6493 197.653 58.6044V64H190.581L189.898 62.6337L189.215 64H174.796V59.0728C174.695 58.933 174.54 58.7411 174.317 58.4975C173.708 57.8293 172.84 57.037 171.896 56.2435C170.967 55.4622 170.029 54.7334 169.318 54.1965C168.964 53.9292 168.67 53.7121 168.466 53.5629C168.364 53.4884 168.285 53.431 168.232 53.3929L168.174 53.3506L168.157 53.3389M174.933 59.2893C174.932 59.289 174.924 59.2751 174.912 59.2486C174.928 59.2764 174.934 59.2896 174.933 59.2893Z" fill="#343741"/> +</g> +<defs> +<filter id="filter0_d_268_49515" x="0" y="0" width="188" height="52" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dx="4" dy="4"/> +<feGaussianBlur stdDeviation="2"/> +<feComposite in2="hardAlpha" operator="out"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> +</filter> +<filter id="filter1_d_268_49515" x="164.857" y="24.8572" width="37.8367" height="40.2856" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> +</filter> +<filter id="filter2_d_268_49515" x="162" y="22" width="43.551" height="46" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> +</filter> +</defs> +</svg> diff --git a/src/plugins/wizard/public/assets/wizard_icon.svg b/src/plugins/wizard/public/assets/wizard_icon.svg new file mode 100644 index 000000000000..69da8016eb12 --- /dev/null +++ b/src/plugins/wizard/public/assets/wizard_icon.svg @@ -0,0 +1,25 @@ +<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 3C10 2.44771 10.4477 2 11 2H29C29.5523 2 30 2.44772 30 3V12C30 12.5523 29.5523 13 29 13H11C10.4477 13 10 12.5523 10 12V3Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M29 3H11V12H29V3ZM11 2C10.4477 2 10 2.44771 10 3V12C10 12.5523 10.4477 13 11 13H29C29.5523 13 30 12.5523 30 12V3C30 2.44772 29.5523 2 29 2H11Z" fill="#343741"/> +<path d="M2 10C2 9.44771 2.44772 9 3 9H21C21.5523 9 22 9.44772 22 10V19C22 19.5523 21.5523 20 21 20H3C2.44772 20 2 19.5523 2 19V10Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21 10H3V19H21V10ZM3 9C2.44772 9 2 9.44771 2 10V19C2 19.5523 2.44772 20 3 20H21C21.5523 20 22 19.5523 22 19V10C22 9.44772 21.5523 9 21 9H3Z" fill="#343741"/> +<g filter="url(#filter0_d_38_310)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4285 29L22.5714 27.2857L21.7143 29H18.2857V28.1116C18.2857 27.4098 15.5427 25.4544 15.5427 25.4544C15.1641 25.173 14.8571 24.5666 14.8571 24.0889V21C14.8571 20.0182 15.6247 19.2222 16.5714 19.2222V21.8889H17.4285V17.7409C17.4285 17.3316 17.7483 17 18.143 17C18.932 17 19.5714 17.6631 19.5714 18.4813V18.1853C19.5714 17.776 19.8911 17.4444 20.2858 17.4444C21.0748 17.4444 21.7143 18.1076 21.7143 18.9258V18.6298C21.7143 18.2204 22.034 17.8889 22.4287 17.8571C23.2177 17.8889 23.8571 18.552 23.8571 19.3702V19.0742C23.8571 18.6649 24.1768 18.3333 24.5715 18.3333C25.3605 18.3333 26 18.9964 26 19.8147V23.8608C26 24.3322 25.7966 25.041 25.5496 25.4491C25.5496 25.4491 24.2857 27.4098 24.2857 28.1116V29H23.4285ZM20 22.1428H20.8571V25.5714H20V22.1428V22.1428ZM21.7142 22.1428H22.5714V25.5714H21.7142V22.1428V22.1428ZM23.4286 22.1428H24.2857V25.5714H23.4286V22.1428V22.1428Z" fill="white"/> +</g> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5714 27.2857L23.4285 29H24.2857V28.1116C24.2857 27.7716 24.5823 27.1362 24.8881 26.5714C25.0278 26.3133 25.1695 26.07 25.2857 25.8763C25.4402 25.6188 25.5496 25.4491 25.5496 25.4491C25.7966 25.041 26 24.3322 26 23.8608V19.8147C26 19.0189 25.3952 18.3699 24.6362 18.3348C24.6148 18.3338 24.5932 18.3333 24.5715 18.3333C24.1768 18.3333 23.8571 18.6649 23.8571 19.0742V19.3702C23.8571 19.2689 23.8473 19.1699 23.8286 19.0742C23.7475 18.6589 23.4991 18.3055 23.1595 18.0879C22.959 17.9595 22.7267 17.8784 22.4782 17.86C22.4618 17.8588 22.4453 17.8578 22.4287 17.8571C22.034 17.8889 21.7143 18.2204 21.7143 18.6298V18.9258C21.7143 18.8244 21.7044 18.7254 21.6857 18.6298C21.6041 18.2122 21.3532 17.8585 21.0104 17.6489C20.8151 17.5294 20.5899 17.4568 20.3493 17.4459C20.3283 17.4449 20.3071 17.4444 20.2858 17.4444C19.8911 17.4444 19.5714 17.776 19.5714 18.1853V18.4813C19.5714 18.38 19.5616 18.281 19.5429 18.1853C19.4614 17.7685 19.2112 17.4153 18.8694 17.2055C18.6566 17.0749 18.4082 17 18.143 17C17.7483 17 17.4285 17.3316 17.4285 17.7409V21.8889H16.5714V19.2222C16.5233 19.2222 16.4756 19.2243 16.4285 19.2283C16.1102 19.2555 15.8166 19.373 15.5714 19.5559C15.1388 19.8786 14.8571 20.4052 14.8571 21V24.0889C14.8571 24.5666 15.1641 25.173 15.5427 25.4544C15.5427 25.4544 18.2857 27.4098 18.2857 28.1116V29H21.7143L22.5714 27.2857ZM21.7142 22.1428V25.5714H22.5714V22.1428H21.7142ZM20.8571 22.1428H20V25.5714H20.8571V22.1428ZM23.4286 22.1428V25.5714H24.2857V22.1428H23.4286ZM14.9622 26.2686L14.9542 26.2629L14.9462 26.257C14.6003 25.9999 14.3387 25.6406 14.1647 25.2949C13.9905 24.9489 13.8571 24.5232 13.8571 24.0889V21C13.8571 19.5475 14.9657 18.3024 16.4285 18.2259V17.7409C16.4285 16.8139 17.162 16 18.143 16C18.7325 16 19.2629 16.2105 19.6765 16.5563C19.864 16.4843 20.0689 16.4444 20.2858 16.4444C20.8702 16.4444 21.3963 16.6512 21.8083 16.9915C21.9775 16.9208 22.1593 16.8756 22.3485 16.8604L22.4086 16.8555L22.4689 16.858C23.0398 16.8809 23.5592 17.0977 23.9666 17.4435C24.1529 17.3725 24.3563 17.3333 24.5715 17.3333C25.9468 17.3333 27 18.4787 27 19.8147V23.8608C27 24.2194 26.9264 24.6111 26.8292 24.9524C26.7317 25.2951 26.5886 25.6638 26.405 25.967L26.3977 25.979M26.3901 25.9908L26.3879 25.9943L26.3788 26.0086L26.3419 26.0669C26.3096 26.1183 26.2628 26.1934 26.2064 26.2861C26.0931 26.4724 25.9435 26.7255 25.795 26.9969C25.6449 27.2715 25.505 27.5487 25.4053 27.7863C25.3291 27.9679 25.3015 28.0694 25.2914 28.1063C25.2859 28.1263 25.2857 28.1272 25.2857 28.1116V30H22.8105L22.5714 29.5218L22.3323 30H17.2857V28.2755C17.2502 28.2266 17.1959 28.1594 17.1182 28.0741C16.9049 27.8403 16.601 27.563 16.2706 27.2852C15.9454 27.0118 15.6172 26.7567 15.3685 26.5688C15.2446 26.4752 15.1417 26.3992 15.0703 26.347C15.0347 26.3209 15.0069 26.3008 14.9885 26.2875L14.9679 26.2727L14.9622 26.2686M17.3337 28.3513C17.3337 28.3513 17.3305 28.3463 17.3265 28.337C17.3321 28.3467 17.3337 28.3513 17.3337 28.3513Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9 15H5V14H9V15Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M17 8H13V7H17V8Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20 15H11V14H20V15Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M28 8H19V7H28V8Z" fill="#343741"/> +<defs> +<filter id="filter0_d_38_310" x="11.8571" y="15" width="17.1429" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_38_310"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_38_310" result="shape"/> +</filter> +</defs> +</svg> diff --git a/src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg b/src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg new file mode 100644 index 000000000000..cdaad42f6276 --- /dev/null +++ b/src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg @@ -0,0 +1,25 @@ +<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 3C10 2.44771 10.4477 2 11 2H29C29.5523 2 30 2.44772 30 3V12C30 12.5523 29.5523 13 29 13H11C10.4477 13 10 12.5523 10 12V3Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M29 3H11V12H29V3ZM11 2C10.4477 2 10 2.44771 10 3V12C10 12.5523 10.4477 13 11 13H29C29.5523 13 30 12.5523 30 12V3C30 2.44772 29.5523 2 29 2H11Z" fill="#017D73"/> +<path d="M2 10C2 9.44771 2.44772 9 3 9H21C21.5523 9 22 9.44772 22 10V19C22 19.5523 21.5523 20 21 20H3C2.44772 20 2 19.5523 2 19V10Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21 10H3V19H21V10ZM3 9C2.44772 9 2 9.44771 2 10V19C2 19.5523 2.44772 20 3 20H21C21.5523 20 22 19.5523 22 19V10C22 9.44772 21.5523 9 21 9H3Z" fill="#017D73"/> +<g filter="url(#filter0_d_201_310)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4285 29L22.5714 27.2857L21.7143 29H18.2857V28.1116C18.2857 27.4098 15.5427 25.4544 15.5427 25.4544C15.1641 25.173 14.8571 24.5666 14.8571 24.0889V21C14.8571 20.0182 15.6247 19.2222 16.5714 19.2222V21.8889H17.4285V17.7409C17.4285 17.3316 17.7483 17 18.143 17C18.932 17 19.5714 17.6631 19.5714 18.4813V18.1853C19.5714 17.776 19.8911 17.4444 20.2858 17.4444C21.0748 17.4444 21.7143 18.1076 21.7143 18.9258V18.6298C21.7143 18.2204 22.034 17.8889 22.4287 17.8571C23.2177 17.8889 23.8571 18.552 23.8571 19.3702V19.0742C23.8571 18.6649 24.1768 18.3333 24.5715 18.3333C25.3605 18.3333 26 18.9964 26 19.8147V23.8608C26 24.3322 25.7966 25.041 25.5496 25.4491C25.5496 25.4491 24.2857 27.4098 24.2857 28.1116V29H23.4285ZM20 22.1428H20.8571V25.5714H20V22.1428V22.1428ZM21.7142 22.1428H22.5714V25.5714H21.7142V22.1428V22.1428ZM23.4286 22.1428H24.2857V25.5714H23.4286V22.1428V22.1428Z" fill="white"/> +</g> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5714 27.2857L23.4285 29H24.2857V28.1116C24.2857 27.7716 24.5823 27.1362 24.8881 26.5714C25.0278 26.3133 25.1695 26.07 25.2857 25.8763C25.4402 25.6188 25.5496 25.4491 25.5496 25.4491C25.7966 25.041 26 24.3322 26 23.8608V19.8147C26 19.0189 25.3952 18.3699 24.6362 18.3348C24.6148 18.3338 24.5932 18.3333 24.5715 18.3333C24.1768 18.3333 23.8571 18.6649 23.8571 19.0742V19.3702C23.8571 19.2689 23.8473 19.1699 23.8286 19.0742C23.7475 18.6589 23.4991 18.3055 23.1595 18.0879C22.959 17.9595 22.7267 17.8784 22.4782 17.86C22.4618 17.8588 22.4453 17.8578 22.4287 17.8571C22.034 17.8889 21.7143 18.2204 21.7143 18.6298V18.9258C21.7143 18.8244 21.7044 18.7254 21.6857 18.6298C21.6041 18.2122 21.3532 17.8585 21.0104 17.6489C20.8151 17.5294 20.5899 17.4568 20.3493 17.4459C20.3283 17.4449 20.3071 17.4444 20.2858 17.4444C19.8911 17.4444 19.5714 17.776 19.5714 18.1853V18.4813C19.5714 18.38 19.5616 18.281 19.5429 18.1853C19.4614 17.7685 19.2112 17.4153 18.8694 17.2055C18.6566 17.0749 18.4082 17 18.143 17C17.7483 17 17.4285 17.3316 17.4285 17.7409V21.8889H16.5714V19.2222C16.5233 19.2222 16.4756 19.2243 16.4285 19.2283C16.1102 19.2555 15.8166 19.373 15.5714 19.5559C15.1388 19.8786 14.8571 20.4052 14.8571 21V24.0889C14.8571 24.5666 15.1641 25.173 15.5427 25.4544C15.5427 25.4544 18.2857 27.4098 18.2857 28.1116V29H21.7143L22.5714 27.2857ZM21.7142 22.1428V25.5714H22.5714V22.1428H21.7142ZM20.8571 22.1428H20V25.5714H20.8571V22.1428ZM23.4286 22.1428V25.5714H24.2857V22.1428H23.4286ZM14.9622 26.2686L14.9542 26.2629L14.9462 26.257C14.6003 25.9999 14.3387 25.6406 14.1647 25.2949C13.9905 24.9489 13.8571 24.5232 13.8571 24.0889V21C13.8571 19.5475 14.9657 18.3024 16.4285 18.2259V17.7409C16.4285 16.8139 17.162 16 18.143 16C18.7325 16 19.2629 16.2105 19.6765 16.5563C19.864 16.4843 20.0689 16.4444 20.2858 16.4444C20.8702 16.4444 21.3963 16.6512 21.8083 16.9915C21.9775 16.9208 22.1593 16.8756 22.3485 16.8604L22.4086 16.8555L22.4689 16.858C23.0398 16.8809 23.5592 17.0977 23.9666 17.4435C24.1529 17.3725 24.3563 17.3333 24.5715 17.3333C25.9468 17.3333 27 18.4787 27 19.8147V23.8608C27 24.2194 26.9264 24.6111 26.8292 24.9524C26.7317 25.2951 26.5886 25.6638 26.405 25.967L26.3977 25.979M26.3901 25.9908L26.3879 25.9943L26.3788 26.0086L26.3419 26.0669C26.3096 26.1183 26.2628 26.1934 26.2064 26.2861C26.0931 26.4724 25.9435 26.7255 25.795 26.9969C25.6449 27.2715 25.505 27.5487 25.4053 27.7863C25.3291 27.9679 25.3015 28.0694 25.2914 28.1063C25.2859 28.1263 25.2857 28.1272 25.2857 28.1116V30H22.8105L22.5714 29.5218L22.3323 30H17.2857V28.2755C17.2502 28.2266 17.1959 28.1594 17.1182 28.0741C16.9049 27.8403 16.601 27.563 16.2706 27.2852C15.9454 27.0118 15.6172 26.7567 15.3685 26.5688C15.2446 26.4752 15.1417 26.3992 15.0703 26.347C15.0347 26.3209 15.0069 26.3008 14.9885 26.2875L14.9679 26.2727L14.9622 26.2686M17.3337 28.3513C17.3337 28.3513 17.3305 28.3463 17.3265 28.337C17.3321 28.3467 17.3337 28.3513 17.3337 28.3513Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9 15H5V14H9V15Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M17 8H13V7H17V8Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20 15H11V14H20V15Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M28 8H19V7H28V8Z" fill="#017D73"/> +<defs> +<filter id="filter0_d_201_310" x="11.8571" y="15" width="17.1429" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_201_310"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_201_310" result="shape"/> +</filter> +</defs> +</svg> diff --git a/src/plugins/wizard/public/embeddable/disabled_embeddable.tsx b/src/plugins/wizard/public/embeddable/disabled_embeddable.tsx new file mode 100644 index 000000000000..5f55859156d0 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_embeddable.tsx @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Embeddable, EmbeddableOutput } from '../../../embeddable/public'; + +import { DisabledVisualization } from './disabled_visualization'; +import { WizardInput, WIZARD_EMBEDDABLE } from './wizard_embeddable'; + +export class DisabledEmbeddable extends Embeddable<WizardInput, EmbeddableOutput> { + private domNode?: HTMLElement; + public readonly type = WIZARD_EMBEDDABLE; + + constructor(private readonly title: string, initialInput: WizardInput) { + super(initialInput, { title }); + } + + public reload() {} + public render(domNode: HTMLElement) { + if (this.title) { + this.domNode = domNode; + ReactDOM.render(<DisabledVisualization title={this.title} />, domNode); + } + } + + public destroy() { + if (this.domNode) { + ReactDOM.unmountComponentAtNode(this.domNode); + } + } +} diff --git a/src/plugins/wizard/public/embeddable/disabled_visualization.scss b/src/plugins/wizard/public/embeddable/disabled_visualization.scss new file mode 100644 index 000000000000..792bb1777ad6 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_visualization.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizDisabledVisualization { + width: 100%; + display: grid; + grid-gap: $euiSize; + place-content: center; + place-items: center; + text-align: center; +} diff --git a/src/plugins/wizard/public/embeddable/disabled_visualization.tsx b/src/plugins/wizard/public/embeddable/disabled_visualization.tsx new file mode 100644 index 000000000000..04b8bf234541 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_visualization.tsx @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import React from 'react'; + +import './disabled_visualization.scss'; + +export function DisabledVisualization({ title }: { title: string }) { + return ( + <div className="wizDisabledVisualization"> + <EuiIcon type="beaker" size="xl" /> + <div> + <FormattedMessage + id="wizard.disabledVisualizationTitle" + defaultMessage="{title} is an experimental visualization." + values={{ title: <em className="visDisabledLabVisualization__title">{title}</em> }} + /> + </div> + <div> + <FormattedMessage + id="wizard.disabledVisualizationMessage" + defaultMessage="Please turn on lab-mode in the advanced settings to see these visualizations." + /> + </div> + </div> + ); +} diff --git a/src/plugins/wizard/public/embeddable/index.ts b/src/plugins/wizard/public/embeddable/index.ts new file mode 100644 index 000000000000..d0137757e0ab --- /dev/null +++ b/src/plugins/wizard/public/embeddable/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './wizard_embeddable'; +export * from './wizard_embeddable_factory'; diff --git a/src/plugins/wizard/public/embeddable/wizard_component.tsx b/src/plugins/wizard/public/embeddable/wizard_component.tsx new file mode 100644 index 000000000000..675baf28796a --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_component.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { SavedObjectEmbeddableInput, withEmbeddableSubscription } from '../../../embeddable/public'; +import { WizardEmbeddable, WizardOutput } from './wizard_embeddable'; +import { getReactExpressionRenderer } from '../plugin_services'; + +interface Props { + embeddable: WizardEmbeddable; + input: SavedObjectEmbeddableInput; + output: WizardOutput; +} + +function WizardEmbeddableComponentInner({ embeddable, input: {}, output: { error } }: Props) { + const { expression } = embeddable; + const ReactExpressionRenderer = getReactExpressionRenderer(); + + return ( + <> + {error?.message ? ( + // TODO: add correct loading and error states + <div>{error.message}</div> + ) : ( + <ReactExpressionRenderer expression={expression ?? ''} /> + )} + </> + ); +} + +export const WizardEmbeddableComponent = withEmbeddableSubscription< + SavedObjectEmbeddableInput, + WizardOutput, + WizardEmbeddable +>(WizardEmbeddableComponentInner); diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx new file mode 100644 index 000000000000..1acbd295e43a --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx @@ -0,0 +1,298 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { cloneDeep, isEqual } from 'lodash'; +import ReactDOM from 'react-dom'; +import { merge, Subscription } from 'rxjs'; + +import { PLUGIN_ID, WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from '../../common'; +import { + Embeddable, + EmbeddableOutput, + ErrorEmbeddable, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../embeddable/public'; +import { + ExpressionRenderError, + ExpressionsStart, + IExpressionLoaderParams, +} from '../../../expressions/public'; +import { + Filter, + opensearchFilters, + Query, + TimefilterContract, + TimeRange, +} from '../../../data/public'; +import { validateSchemaState } from '../application/utils/validate_schema_state'; +import { getExpressionLoader, getTypeService } from '../plugin_services'; + +// Apparently this needs to match the saved object type for the clone and replace panel actions to work +export const WIZARD_EMBEDDABLE = WIZARD_SAVED_OBJECT; + +export interface WizardEmbeddableConfiguration { + savedWizard: WizardSavedObjectAttributes; + // TODO: add indexPatterns as part of configuration + // indexPatterns?: IIndexPattern[]; + editPath: string; + editUrl: string; + editable: boolean; +} + +export interface WizardOutput extends EmbeddableOutput { + /** + * Will contain the saved object attributes of the Wizard Saved Object that matches + * `input.savedObjectId`. If the id is invalid, this may be undefined. + */ + savedWizard?: WizardSavedObjectAttributes; +} + +type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>; + +export class WizardEmbeddable extends Embeddable<SavedObjectEmbeddableInput, WizardOutput> { + public readonly type = WIZARD_EMBEDDABLE; + private handler?: ExpressionLoader; + private timeRange?: TimeRange; + private query?: Query; + private filters?: Filter[]; + private abortController?: AbortController; + public expression: string = ''; + private autoRefreshFetchSubscription: Subscription; + private subscriptions: Subscription[] = []; + private node?: HTMLElement; + private savedWizard?: WizardSavedObjectAttributes; + private serializedState?: { visualization: string; style: string }; + + constructor( + timefilter: TimefilterContract, + { savedWizard, editPath, editUrl, editable }: WizardEmbeddableConfiguration, + initialInput: SavedObjectEmbeddableInput, + { + parent, + }: { + parent?: IContainer; + } + ) { + super( + initialInput, + { + defaultTitle: savedWizard.title, + editPath, + editApp: PLUGIN_ID, + editUrl, + editable, + savedWizard, + }, + parent + ); + + this.savedWizard = savedWizard; + + this.autoRefreshFetchSubscription = timefilter + .getAutoRefreshFetch$() + .subscribe(this.updateHandler.bind(this)); + + this.subscriptions.push( + merge(this.getOutput$(), this.getInput$()).subscribe(() => { + this.handleChanges(); + }) + ); + } + + private getSerializedState = () => { + const { visualizationState: visualization = '{}', styleState: style = '{}' } = + this.savedWizard || {}; + return { + visualization, + style, + }; + }; + + private getExpression = async () => { + if (!this.serializedState) { + return; + } + const { visualization, style } = this.serializedState; + + const vizStateWithoutIndex = JSON.parse(visualization); + const visualizationState = { + searchField: vizStateWithoutIndex.searchField, + activeVisualization: vizStateWithoutIndex.activeVisualization, + indexPattern: this.savedWizard?.searchSourceFields?.index, + }; + const rootState = { + visualization: visualizationState, + style: JSON.parse(style), + }; + const visualizationName = rootState.visualization?.activeVisualization?.name ?? ''; + const visualizationType = getTypeService().get(visualizationName); + if (!visualizationType) { + this.onContainerError(new Error(`Invalid visualization type ${visualizationName}`)); + return; + } + const { toExpression, ui } = visualizationType; + const schemas = ui.containerConfig.data.schemas; + const [valid, errorMsg] = validateSchemaState(schemas, rootState); + + if (!valid) { + if (errorMsg) { + this.onContainerError(new Error(errorMsg)); + return; + } + } else { + // TODO: handle error in Expression creation + const exp = await toExpression(rootState); + return exp; + } + }; + + // Needed to enable inspection panel option + public getInspectorAdapters = () => { + if (!this.handler) { + return undefined; + } + return this.handler.inspect(); + }; + + // Needed to add informational tooltip + public getDescription() { + return this.savedWizard?.description; + } + + public render(node: HTMLElement) { + if (this.output.error) { + // TODO: Can we find a more elegant way to throw, propagate, and render errors? + const errorEmbeddable = new ErrorEmbeddable( + this.output.error as Error, + this.input, + this.parent + ); + return errorEmbeddable.render(node); + } + this.timeRange = cloneDeep(this.input.timeRange); + + const div = document.createElement('div'); + div.className = `wizard visualize panel-content panel-content--fullWidth`; + node.appendChild(div); + + this.node = div; + super.render(this.node); + + // TODO: Investigate migrating to using `./wizard_component` for React rendering instead + const ExpressionLoader = getExpressionLoader(); + this.handler = new ExpressionLoader(this.node, undefined, { + onRenderError: (_element: HTMLElement, error: ExpressionRenderError) => { + this.onContainerError(error); + }, + }); + + if (this.savedWizard?.description) { + div.setAttribute('data-description', this.savedWizard.description); + } + + div.setAttribute('data-test-subj', 'wizardLoader'); + + this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading)); + this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender)); + + this.updateHandler(); + } + + public async reload() { + this.updateHandler(); + } + + public destroy() { + super.destroy(); + this.subscriptions.forEach((s) => s.unsubscribe()); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + + if (this.handler) { + this.handler.destroy(); + this.handler.getElement().remove(); + } + this.autoRefreshFetchSubscription.unsubscribe(); + } + + private async updateHandler() { + const expressionParams: IExpressionLoaderParams = { + searchContext: { + timeRange: this.timeRange, + query: this.input.query, + filters: this.input.filters, + }, + }; + if (this.abortController) { + this.abortController.abort(); + } + this.abortController = new AbortController(); + const abortController = this.abortController; + + if (this.handler && !abortController.signal.aborted) { + this.handler.update(this.expression, expressionParams); + } + } + + public async handleChanges() { + // TODO: refactor (here and in visualize) to remove lodash dependency - immer probably a better choice + + let dirty = false; + + // Check if timerange has changed + if (!isEqual(this.input.timeRange, this.timeRange)) { + this.timeRange = cloneDeep(this.input.timeRange); + dirty = true; + } + + // Check if filters has changed + if (!opensearchFilters.onlyDisabledFiltersChanged(this.input.filters, this.filters)) { + this.filters = this.input.filters; + dirty = true; + } + + // Check if query has changed + if (!isEqual(this.input.query, this.query)) { + this.query = this.input.query; + dirty = true; + } + + // Check if rootState has changed + if (!isEqual(this.getSerializedState(), this.serializedState)) { + this.serializedState = this.getSerializedState(); + this.expression = (await this.getExpression()) ?? ''; + dirty = true; + } + + if (this.handler && dirty) { + this.updateHandler(); + } + } + + onContainerLoading = () => { + this.renderComplete.dispatchInProgress(); + this.updateOutput({ loading: true, error: undefined }); + }; + + onContainerRender = () => { + this.renderComplete.dispatchComplete(); + this.updateOutput({ loading: false, error: undefined }); + }; + + onContainerError = (error: ExpressionRenderError) => { + if (this.abortController) { + this.abortController.abort(); + } + this.renderComplete.dispatchError(); + this.updateOutput({ loading: false, error }); + }; + + // TODO: we may eventually need to add support for visualizations that use triggers like filter or brush, but current wizard vis types don't support triggers + // public supportedTriggers(): TriggerId[] { + // return this.visType.getSupportedTriggers?.() ?? []; + // } +} diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx new file mode 100644 index 000000000000..f2d92d001303 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + EmbeddableFactory, + EmbeddableFactoryDefinition, + EmbeddableOutput, + ErrorEmbeddable, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../embeddable/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public'; +import { + EDIT_PATH, + PLUGIN_ID, + PLUGIN_NAME, + WizardSavedObjectAttributes, + WIZARD_SAVED_OBJECT, +} from '../../common'; +import { DisabledEmbeddable } from './disabled_embeddable'; +import { WizardEmbeddable, WizardOutput, WIZARD_EMBEDDABLE } from './wizard_embeddable'; +import wizardIcon from '../assets/wizard_icon.svg'; +import { getHttp, getSavedWizardLoader, getTimeFilter, getUISettings } from '../plugin_services'; + +// TODO: use or remove? +export type WizardEmbeddableFactory = EmbeddableFactory< + SavedObjectEmbeddableInput, + WizardOutput | EmbeddableOutput, + WizardEmbeddable | DisabledEmbeddable, + WizardSavedObjectAttributes +>; + +export class WizardEmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition< + SavedObjectEmbeddableInput, + WizardOutput | EmbeddableOutput, + WizardEmbeddable | DisabledEmbeddable, + WizardSavedObjectAttributes + > { + public readonly type = WIZARD_EMBEDDABLE; + public readonly savedObjectMetaData = { + // TODO: Update to include most vis functionality + name: PLUGIN_NAME, + includeFields: ['visualizationState'], + type: WIZARD_SAVED_OBJECT, + getIconForSavedObject: () => wizardIcon, + }; + + // TODO: Would it be better to explicitly declare start service dependencies? + constructor() {} + + public canCreateNew() { + // Because wizard creation starts with the visualization modal, no need to have a separate entry for wizard until it's separate + return false; + } + + public async isEditable() { + // TODO: Add proper access controls + // return getCapabilities().visualize.save as boolean; + return true; + } + + public async createFromSavedObject( + savedObjectId: string, + input: Partial<SavedObjectEmbeddableInput> & { id: string }, + parent?: IContainer + ): Promise<WizardEmbeddable | ErrorEmbeddable | DisabledEmbeddable> { + try { + const savedWizard = await getSavedWizardLoader().get(savedObjectId); + + const editPath = `${EDIT_PATH}/${savedObjectId}`; + + const editUrl = getHttp().basePath.prepend(`/app/${PLUGIN_ID}${editPath}`); + + const isLabsEnabled = getUISettings().get<boolean>(VISUALIZE_ENABLE_LABS_SETTING); + + if (!isLabsEnabled) { + return new DisabledEmbeddable(PLUGIN_NAME, input); + } + + return new WizardEmbeddable( + getTimeFilter(), + { + savedWizard, + editUrl, + editPath, + editable: true, + }, + { + ...input, + savedObjectId: input.savedObjectId ?? '', + }, + { + parent, + } + ); + } catch (e) { + console.error(e); // eslint-disable-line no-console + return new ErrorEmbeddable(e as Error, input, parent); + } + } + + public async create(_input: SavedObjectEmbeddableInput, _parent?: IContainer) { + return undefined; + } + + public getDisplayName() { + return i18n.translate('wizard.displayName', { + defaultMessage: PLUGIN_ID, + }); + } +} diff --git a/src/plugins/wizard/public/index.ts b/src/plugins/wizard/public/index.ts new file mode 100644 index 000000000000..713e9448b933 --- /dev/null +++ b/src/plugins/wizard/public/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginInitializerContext } from '../../../core/public'; +import { WizardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +export function plugin(initializerContext: PluginInitializerContext) { + return new WizardPlugin(initializerContext); +} +export { WizardServices, WizardPluginStartDependencies, WizardStart } from './types'; diff --git a/src/plugins/wizard/public/plugin.test.ts b/src/plugins/wizard/public/plugin.test.ts new file mode 100644 index 000000000000..6dafa46c86ff --- /dev/null +++ b/src/plugins/wizard/public/plugin.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock, savedObjectsServiceMock } from '../../../core/public/mocks'; +import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; +import { dataPluginMock } from '../../../plugins/data/public/mocks'; +import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks'; +import { navigationPluginMock } from '../../../plugins/navigation/public/mocks'; +import { visualizationsPluginMock } from '../../../plugins/visualizations/public/mocks'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common'; +import { WizardPlugin } from './plugin'; + +describe('WizardPlugin', () => { + describe('setup', () => { + it('initializes the plugin correctly and registers it as an alias visualization', () => { + const plugin = new WizardPlugin(coreMock.createPluginInitializerContext()); + const pluginStartContract = { + data: dataPluginMock.createStartContract(), + savedObject: savedObjectsServiceMock.createStartContract(), + navigation: navigationPluginMock.createStartContract(), + dashboard: dashboardPluginMock.createStartContract(), + }; + const coreSetup = coreMock.createSetup({ + pluginStartContract, + }) as any; + const setupDeps = { + visualizations: visualizationsPluginMock.createSetupContract(), + embeddable: embeddablePluginMock.createSetupContract(), + }; + + const setup = plugin.setup(coreSetup, setupDeps); + expect(setup).toHaveProperty('createVisualizationType'); + expect(setupDeps.visualizations.registerAlias).toHaveBeenCalledWith( + expect.objectContaining({ + name: PLUGIN_ID, + title: PLUGIN_NAME, + aliasPath: '#/', + aliasApp: PLUGIN_ID, + stage: 'experimental', + }) + ); + }); + }); +}); diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts new file mode 100644 index 000000000000..9c56562f4ba1 --- /dev/null +++ b/src/plugins/wizard/public/plugin.ts @@ -0,0 +1,170 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + AppMountParameters, + AppNavLinkStatus, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../core/public'; +import { + WizardPluginSetupDependencies, + WizardPluginStartDependencies, + WizardServices, + WizardSetup, + WizardStart, +} from './types'; +import { WizardEmbeddableFactoryDefinition, WIZARD_EMBEDDABLE } from './embeddable'; +import wizardIconSecondaryFill from './assets/wizard_icon_secondary_fill.svg'; +import wizardIcon from './assets/wizard_icon.svg'; +import { EDIT_PATH, PLUGIN_ID, PLUGIN_NAME, WIZARD_SAVED_OBJECT } from '../common'; +import { TypeService } from './services/type_service'; +import { getPreloadedStore } from './application/utils/state_management'; +import { + setAggService, + setIndexPatterns, + setHttp, + setSavedWizardLoader, + setExpressionLoader, + setTimeFilter, + setUISettings, + setTypeService, + setReactExpressionRenderer, +} from './plugin_services'; +import { createSavedWizardLoader } from './saved_visualizations'; +import { registerDefaultTypes } from './visualizations'; +import { ConfigSchema } from '../config'; + +export class WizardPlugin + implements + Plugin<WizardSetup, WizardStart, WizardPluginSetupDependencies, WizardPluginStartDependencies> { + private typeService = new TypeService(); + + constructor(public initializerContext: PluginInitializerContext<ConfigSchema>) {} + + public setup( + core: CoreSetup<WizardPluginStartDependencies, WizardStart>, + { embeddable, visualizations }: WizardPluginSetupDependencies + ) { + const typeService = this.typeService; + registerDefaultTypes(typeService.setup()); + + // Register the plugin to core + core.application.register({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + + // Get start services as specified in opensearch_dashboards.json + const [coreStart, pluginsStart, selfStart] = await core.getStartServices(); + const { data, savedObjects, navigation, expressions } = pluginsStart; + + // make sure the index pattern list is up to date + data.indexPatterns.clearCache(); + // make sure a default index pattern exists + // if not, the page will be redirected to management and visualize won't be rendered + // TODO: Add the redirect + await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); + + // Register Default Visualizations + + const services: WizardServices = { + ...coreStart, + toastNotifications: coreStart.notifications.toasts, + data, + savedObjectsPublic: savedObjects, + navigation, + expressions, + history: params.history, + setHeaderActionMenu: params.setHeaderActionMenu, + types: typeService.start(), + savedWizardLoader: selfStart.savedWizardLoader, + }; + + // Instantiate the store + const store = await getPreloadedStore(services); + + // Render the application + return renderApp(params, services, store); + }, + }); + + // Register embeddable + // TODO: investigate simplification via getter a la visualizations: + // const start = createStartServicesGetter(core.getStartServices)); + // const embeddableFactory = new WizardEmbeddableFactoryDefinition({ start }); + const embeddableFactory = new WizardEmbeddableFactoryDefinition(); + embeddable.registerEmbeddableFactory(WIZARD_EMBEDDABLE, embeddableFactory); + + // Register the plugin as an alias to create visualization + visualizations.registerAlias({ + name: PLUGIN_ID, + title: PLUGIN_NAME, + description: i18n.translate('wizard.visPicker.description', { + defaultMessage: 'Create visualizations using the new Drag & Drop experience', + }), + icon: wizardIconSecondaryFill, + stage: 'experimental', + aliasApp: PLUGIN_ID, + aliasPath: '#/', + appExtensions: { + visualizations: { + docTypes: [PLUGIN_ID], + toListItem: ({ id, attributes }) => ({ + description: attributes?.description, + editApp: PLUGIN_ID, + editUrl: `${EDIT_PATH}/${encodeURIComponent(id)}`, + icon: wizardIcon, + id, + savedObjectType: WIZARD_SAVED_OBJECT, + stage: 'experimental', + title: attributes?.title, + typeTitle: PLUGIN_NAME, + }), + }, + }, + }); + + return { + ...typeService.setup(), + }; + } + + public start(core: CoreStart, { data, expressions }: WizardPluginStartDependencies): WizardStart { + const typeService = this.typeService.start(); + + const savedWizardLoader = createSavedWizardLoader({ + savedObjectsClient: core.savedObjects.client, + indexPatterns: data.indexPatterns, + search: data.search, + chrome: core.chrome, + overlays: core.overlays, + }); + + // Register plugin services + setAggService(data.search.aggs); + setExpressionLoader(expressions.ExpressionLoader); + setReactExpressionRenderer(expressions.ReactExpressionRenderer); + setHttp(core.http); + setIndexPatterns(data.indexPatterns); + setSavedWizardLoader(savedWizardLoader); + setTimeFilter(data.query.timefilter.timefilter); + setTypeService(typeService); + setUISettings(core.uiSettings); + + return { + ...typeService, + savedWizardLoader, + }; + } + + public stop() {} +} diff --git a/src/plugins/wizard/public/plugin_services.ts b/src/plugins/wizard/public/plugin_services.ts new file mode 100644 index 000000000000..8f01ea6e9b6b --- /dev/null +++ b/src/plugins/wizard/public/plugin_services.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createGetterSetter } from '../../opensearch_dashboards_utils/common'; +import { DataPublicPluginStart, TimefilterContract } from '../../data/public'; +import { SavedWizardLoader } from './saved_visualizations'; +import { HttpStart, IUiSettingsClient } from '../../../core/public'; +import { ExpressionsStart } from '../../expressions/public'; +import { TypeServiceStart } from './services/type_service'; + +export const [getAggService, setAggService] = createGetterSetter< + DataPublicPluginStart['search']['aggs'] +>('data.search.aggs'); + +export const [getExpressionLoader, setExpressionLoader] = createGetterSetter< + ExpressionsStart['ExpressionLoader'] +>('expressions.ExpressionLoader'); + +export const [getReactExpressionRenderer, setReactExpressionRenderer] = createGetterSetter< + ExpressionsStart['ReactExpressionRenderer'] +>('expressions.ReactExpressionRenderer'); + +export const [getHttp, setHttp] = createGetterSetter<HttpStart>('Http'); + +export const [getIndexPatterns, setIndexPatterns] = createGetterSetter< + DataPublicPluginStart['indexPatterns'] +>('data.indexPatterns'); + +export const [getSavedWizardLoader, setSavedWizardLoader] = createGetterSetter<SavedWizardLoader>( + 'SavedWizardLoader' +); + +export const [getTimeFilter, setTimeFilter] = createGetterSetter<TimefilterContract>('TimeFilter'); + +export const [getTypeService, setTypeService] = createGetterSetter<TypeServiceStart>('TypeService'); + +export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings'); diff --git a/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts new file mode 100644 index 000000000000..d269642bd8bc --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + createSavedObjectClass, + SavedObjectOpenSearchDashboardsServices, +} from '../../../saved_objects/public'; +import { EDIT_PATH, PLUGIN_ID, WIZARD_SAVED_OBJECT } from '../../common'; +import { injectReferences } from './saved_visualization_references'; + +export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboardsServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedWizardVis extends SavedObjectClass { + public static type = WIZARD_SAVED_OBJECT; + + // if type:wizard has no mapping, we push this mapping into OpenSearch + public static mapping = { + title: 'text', + description: 'text', + visualizationState: 'text', + styleState: 'text', + version: 'integer', + }; + + // Order these fields to the top, the rest are alphabetical + static fieldOrder = ['title', 'description']; + + // ID is optional, without it one will be generated on save. + constructor(id: string) { + super({ + type: SavedWizardVis.type, + mapping: SavedWizardVis.mapping, + injectReferences, + + // if this is null/undefined then the SavedObject will be assigned the defaults + id, + + // default values that will get assigned if the doc is new + defaults: { + title: '', + description: '', + visualizationState: '{}', + styleState: '{}', + version: 2, + }, + }); + this.showInRecentlyAccessed = true; + this.getFullPath = () => `/app/${PLUGIN_ID}${EDIT_PATH}/${this.id}`; + } + } + + return SavedWizardVis; +} diff --git a/src/plugins/wizard/public/saved_visualizations/index.ts b/src/plugins/wizard/public/saved_visualizations/index.ts new file mode 100644 index 000000000000..442d5107ea05 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './saved_visualizations'; diff --git a/src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts b/src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts new file mode 100644 index 000000000000..81a2a54a01c6 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectReference } from '../../../../core/public'; +import { WizardVisSavedObject } from '../types'; +import { injectSearchSourceReferences } from '../../../data/public'; + +export function injectReferences( + savedObject: WizardVisSavedObject, + references: SavedObjectReference[] +) { + if (savedObject.searchSourceFields) { + savedObject.searchSourceFields = injectSearchSourceReferences( + savedObject.searchSourceFields as any, + references + ); + } +} diff --git a/src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts b/src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts new file mode 100644 index 000000000000..f07dfd940312 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SavedObjectLoader, + SavedObjectOpenSearchDashboardsServices, +} from '../../../saved_objects/public'; +import { createSavedWizardVisClass } from './_saved_vis'; + +export type SavedWizardLoader = ReturnType<typeof createSavedWizardLoader>; +export function createSavedWizardLoader(services: SavedObjectOpenSearchDashboardsServices) { + const { savedObjectsClient } = services; + const SavedWizardVisClass = createSavedWizardVisClass(services); + + return new SavedObjectLoader(SavedWizardVisClass, savedObjectsClient); +} diff --git a/src/plugins/wizard/public/services/type_service/index.ts b/src/plugins/wizard/public/services/type_service/index.ts new file mode 100644 index 000000000000..9baeb6dbc9f4 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './type_service'; +export * from './types'; diff --git a/src/plugins/wizard/public/services/type_service/type_service.test.ts b/src/plugins/wizard/public/services/type_service/type_service.test.ts new file mode 100644 index 000000000000..89e1ecb59154 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/type_service.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationTypeOptions } from './types'; +import { TypeService } from './type_service'; + +const DEFAULT_VIZ_PROPS: VisualizationTypeOptions = { + name: 'some-name', + icon: 'some-icon', + title: 'Some Title', + ui: {} as any, // Not required for this test + toExpression: async (state) => { + return 'test'; + }, +}; + +describe('TypeService', () => { + const createVizType = (props?: Partial<VisualizationTypeOptions>): VisualizationTypeOptions => { + return { + ...DEFAULT_VIZ_PROPS, + ...props, + }; + }; + + let service: TypeService; + + beforeEach(() => { + service = new TypeService(); + }); + + describe('#setup', () => { + test('should throw an error if two visualizations of the same id are registered', () => { + const { createVisualizationType } = service.setup(); + + createVisualizationType(createVizType({ name: 'viz-type-1' })); + + expect(() => { + createVisualizationType(createVizType({ name: 'viz-type-1' })); + }).toThrowErrorMatchingInlineSnapshot( + `"A visualization with this the name viz-type-1 already exists!"` + ); + }); + }); + + describe('#start', () => { + test('should return registered visualization if it exists', () => { + const { createVisualizationType } = service.setup(); + createVisualizationType(createVizType({ name: 'viz-type-1' })); + + const { get } = service.start(); + expect(get('viz-type-1')).toEqual(expect.objectContaining({ name: 'viz-type-1' })); + expect(get('viz-type-no-exists')).toBeUndefined(); + }); + + test('should return all registered visualizations', () => { + const { createVisualizationType } = service.setup(); + createVisualizationType(createVizType({ name: 'viz-type-1' })); + createVisualizationType(createVizType({ name: 'viz-type-2' })); + + const { all } = service.start(); + const allRegisteredVisualizations = all(); + expect(allRegisteredVisualizations.map(({ name }) => name)).toEqual([ + 'viz-type-1', + 'viz-type-2', + ]); + }); + }); +}); diff --git a/src/plugins/wizard/public/services/type_service/type_service.ts b/src/plugins/wizard/public/services/type_service/type_service.ts new file mode 100644 index 000000000000..ddbd735fb9e8 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/type_service.ts @@ -0,0 +1,88 @@ +/* + * 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 { CoreService } from '../../../../../core/types'; +import { VisualizationTypeOptions } from './types'; +import { VisualizationType } from './visualization_type'; + +/** + * Visualization Types Service + * + * @internal + */ +export class TypeService implements CoreService<TypeServiceSetup, TypeServiceStart> { + private types: Record<string, VisualizationType> = {}; + + private registerVisualizationType(visDefinition: VisualizationType) { + if (this.types[visDefinition.name]) { + throw new Error(`A visualization with this the name ${visDefinition.name} already exists!`); + } + this.types[visDefinition.name] = visDefinition; + } + + public setup() { + return { + /** + * registers a visualization type + * @param config - visualization type definition + */ + createVisualizationType: (config: VisualizationTypeOptions): void => { + const vis = new VisualizationType(config); + this.registerVisualizationType(vis); + }, + }; + } + + public start() { + return { + /** + * returns specific visualization or undefined if not found + * @param {string} visualization - id of visualization to return + */ + get: (visualization: string): VisualizationType | undefined => { + return this.types[visualization]; + }, + /** + * returns all registered visualization types + */ + all: (): VisualizationType[] => { + return [...Object.values(this.types)]; + }, + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @internal */ +export type TypeServiceSetup = ReturnType<TypeService['setup']>; +export type TypeServiceStart = ReturnType<TypeService['start']>; diff --git a/src/plugins/wizard/public/services/type_service/types.ts b/src/plugins/wizard/public/services/type_service/types.ts new file mode 100644 index 000000000000..8542c0da0538 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { ReactElement } from 'react'; +import { IconType } from '@elastic/eui'; +import { RootState } from '../../application/utils/state_management'; +import { Schemas } from '../../../../vis_default_editor/public'; + +export interface DataTabConfig { + schemas: Schemas; +} + +export interface StyleTabConfig<T = any> { + defaults: T; + render: () => ReactElement; +} + +export interface VisualizationTypeOptions<T = any> { + readonly name: string; + readonly title: string; + readonly description?: string; + readonly icon: IconType; + readonly stage?: 'experimental' | 'production'; + readonly ui: { + containerConfig: { + data: DataTabConfig; + style: StyleTabConfig<T>; + }; + }; + readonly toExpression: (state: RootState) => Promise<string | undefined>; +} diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx new file mode 100644 index 000000000000..305b7a716cf6 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { IconType } from '@elastic/eui'; +import { RootState } from '../../application/utils/state_management'; +import { VisualizationTypeOptions } from './types'; + +type IVisualizationType = VisualizationTypeOptions; + +export class VisualizationType implements IVisualizationType { + public readonly name: string; + public readonly title: string; + public readonly description: string; + public readonly icon: IconType; + public readonly stage: 'experimental' | 'production'; + public readonly ui: IVisualizationType['ui']; + public readonly toExpression: (state: RootState) => Promise<string | undefined>; + + constructor(options: VisualizationTypeOptions) { + this.name = options.name; + this.title = options.title; + this.description = options.description ?? ''; + this.icon = options.icon; + this.stage = options.stage ?? 'production'; + this.ui = options.ui; + this.toExpression = options.toExpression; + } +} diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts new file mode 100644 index 000000000000..f8371b832bdc --- /dev/null +++ b/src/plugins/wizard/public/types.ts @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { History } from 'history'; +import { SavedObject, SavedObjectsStart } from '../../saved_objects/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import { DashboardStart } from '../../dashboard/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { ExpressionsStart } from '../../expressions/public'; +import { NavigationPublicPluginStart } from '../../navigation/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; +import { SavedObjectLoader } from '../../saved_objects/public'; +import { AppMountParameters, CoreStart, ToastsStart } from '../../../core/public'; + +export type WizardSetup = TypeServiceSetup; +export interface WizardStart extends TypeServiceStart { + savedWizardLoader: SavedObjectLoader; +} + +export interface WizardPluginSetupDependencies { + embeddable: EmbeddableSetup; + visualizations: VisualizationsSetup; +} +export interface WizardPluginStartDependencies { + embeddable: EmbeddableStart; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; + savedObjects: SavedObjectsStart; + dashboard: DashboardStart; + expressions: ExpressionsStart; +} + +export interface WizardServices extends CoreStart { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + savedWizardLoader: WizardStart['savedWizardLoader']; + toastNotifications: ToastsStart; + savedObjectsPublic: SavedObjectsStart; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; + types: TypeServiceStart; + expressions: ExpressionsStart; + history: History; +} + +export interface ISavedVis { + id?: string; + title: string; + description?: string; + visualizationState?: string; + styleState?: string; + version?: number; +} + +export interface WizardVisSavedObject extends SavedObject, ISavedVis {} diff --git a/src/plugins/wizard/public/visualizations/common/expression_helpers.ts b/src/plugins/wizard/public/visualizations/common/expression_helpers.ts new file mode 100644 index 000000000000..561f724f2568 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/common/expression_helpers.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { cloneDeep } from 'lodash'; +import { OpenSearchaggsExpressionFunctionDefinition } from '../../../../data/public'; +import { ExpressionFunctionOpenSearchDashboards } from '../../../../expressions'; +import { buildExpressionFunction } from '../../../../expressions/public'; +import { VisualizationState } from '../../application/utils/state_management'; +import { getAggService, getIndexPatterns } from '../../plugin_services'; + +export const getAggExpressionFunctions = async (visualization: VisualizationState) => { + const { activeVisualization, indexPattern: indexId = '' } = visualization; + const { aggConfigParams } = activeVisualization || {}; + + const indexPatternsService = getIndexPatterns(); + const indexPattern = await indexPatternsService.get(indexId); + // aggConfigParams is the serealizeable aggConfigs that need to be reconstructed here using the agg servce + const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + + const opensearchDashboards = buildExpressionFunction<ExpressionFunctionOpenSearchDashboards>( + 'opensearchDashboards', + {} + ); + + // soon this becomes: const opensearchaggs = vis.data.aggs!.toExpressionAst(); + const opensearchaggs = buildExpressionFunction<OpenSearchaggsExpressionFunctionDefinition>( + 'opensearchaggs', + { + index: indexId, + metricsAtAllLevels: false, + partialRows: false, + aggConfigs: JSON.stringify(aggConfigs.aggs), + includeFormatHints: false, + } + ); + + return { + aggConfigs, + expressionFns: [opensearchDashboards, opensearchaggs], + }; +}; diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts new file mode 100644 index 000000000000..6787c28a6ff8 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { TypeServiceSetup } from '../services/type_service'; +import { createMetricConfig } from './metric'; +import { createHistogramConfig, createLineConfig, createAreaConfig } from './vislib'; + +export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { + const visualizationTypes = [ + createHistogramConfig, + createLineConfig, + createAreaConfig, + createMetricConfig, + ]; + + visualizationTypes.forEach((createTypeConfig) => { + typeServiceSetup.createVisualizationType(createTypeConfig()); + }); +} diff --git a/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx new file mode 100644 index 000000000000..4a626cb01179 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx @@ -0,0 +1,170 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; +import produce from 'immer'; +import { Draft } from 'immer'; +import { + ColorModes, + ColorRanges, + ColorSchemaOptions, + colorSchemas, + RangeOption, + SwitchOption, +} from '../../../../../charts/public'; +import { + useTypedDispatch, + useTypedSelector, + setStyleState, +} from '../../../application/utils/state_management'; +import { MetricOptionsDefaults } from '../metric_viz_type'; +import { PersistedState } from '../../../../../visualizations/public'; +import { Option } from '../../../application/app'; + +const METRIC_COLOR_MODES = [ + { + id: ColorModes.NONE, + label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { + defaultMessage: 'None', + }), + }, + { + id: ColorModes.LABELS, + label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { + defaultMessage: 'Labels', + }), + }, + { + id: ColorModes.BACKGROUND, + label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { + defaultMessage: 'Background', + }), + }, +]; + +function MetricVizOptions() { + const styleState = useTypedSelector((state) => state.style) as MetricOptionsDefaults; + const dispatch = useTypedDispatch(); + const { metric } = styleState; + + const setOption = useCallback( + (callback: (draft: Draft<typeof styleState>) => void) => { + const newState = produce(styleState, callback); + dispatch(setStyleState<MetricOptionsDefaults>(newState)); + }, + [dispatch, styleState] + ); + + const metricColorModeLabel = i18n.translate('visTypeMetric.params.color.useForLabel', { + defaultMessage: 'Use color for', + }); + + return ( + <> + <Option + title={i18n.translate('visTypeMetric.params.settingsTitle', { defaultMessage: 'Settings' })} + initialIsOpen + > + <SwitchOption + label={i18n.translate('visTypeMetric.params.percentageModeLabel', { + defaultMessage: 'Percentage mode', + })} + paramName="percentageMode" + value={metric.percentageMode} + setValue={(_, value) => + setOption((draft) => { + draft.metric.percentageMode = value; + }) + } + /> + + <SwitchOption + label={i18n.translate('visTypeMetric.params.showTitleLabel', { + defaultMessage: 'Show title', + })} + paramName="show" + value={metric.labels.show} + setValue={(_, value) => + setOption((draft) => { + draft.metric.labels.show = value; + }) + } + /> + </Option> + <Option + title={i18n.translate('visTypeMetric.params.rangesTitle', { defaultMessage: 'Ranges' })} + > + <ColorRanges + data-test-subj="metricColorRange" + colorsRange={metric.colorsRange} + setValue={(_, value) => + setOption((draft) => { + draft.metric.colorsRange = value; + }) + } + setTouched={() => {}} + setValidity={() => {}} + /> + + <EuiFormRow fullWidth display="rowCompressed" label={metricColorModeLabel}> + <EuiButtonGroup + buttonSize="compressed" + idSelected={metric.metricColorMode} + isDisabled={metric.colorsRange.length === 1} + isFullWidth={true} + legend={metricColorModeLabel} + options={METRIC_COLOR_MODES} + onChange={(value) => + setOption((draft) => { + draft.metric.metricColorMode = value as ColorModes; + }) + } + /> + </EuiFormRow> + + <ColorSchemaOptions + colorSchema={metric.colorSchema} + colorSchemas={colorSchemas} + disabled={metric.colorsRange.length === 1 || metric.metricColorMode === ColorModes.NONE} + invertColors={metric.invertColors} + setValue={(paramName, value) => + setOption((draft) => { + // The paramName and associated value are expected to pair correctly but will be messy to type correctly + draft.metric[paramName] = value as any; + }) + } + showHelpText={false} + // uistate here is used for custom colors which is not currently supported. Update when supported + uiState={new PersistedState({})} + /> + </Option> + <Option + title={i18n.translate('visTypeMetric.params.style.styleTitle', { defaultMessage: 'Style' })} + > + <RangeOption + label={i18n.translate('visTypeMetric.params.style.fontSizeLabel', { + defaultMessage: 'Metric font size in points', + })} + min={12} + max={120} + paramName="fontSize" + value={metric.style.fontSize} + setValue={(_, value) => + setOption((draft) => { + draft.metric.style.fontSize = value; + }) + } + showInput={true} + showLabels={true} + showValue={false} + /> + </Option> + </> + ); +} + +export { MetricVizOptions }; diff --git a/src/plugins/wizard/public/visualizations/metric/index.ts b/src/plugins/wizard/public/visualizations/metric/index.ts new file mode 100644 index 000000000000..8efccb2639d7 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createMetricConfig } from './metric_viz_type'; diff --git a/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts new file mode 100644 index 000000000000..ce85db45c51b --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { RangeValues, Schemas } from '../../../../vis_default_editor/public'; +import { AggGroupNames } from '../../../../data/public'; +import { ColorModes, ColorSchemas } from '../../../../charts/public'; +import { MetricVizOptions } from './components/metric_viz_options'; +import { VisualizationTypeOptions } from '../../services/type_service'; +import { toExpression } from './to_expression'; + +export interface MetricOptionsDefaults { + addTooltip: boolean; + addLegend: boolean; + type: 'metric'; + metric: { + percentageMode: boolean; + useRanges: boolean; + colorSchema: ColorSchemas; + metricColorMode: ColorModes; + colorsRange: RangeValues[]; + labels: { + show: boolean; + }; + invertColors: boolean; + style: { + bgFill: string; + bgColor: boolean; + labelColor: boolean; + subText: string; + fontSize: number; + }; + }; +} + +export const createMetricConfig = (): VisualizationTypeOptions<MetricOptionsDefaults> => ({ + name: 'metric', + title: 'Metric', + icon: 'visMetric', + description: 'Display metric visualizations', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeMetric.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + min: 1, + aggFilter: [ + '!std_dev', + '!geo_centroid', + '!derivative', + '!serial_diff', + '!moving_avg', + '!cumulative_sum', + '!geo_bounds', + ], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + defaults: { + aggTypes: ['avg', 'cardinality'], + }, + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('visTypeMetric.schemas.splitGroupTitle', { + defaultMessage: 'Split group', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + defaults: { + aggTypes: ['terms'], + }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: ColorSchemas.GreenToRed, + metricColorMode: ColorModes.NONE, + colorsRange: [{ from: 0, to: 10000 }], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, + }, + }, + }, + render: MetricVizOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts new file mode 100644 index 000000000000..181e504ec0e8 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -0,0 +1,151 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SchemaConfig } from '../../../../visualizations/public'; +import { MetricVisExpressionFunctionDefinition } from '../../../../vis_type_metric/public'; +import { AggConfigs, IAggConfig } from '../../../../data/common'; +import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; +import { RootState } from '../../application/utils/state_management'; +import { MetricOptionsDefaults } from './metric_viz_type'; +import { getAggExpressionFunctions } from '../common/expression_helpers'; + +const prepareDimension = (params: SchemaConfig) => { + const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); + + if (params.format) { + visdimension.addArgument('format', params.format.id); + visdimension.addArgument('formatParams', JSON.stringify(params.format.params)); + } + + return buildExpression([visdimension]); +}; + +// TODO: Update to the common getShemas from src/plugins/visualizations/public/legacy/build_pipeline.ts +// And move to a common location accessible by all the visualizations +const getVisSchemas = (aggConfigs: AggConfigs): any => { + const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { + const hasSubAgg = [ + 'derivative', + 'moving_avg', + 'serial_diff', + 'cumulative_sum', + 'sum_bucket', + 'avg_bucket', + 'min_bucket', + 'max_bucket', + ].includes(agg.type.name); + + const formatAgg = hasSubAgg + ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) + : agg; + + const params = {}; + + const label = agg.makeLabel && agg.makeLabel(); + + return { + accessor, + format: formatAgg.toSerializedFieldFormat(), + params, + label, + aggType: agg.type.name, + }; + }; + + let cnt = 0; + const schemas: any = { + metric: [], + }; + + if (!aggConfigs) { + return schemas; + } + + const responseAggs = aggConfigs.getResponseAggs(); + responseAggs.forEach((agg) => { + const schemaName = agg.schema; + + if (!schemaName) { + cnt++; + return; + } + + if (!schemas[schemaName]) { + schemas[schemaName] = []; + } + + schemas[schemaName]!.push(createSchemaConfig(cnt++, agg)); + }); + + return schemas; +}; + +export interface MetricRootState extends RootState { + style: MetricOptionsDefaults; +} + +export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + + // TODO: Update to use the getVisSchemas function from the Visualizations plugin + // const schemas = getVisSchemas(vis, params); + + const { + percentageMode, + useRanges, + colorSchema, + metricColorMode, + colorsRange, + labels, + invertColors, + style, + } = styleState.metric; + + const schemas = getVisSchemas(aggConfigs); + + // fix formatter for percentage mode + if (percentageMode === true) { + schemas.metric.forEach((metric: SchemaConfig) => { + metric.format = { id: 'percent' }; + }); + } + + // TODO: ExpressionFunctionDefinitions mark all arguments as required even though the function marks most as optional + // Update buildExpressionFunction to correctly handle optional arguments + // @ts-expect-error + const metricVis = buildExpressionFunction<MetricVisExpressionFunctionDefinition>('metricVis', { + percentageMode, + colorSchema, + colorMode: metricColorMode, + useRanges, + invertColors, + showLabels: labels && labels.show, + }); + + if (style) { + metricVis.addArgument('bgFill', style.bgFill); + metricVis.addArgument('font', buildExpression(`font size=${style.fontSize}`)); + metricVis.addArgument('subText', style.subText); + } + + if (colorsRange) { + colorsRange.forEach((range: any) => { + metricVis.addArgument( + 'colorRange', + buildExpression(`range from=${range.from} to=${range.to}`) + ); + }); + } + + if (schemas.group) { + metricVis.addArgument('bucket', prepareDimension(schemas.group[0])); + } + + schemas.metric.forEach((metric: SchemaConfig) => { + metricVis.addArgument('metric', prepareDimension(metric)); + }); + + return buildExpression([...expressionFns, metricVis]).toString(); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts b/src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts new file mode 100644 index 000000000000..14b524bf008f --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/area_vis_type.ts @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { AreaVisOptions } from './components/area_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; +import { BasicOptionsDefaults } from '../common/types'; + +export interface AreaOptionsDefaults extends BasicOptionsDefaults { + type: 'area'; +} + +export const createAreaConfig = (): VisualizationTypeOptions<AreaOptionsDefaults> => ({ + name: 'area', + title: 'Area', + icon: 'visArea', + description: 'Display area chart', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.area.metricTitle', { + defaultMessage: 'Y-axis', + }), + min: 1, + max: 3, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: { aggTypes: ['median'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('visTypeVislib.area.segmentTitle', { + defaultMessage: 'X-axis', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!filters'], + defaults: { aggTypes: ['terms'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + type: 'area', + }, + render: AreaVisOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx new file mode 100644 index 000000000000..4b3116c83992 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/components/area_vis_options.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { AreaOptionsDefaults } from '../area_vis_type'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; +import { BasicVisOptions } from '../../common/basic_vis_options'; + +function AreaVisOptions() { + const styleState = useTypedSelector((state) => state.style) as AreaOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft<typeof styleState>) => void) => { + const newState = produce(styleState, callback); + dispatch(setState<AreaOptionsDefaults>(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + <Option + title={i18n.translate('visTypeVislib.area.params.settingsTitle', { + defaultMessage: 'Settings', + })} + initialIsOpen + > + <BasicVisOptions styleState={styleState} setOption={setOption} /> + </Option> + </> + ); +} + +export { AreaVisOptions }; diff --git a/src/plugins/wizard/public/visualizations/vislib/area/index.ts b/src/plugins/wizard/public/visualizations/vislib/area/index.ts new file mode 100644 index 000000000000..7ec1f37a601d --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createAreaConfig } from './area_vis_type'; diff --git a/src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts b/src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts new file mode 100644 index 000000000000..9d40d64c1cfc --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/area/to_expression.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis, buildVislibDimensions } from '../../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../../../expressions/public'; +import { AreaOptionsDefaults } from './area_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState, getValueAxes, getPipelineParams } from '../common'; + +export const toExpression = async ({ + style: styleState, + visualization, +}: VislibRootState<AreaOptionsDefaults>) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + const { addLegend, addTooltip, legendPosition, type } = styleState; + const params = getPipelineParams(); + + const vis = new Vis(type); + vis.data.aggs = aggConfigs; + + const dimensions = await buildVislibDimensions(vis, params); + const valueAxes = getValueAxes(dimensions.y); + + // TODO: what do we want to put in this "vis config"? + const visConfig = { + addLegend, + legendPosition, + addTimeMarker: false, + addTooltip, + dimensions, + valueAxes, + }; + + const vislib = buildExpressionFunction<any>('vislib', { + type, + visConfig: JSON.stringify(visConfig), + }); + + return buildExpression([...expressionFns, vislib]).toString(); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx new file mode 100644 index 000000000000..6b088a68547f --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/basic_vis_options.tsx @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { Draft } from 'immer'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; +import { getConfigCollections } from '../../../../../vis_type_vislib/public'; +import { BasicOptionsDefaults } from './types'; + +interface Props { + styleState: BasicOptionsDefaults; + setOption: (callback: (draft: Draft<BasicOptionsDefaults>) => void) => void; +} + +export const BasicVisOptions = ({ styleState, setOption }: Props) => { + const { legendPositions } = getConfigCollections(); + return ( + <> + <SelectOption + label={i18n.translate('charts.controls.vislibBasicOptions.legendPositionLabel', { + defaultMessage: 'Legend position', + })} + options={legendPositions} + paramName="legendPosition" + value={styleState.legendPosition} + setValue={(_, value) => + setOption((draft) => { + draft.legendPosition = value; + }) + } + /> + <SwitchOption + label={i18n.translate('charts.controls.vislibBasicOptions.showTooltipLabel', { + defaultMessage: 'Show tooltip', + })} + paramName="addTooltip" + value={styleState.addTooltip} + setValue={(_, value) => + setOption((draft) => { + draft.addTooltip = value; + }) + } + /> + </> + ); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/common/get_pipeline_params.ts b/src/plugins/wizard/public/visualizations/vislib/common/get_pipeline_params.ts new file mode 100644 index 000000000000..12288c138b2e --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/get_pipeline_params.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BuildPipelineParams } from '../../../../../visualizations/public'; +import { getTimeFilter } from '../../../plugin_services'; + +export const getPipelineParams = (): BuildPipelineParams => { + const timeFilter = getTimeFilter(); + return { + timefilter: timeFilter, + timeRange: timeFilter.getTime(), + }; +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts b/src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts new file mode 100644 index 000000000000..86c135110f50 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/get_value_axes.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SchemaConfig } from '../../../../../visualizations/public'; +import { ValueAxis } from '../../../../../vis_type_vislib/public'; + +interface ValueAxisConfig extends ValueAxis { + style: any; +} + +export const getValueAxes = (yAxes: SchemaConfig[]): ValueAxisConfig[] => + yAxes.map((y, index) => ({ + id: `ValueAxis-${index + 1}`, + labels: { + show: true, + }, + name: `ValueAxis-${index + 1}`, + position: 'left', + scale: { + type: 'linear', + mode: 'normal', + }, + show: true, + style: {}, + title: { + text: y.label, + }, + type: 'value', + })); diff --git a/src/plugins/wizard/public/visualizations/vislib/common/index.ts b/src/plugins/wizard/public/visualizations/vislib/common/index.ts new file mode 100644 index 000000000000..70614ce555eb --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './basic_vis_options'; +export * from './get_pipeline_params'; +export * from './get_value_axes'; +export * from './types'; diff --git a/src/plugins/wizard/public/visualizations/vislib/common/types.ts b/src/plugins/wizard/public/visualizations/vislib/common/types.ts new file mode 100644 index 000000000000..4861c1af2e55 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/common/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Positions } from '../../../../../vis_type_vislib/public'; +import { RootState } from '../../../application/utils/state_management'; + +export interface BasicOptionsDefaults { + addTooltip: boolean; + addLegend: boolean; + legendPosition: Positions; + type: string; +} + +export interface VislibRootState<T extends BasicOptionsDefaults> extends RootState { + style: T; +} diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx new file mode 100644 index 000000000000..873b26ca4301 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { HistogramOptionsDefaults } from '../histogram_vis_type'; +import { BasicVisOptions } from '../../common/basic_vis_options'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; + +function HistogramVisOptions() { + const styleState = useTypedSelector((state) => state.style) as HistogramOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft<typeof styleState>) => void) => { + const newState = produce(styleState, callback); + dispatch(setState<HistogramOptionsDefaults>(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + <Option + title={i18n.translate('visTypeVislib.histogram.params.settingsTitle', { + defaultMessage: 'Settings', + })} + initialIsOpen + > + <BasicVisOptions styleState={styleState} setOption={setOption} /> + </Option> + </> + ); +} + +export { HistogramVisOptions }; diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts b/src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts new file mode 100644 index 000000000000..e99f062cb615 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/histogram_vis_type.ts @@ -0,0 +1,64 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { BasicOptionsDefaults } from '../common/types'; +import { HistogramVisOptions } from './components/histogram_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; + +export interface HistogramOptionsDefaults extends BasicOptionsDefaults { + type: 'histogram'; +} + +export const createHistogramConfig = (): VisualizationTypeOptions<HistogramOptionsDefaults> => ({ + name: 'histogram', + title: 'Histogram', + icon: 'visBarVertical', + description: 'Display histogram visualizations', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.histogram.metricTitle', { + defaultMessage: 'Y-axis', + }), + min: 1, + max: 3, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: { aggTypes: ['median'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('visTypeVislib.histogram.segmentTitle', { + defaultMessage: 'X-axis', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!filters'], + defaults: { aggTypes: ['terms'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + type: 'histogram', + }, + render: HistogramVisOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/index.ts b/src/plugins/wizard/public/visualizations/vislib/histogram/index.ts new file mode 100644 index 000000000000..bba280de2d77 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createHistogramConfig } from './histogram_vis_type'; diff --git a/src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts b/src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts new file mode 100644 index 000000000000..6357b6acee22 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/histogram/to_expression.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis, buildVislibDimensions } from '../../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../../../expressions/public'; +import { HistogramOptionsDefaults } from './histogram_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState, getValueAxes, getPipelineParams } from '../common'; + +export const toExpression = async ({ + style: styleState, + visualization, +}: VislibRootState<HistogramOptionsDefaults>) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + const { addLegend, addTooltip, legendPosition, type } = styleState; + const params = getPipelineParams(); + + const vis = new Vis(type); + vis.data.aggs = aggConfigs; + + const dimensions = await buildVislibDimensions(vis, params); + const valueAxes = getValueAxes(dimensions.y); + + // TODO: what do we want to put in this "vis config"? + const visConfig = { + addLegend, + legendPosition, + addTimeMarker: false, + addTooltip, + dimensions, + valueAxes, + }; + + const vislib = buildExpressionFunction<any>('vislib', { + type, + visConfig: JSON.stringify(visConfig), + }); + + return buildExpression([...expressionFns, vislib]).toString(); +}; diff --git a/src/plugins/wizard/public/visualizations/vislib/index.ts b/src/plugins/wizard/public/visualizations/vislib/index.ts new file mode 100644 index 000000000000..84dc3e346ef5 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createHistogramConfig } from './histogram'; +export { createLineConfig } from './line'; +export { createAreaConfig } from './area'; diff --git a/src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx b/src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx new file mode 100644 index 000000000000..a5bb1994c92a --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/components/line_vis_options.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { LineOptionsDefaults } from '../line_vis_type'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; +import { BasicVisOptions } from '../../common/basic_vis_options'; + +function LineVisOptions() { + const styleState = useTypedSelector((state) => state.style) as LineOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft<typeof styleState>) => void) => { + const newState = produce(styleState, callback); + dispatch(setState<LineOptionsDefaults>(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + <Option + title={i18n.translate('visTypeVislib.line.params.settingsTitle', { + defaultMessage: 'Settings', + })} + initialIsOpen + > + <BasicVisOptions styleState={styleState} setOption={setOption} /> + </Option> + </> + ); +} + +export { LineVisOptions }; diff --git a/src/plugins/wizard/public/visualizations/vislib/line/index.ts b/src/plugins/wizard/public/visualizations/vislib/line/index.ts new file mode 100644 index 000000000000..721ec7858a7a --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createLineConfig } from './line_vis_type'; diff --git a/src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts b/src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts new file mode 100644 index 000000000000..b92019cddd9d --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/line_vis_type.ts @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { LineVisOptions } from './components/line_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; +import { BasicOptionsDefaults } from '../common/types'; + +export interface LineOptionsDefaults extends BasicOptionsDefaults { + type: 'line'; +} + +export const createLineConfig = (): VisualizationTypeOptions<LineOptionsDefaults> => ({ + name: 'line', + title: 'Line', + icon: 'visLine', + description: 'Display line chart', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.line.metricTitle', { + defaultMessage: 'Y-axis', + }), + min: 1, + max: 3, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: { aggTypes: ['median'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('visTypeVislib.line.segmentTitle', { + defaultMessage: 'X-axis', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!filters'], + defaults: { aggTypes: ['terms'] }, + }, + { + group: AggGroupNames.Metrics, + name: 'radius', + title: i18n.translate('visTypeVislib.line.radiusTitle', { + defaultMessage: 'Dot size', + }), + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], + defaults: { aggTypes: ['count'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + type: 'line', + }, + render: LineVisOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts b/src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts new file mode 100644 index 000000000000..32d40726bedf --- /dev/null +++ b/src/plugins/wizard/public/visualizations/vislib/line/to_expression.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Vis, buildVislibDimensions } from '../../../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../../../expressions/public'; +import { LineOptionsDefaults } from './line_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState, getValueAxes, getPipelineParams } from '../common'; + +export const toExpression = async ({ + style: styleState, + visualization, +}: VislibRootState<LineOptionsDefaults>) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); + const { addLegend, addTooltip, legendPosition, type } = styleState; + const params = getPipelineParams(); + + const vis = new Vis(type); + vis.data.aggs = aggConfigs; + + const dimensions = await buildVislibDimensions(vis, params); + const valueAxes = getValueAxes(dimensions.y); + + // TODO: what do we want to put in this "vis config"? + const visConfig = { + addLegend, + legendPosition, + addTimeMarker: false, + addTooltip, + dimensions, + valueAxes, + }; + + const vislib = buildExpressionFunction<any>('vislib', { + type, + visConfig: JSON.stringify(visConfig), + }); + + return buildExpression([...expressionFns, vislib]).toString(); +}; diff --git a/src/plugins/wizard/server/capabilities_provider.ts b/src/plugins/wizard/server/capabilities_provider.ts new file mode 100644 index 000000000000..9bbede2d53a9 --- /dev/null +++ b/src/plugins/wizard/server/capabilities_provider.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const capabilitiesProvider = () => ({ + wizard: { + // TODO: investigate which capabilities we need to provide + // createNew: true, + // createShortUrl: true, + // delete: true, + show: true, + // showWriteControls: true, + // save: true, + // saveQuery: true, + }, +}); diff --git a/src/plugins/wizard/server/index.ts b/src/plugins/wizard/server/index.ts new file mode 100644 index 000000000000..cd5e3aa3a30e --- /dev/null +++ b/src/plugins/wizard/server/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; +import { ConfigSchema, configSchema } from '../config'; +import { WizardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as the OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new WizardPlugin(initializerContext); +} + +export { WizardPluginSetup, WizardPluginStart } from './types'; + +export const config: PluginConfigDescriptor<ConfigSchema> = { + exposeToBrowser: { + enabled: true, + }, + schema: configSchema, +}; diff --git a/src/plugins/wizard/server/plugin.ts b/src/plugins/wizard/server/plugin.ts new file mode 100644 index 000000000000..25b7f27c1f81 --- /dev/null +++ b/src/plugins/wizard/server/plugin.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { WizardPluginSetup, WizardPluginStart } from './types'; +import { capabilitiesProvider } from './capabilities_provider'; +import { wizardSavedObjectType } from './saved_objects'; + +export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart> { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup({ capabilities, http, savedObjects }: CoreSetup) { + this.logger.debug('wizard: Setup'); + + // Register saved object types + savedObjects.registerType(wizardSavedObjectType); + + // Register capabilities + capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(_core: CoreStart) { + this.logger.debug('wizard: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/wizard/server/saved_objects/index.ts b/src/plugins/wizard/server/saved_objects/index.ts new file mode 100644 index 000000000000..eabc7abf2761 --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { wizardSavedObjectType } from './wizard_app'; diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts new file mode 100644 index 000000000000..f9b57d53e3de --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObject, SavedObjectsType } from '../../../../core/server'; +import { + EDIT_PATH, + PLUGIN_ID, + WizardSavedObjectAttributes, + WIZARD_SAVED_OBJECT, +} from '../../common'; +import { wizardSavedObjectTypeMigrations } from './wizard_migration'; + +export const wizardSavedObjectType: SavedObjectsType = { + name: WIZARD_SAVED_OBJECT, + hidden: false, + namespaceType: 'single', + management: { + // icon: '', // TODO: Need a custom icon here - unfortunately a custom SVG won't work without changes to the SavedObjectsManagement plugin + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: ({ attributes: { title } }: SavedObject<WizardSavedObjectAttributes>) => title, + getEditUrl: ({ id }: SavedObject) => + `/management/opensearch-dashboards/objects/savedWizard/${encodeURIComponent(id)}`, + getInAppUrl({ id }: SavedObject) { + return { + path: `/app/${PLUGIN_ID}${EDIT_PATH}/${encodeURIComponent(id)}`, + uiCapabilitiesPath: 'wizard.show', + }; + }, + }, + migrations: wizardSavedObjectTypeMigrations, + mappings: { + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + visualizationState: { + type: 'text', + index: false, + }, + styleState: { + type: 'text', + index: false, + }, + version: { type: 'integer' }, + // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow + // When we save a saved object, the saved object plugin will extract the search source into two parts + // Some information will be put into kibanaSavedObjectMeta while others will be created as a reference object and pushed to the reference array + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false } }, + }, + }, + }, +}; diff --git a/src/plugins/wizard/server/saved_objects/wizard_migration.test.ts b/src/plugins/wizard/server/saved_objects/wizard_migration.test.ts new file mode 100644 index 000000000000..0e9248e1951f --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_migration.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectMigrationFn, SavedObjectMigrationContext } from '../../../../core/server'; +import { wizardSavedObjectTypeMigrations } from './wizard_migration'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('2.3.0', () => { + const migrate = (doc: any) => + wizardSavedObjectTypeMigrations['2.3.0']( + doc as Parameters<SavedObjectMigrationFn>[0], + savedObjectMigrationContext + ); + + it('should return original doc if visualizationState is not found', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: {}, + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: {}, + }); + }); + + it('should return original doc if indexPattern is not found within visualizationState', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: { + visualizationState: { + searchSource: '', + activeVisualization: {}, + }, + }, + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: { + visualizationState: { + searchSource: '', + activeVisualization: {}, + }, + }, + }); + }); + + it('should return original doc if references is not an array', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: { + visualizationState: {}, + }, + references: {}, + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: { + visualizationState: {}, + }, + references: {}, + }); + }); + + it('should migrate the old version wizard saved object to new version wizard saved object', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: { + visualizationState: JSON.stringify({ + searchFields: {}, + activeVisualization: {}, + indexPattern: 'indexPatternId', + }), + version: 1, + }, + references: [], + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: { + visualizationState: JSON.stringify({ + searchFields: {}, + activeVisualization: {}, + }), + version: 2, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }), + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'indexPatternId', + }, + ], + }); + }); +}); diff --git a/src/plugins/wizard/server/saved_objects/wizard_migration.ts b/src/plugins/wizard/server/saved_objects/wizard_migration.ts new file mode 100644 index 000000000000..dcbaf8252218 --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_migration.ts @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get, flow } from 'lodash'; +import { SavedObjectMigrationFn } from '../../../../core/server'; + +const migrateIndexPattern: SavedObjectMigrationFn<any, any> = (doc) => { + try { + const visualizationStateJSON = get(doc, 'attributes.visualizationState'); + const visualizationState = JSON.parse(visualizationStateJSON); + const indexPatternId = visualizationState.indexPattern; + const indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + + if (indexPatternId && Array.isArray(doc.references)) { + const searchSourceIndex = { + indexRefName, + }; + const visualizationWithoutIndex = { + searchFields: visualizationState.searchFields, + activeVisualization: visualizationState.activeVisualization, + }; + doc.attributes.visualizationState = JSON.stringify(visualizationWithoutIndex); + + doc.references.push({ + name: indexRefName, + type: 'index-pattern', + id: indexPatternId, + }); + doc.attributes.version = 2; + + return { + ...doc, + attributes: { + ...doc.attributes, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify(searchSourceIndex), + }, + }, + }; + } + return doc; + } catch (e) { + return doc; + } +}; + +export const wizardSavedObjectTypeMigrations = { + '2.3.0': flow(migrateIndexPattern), +}; diff --git a/src/plugins/wizard/server/types.ts b/src/plugins/wizard/server/types.ts new file mode 100644 index 000000000000..69f9ea0996d3 --- /dev/null +++ b/src/plugins/wizard/server/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// We need to export plugin server types, even if empty +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WizardPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WizardPluginStart {} diff --git a/src/plugins/wizard/tsconfig.json b/src/plugins/wizard/tsconfig.json new file mode 100644 index 000000000000..ab8821948be5 --- /dev/null +++ b/src/plugins/wizard/tsconfig.json @@ -0,0 +1,74 @@ +{ + "compilerOptions": { + "baseUrl": "./src/plugins/wizard", + "paths": { + // Allows for importing from `opensearch-dashboards` package for the exported types. + "opensearch-dashboards": ["./opensearch_dashboards"], + "opensearch-dashboards/public": ["src/core/public"], + "opensearch-dashboards/server": ["src/core/server"], + "plugins/*": ["src/legacy/core_plugins/*/public/"], + "test_utils/*": [ + "src/test_utils/public/*" + ], + "fixtures/*": ["src/fixtures/*"], + "@opensearch-project/opensearch": ["node_modules/@opensearch-project/opensearch/api/new"] + }, + // Support .tsx files and transform JSX into calls to React.createElement + "jsx": "react", + // Enables all strict type checking options. + "strict": true, + // save information about the project graph on disk + "incremental": true, + // enables "core language features" + "lib": [ + "esnext", + // includes support for browser APIs + "dom" + ], + // Node 8 should support everything output by esnext, we override this + // in webpack with loader-level compiler options + "target": "esnext", + // Use commonjs for node, overridden in webpack to keep import statements + // to maintain support for things like `await import()` + "module": "commonjs", + // Allows default imports from modules with no default export. This does not affect code emit, just type checking. + // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or + // ESNext module format is used. + "allowSyntheticDefaultImports": true, + // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. + "esModuleInterop": true, + // Resolve modules in the same way as Node.js. Aka make `require` works the + // same in TypeScript as it does in Node.js. + "moduleResolution": "node", + // "resolveJsonModule" allows for importing, extracting types from and generating .json files. + "resolveJsonModule": true, + // Disallow inconsistently-cased references to the same file. + "forceConsistentCasingInFileNames": true, + // Forbid unused local variables as the rule was deprecated by ts-lint + "noUnusedLocals": true, + // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. + "downlevelIteration": true, + // import tslib helpers rather than inlining helpers for iteration or spreading, for instance + "importHelpers": true, + // adding global typings + "noImplicitAny": false, + "types": [ + "node", + "jest", + "react", + "flot", + "@testing-library/jest-dom", + "resize-observer-polyfill" + ] + }, + "include": [ + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../core/tsconfig.json" } + ] +} diff --git a/src/setup_node_env/node_version_validator.test.js b/src/setup_node_env/node_version_validator.test.js index 4e95f7219d5a..cb3639154c6c 100644 --- a/src/setup_node_env/node_version_validator.test.js +++ b/src/setup_node_env/node_version_validator.test.js @@ -43,7 +43,12 @@ describe('NodeVersionValidator', function () { }); it('should run the script WITH error if the patch version is lower', function (done) { - testValidateNodeVersion(done, requiredNodeVersionWithDiff(0, 0, -1), true); + var lowerPatchversion = requiredNodeVersionWithDiff(0, 0, -1); + testValidateNodeVersion( + done, + lowerPatchversion, + REQUIRED_NODE_JS_VERSION !== lowerPatchversion + ); }); it('should run the script WITH error if the major version is higher', function (done) { @@ -51,7 +56,12 @@ describe('NodeVersionValidator', function () { }); it('should run the script WITH error if the major version is lower', function (done) { - testValidateNodeVersion(done, requiredNodeVersionWithDiff(-1, 0, 0), true); + var lowerMajorVersion = requiredNodeVersionWithDiff(-1, 0, 0); + testValidateNodeVersion( + done, + lowerMajorVersion, + REQUIRED_NODE_JS_VERSION !== lowerMajorVersion + ); }); it('should run the script WITH error if the minor version is higher', function (done) { @@ -59,15 +69,20 @@ describe('NodeVersionValidator', function () { }); it('should run the script WITH error if the minor version is lower', function (done) { - testValidateNodeVersion(done, requiredNodeVersionWithDiff(0, -1, 0), true); + var lowerMinorVersion = requiredNodeVersionWithDiff(0, -1, 0); + testValidateNodeVersion( + done, + lowerMinorVersion, + REQUIRED_NODE_JS_VERSION !== lowerMinorVersion + ); }); }); function requiredNodeVersionWithDiff(majorDiff, minorDiff, patchDiff) { var matches = REQUIRED_NODE_JS_VERSION.match(/^v(\d+)\.(\d+)\.(\d+)/); - var major = parseInt(matches[1]) + majorDiff; - var minor = parseInt(matches[2]) + minorDiff; - var patch = parseInt(matches[3]) + patchDiff; + var major = Math.max(parseInt(matches[1], 10) + majorDiff, 0); + var minor = Math.max(parseInt(matches[2], 10) + minorDiff, 0); + var patch = Math.max(parseInt(matches[3], 10) + patchDiff, 0); return `v${major}.${minor}.${patch}`; } diff --git a/tasks/docker_docs/Dockerfile b/tasks/docker_docs/Dockerfile index 435b78f89f2e..479bdde17a0b 100644 --- a/tasks/docker_docs/Dockerfile +++ b/tasks/docker_docs/Dockerfile @@ -15,4 +15,4 @@ USER kibana RUN git clone --depth 1 https://github.com/elastic/docs.git /home/kibana/docs_builder WORKDIR /home/kibana/docs_builder -CMD git pull origin master && ./build_docs.pl --doc /home/kibana/ascii_docs/index.asciidoc --out /home/kibana/html_docs --chunk=1 +CMD git pull origin main && ./build_docs.pl --doc /home/kibana/ascii_docs/index.asciidoc --out /home/kibana/html_docs --chunk=1 diff --git a/test/api_integration/apis/core/index.js b/test/api_integration/apis/core/index.js index edca353e9af0..d35f9ad10590 100644 --- a/test/api_integration/apis/core/index.js +++ b/test/api_integration/apis/core/index.js @@ -44,7 +44,7 @@ export default function ({ getService }) { }); }); - it(`uses compression when there is a whitelisted referer`, async () => { + it(`uses compression when there is a allowlisted referer`, async () => { await supertest .get('/app/opensearch-dashboards') .set('accept-encoding', 'gzip') @@ -54,7 +54,7 @@ export default function ({ getService }) { }); }); - it(`doesn't use compression when there is a non-whitelisted referer`, async () => { + it(`doesn't use compression when there is a non-allowlisted referer`, async () => { await supertest .get('/app/opensearch-dashboards') .set('accept-encoding', 'gzip') diff --git a/test/common/config.js b/test/common/config.js index 5db5748087a3..26abcc2fa586 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -78,6 +78,7 @@ export default function () { `--opensearchDashboards.branding.mark.defaultUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg`, `--opensearchDashboards.branding.mark.darkModeUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg`, `--opensearchDashboards.branding.applicationTitle=OpenSearch`, + `--wizard.enabled=true`, ], }, services, diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts index 2dac8c05849e..434bb9cb69cc 100644 --- a/test/examples/embeddables/dashboard.ts +++ b/test/examples/embeddables/dashboard.ts @@ -30,7 +30,7 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; -export const testDashboardInput = { +const testDashboardInput = { panels: { '1': { gridData: { diff --git a/test/functional/apps/home/_add_data.js b/test/functional/apps/home/_add_data.ts similarity index 92% rename from test/functional/apps/home/_add_data.js rename to test/functional/apps/home/_add_data.ts index d5b9a25de660..44cfdd17aa0d 100644 --- a/test/functional/apps/home/_add_data.js +++ b/test/functional/apps/home/_add_data.ts @@ -29,8 +29,9 @@ */ import expect from '@osd/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard']); diff --git a/test/functional/apps/home/_home.js b/test/functional/apps/home/_home.ts similarity index 77% rename from test/functional/apps/home/_home.js rename to test/functional/apps/home/_home.ts index 2b2a8f4b7c2d..fb09ad836272 100644 --- a/test/functional/apps/home/_home.js +++ b/test/functional/apps/home/_home.ts @@ -29,8 +29,9 @@ */ import expect from '@osd/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const globalNav = getService('globalNav'); const PageObjects = getPageObjects(['common', 'header', 'home']); @@ -38,7 +39,7 @@ export default function ({ getService, getPageObjects }) { describe('OpenSearch Dashboards takes you home', function describeIndexTests() { this.tags('includeFirefox'); - it('clicking on opensearch-dashboards logo should take you to home page', async () => { + it('clicking on logo should take you to home page', async () => { await PageObjects.common.navigateToApp('settings'); await globalNav.clickLogo(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -46,6 +47,14 @@ export default function ({ getService, getPageObjects }) { expect(url.includes('/app/home')).to.be(true); }); + it('clicking on home button should take you to home page', async () => { + await PageObjects.common.navigateToApp('settings'); + await globalNav.clickHomeButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const url = await browser.getCurrentUrl(); + expect(url.includes('/app/home')).to.be(true); + }); + it('clicking on console on homepage should take you to console app', async () => { await PageObjects.home.clickSynopsis('console'); const url = await browser.getCurrentUrl(); diff --git a/test/functional/apps/home/index.js b/test/functional/apps/home/index.ts similarity index 91% rename from test/functional/apps/home/index.js rename to test/functional/apps/home/index.ts index 7334a66f2698..4e318867c800 100644 --- a/test/functional/apps/home/index.js +++ b/test/functional/apps/home/index.ts @@ -28,7 +28,9 @@ * under the License. */ -export default function ({ getService, loadTestFile }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); describe('homepage app', function () { diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 7ddeb6c9b9cd..63538541e813 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -65,6 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Timeline', 'Vega', 'Vertical Bar', + 'Wizard', // TODO: Update to final name when ready ]; if (!isOss) { expectedChartTypes.push('Maps', 'Lens'); diff --git a/test/functional/apps/visualize/_custom_branding.js b/test/functional/apps/visualize/_custom_branding.js deleted file mode 100644 index fe22524db4bb..000000000000 --- a/test/functional/apps/visualize/_custom_branding.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import expect from '@osd/expect'; -import { UI_SETTINGS } from '../../../../src/plugins/data/common'; - -export default function ({ getService, getPageObjects }) { - const browser = getService('browser'); - const globalNav = getService('globalNav'); - const opensearchArchiver = getService('opensearchArchiver'); - const opensearchDashboardsServer = getService('opensearchDashboardsServer'); - const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'home', 'header', 'settings']); - const testSubjects = getService('testSubjects'); - - const expectedFullLogo = - 'https://opensearch.org/assets/brand/SVG/Logo/opensearch_logo_default.svg'; - const expectedFullLogoDarkMode = - 'https://opensearch.org/assets/brand/SVG/Logo/opensearch_logo_darkmode.svg'; - const expectedMarkLogo = - 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg'; - const expectedMarkLogoDarkMode = - 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg'; - const applicationTitle = 'OpenSearch'; - const expectedWelcomeMessage = 'Welcome to OpenSearch'; - - describe('OpenSearch Dashboards branding configuration', function customHomeBranding() { - describe('should render overview page', async () => { - this.tags('includeFirefox'); - - before(async function () { - await PageObjects.common.navigateToApp('home'); - await PageObjects.common.navigateToApp('opensearch_dashboards_overview'); - }); - - it('with customized logo for opensearch overview header in default mode', async () => { - await testSubjects.existOrFail('osdOverviewPageHeaderLogo'); - const actualLabel = await testSubjects.getAttribute( - 'osdOverviewPageHeaderLogo', - 'data-test-logo' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); - }); - - it('with customized logo for opensearch overview header in dark mode', async () => { - await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); - await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); - await PageObjects.common.navigateToApp('opensearch_dashboards_overview'); - await testSubjects.existOrFail('osdOverviewPageHeaderLogo'); - const actualLabel = await testSubjects.getAttribute( - 'osdOverviewPageHeaderLogo', - 'data-test-logo' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); - }); - }); - - describe('should render welcome page', async () => { - this.tags('includeFirefox'); - - //unloading any pre-existing settings so the welcome page will appear - before(async function () { - await opensearchArchiver.unload('logstash_functional'); - await opensearchArchiver.unload('long_window_logstash'); - await opensearchArchiver.unload('visualize'); - await PageObjects.common.navigateToApp('home'); - }); - - //loading the settings again for - after(async function () { - await browser.setWindowSize(1280, 800); - await opensearchArchiver.loadIfNeeded('logstash_functional'); - await opensearchArchiver.loadIfNeeded('long_window_logstash'); - await opensearchArchiver.load('visualize'); - await opensearchDashboardsServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', - }); - }); - - it('with customized logo', async () => { - await testSubjects.existOrFail('welcomeCustomLogo'); - const actualLabel = await testSubjects.getAttribute( - 'welcomeCustomLogo', - 'data-test-image-url' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); - }); - - it('with customized title', async () => { - await testSubjects.existOrFail('welcomeCustomTitle'); - const actualLabel = await testSubjects.getAttribute( - 'welcomeCustomTitle', - 'data-test-title-message' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedWelcomeMessage.toUpperCase()); - }); - - it('with customized logo in dark mode', async () => { - await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); - await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); - await PageObjects.common.navigateToApp('home'); - await testSubjects.existOrFail('welcomeCustomLogo'); - const actualLabel = await testSubjects.getAttribute( - 'welcomeCustomLogo', - 'data-test-image-url' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); - }); - }); - - describe('should render home page', async () => { - this.tags('includeFirefox'); - - before(async function () { - await PageObjects.common.navigateToApp('home'); - }); - - after(async function () { - await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); - await PageObjects.settings.clearAdvancedSettings('theme:darkMode'); - await PageObjects.common.navigateToApp('home'); - }); - - it('with customized logo in header bar', async () => { - await globalNav.logoExistsOrFail(expectedFullLogo); - }); - - it('with customized logo that can take back to home page', async () => { - await PageObjects.common.navigateToApp('settings'); - await globalNav.clickLogo(); - await PageObjects.header.waitUntilLoadingHasFinished(); - const url = await browser.getCurrentUrl(); - expect(url.includes('/app/home')).to.be(true); - }); - - it('with customized logo in home dashboard card', async () => { - await testSubjects.existOrFail('dashboardCustomLogo'); - const actualLabel = await testSubjects.getAttribute( - 'dashboardCustomLogo', - 'data-test-image-url' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); - }); - - it('with customized title in home dashboard card', async () => { - await testSubjects.existOrFail('dashboardCustomTitle'); - const actualLabel = await testSubjects.getAttribute( - 'dashboardCustomTitle', - 'data-test-title' - ); - expect(actualLabel.toUpperCase()).to.equal(applicationTitle.toUpperCase()); - }); - - it('with customized logo for opensearch in side menu', async () => { - await appsMenu.openCollapsibleNav(); - await testSubjects.existOrFail('collapsibleNavGroup-opensearchDashboards'); - const actualLabel = await testSubjects.getAttribute( - 'collapsibleNavGroup-opensearchDashboards', - 'data-test-opensearch-logo' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); - }); - - it('with customized logo in header bar in dark mode', async () => { - await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); - await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); - await PageObjects.common.navigateToApp('home'); - await globalNav.logoExistsOrFail(expectedFullLogoDarkMode); - }); - - it('with customized logo that can take back to home page in dark mode', async () => { - await PageObjects.common.navigateToApp('settings'); - await globalNav.clickLogo(); - await PageObjects.header.waitUntilLoadingHasFinished(); - const url = await browser.getCurrentUrl(); - expect(url.includes('/app/home')).to.be(true); - }); - - it('with customized logo in home dashboard card in dark mode', async () => { - await testSubjects.existOrFail('dashboardCustomLogo'); - const actualLabel = await testSubjects.getAttribute( - 'dashboardCustomLogo', - 'data-test-image-url' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); - }); - - it('with customized logo for opensearch in side menu in dark mode', async () => { - await appsMenu.openCollapsibleNav(); - await testSubjects.existOrFail('collapsibleNavGroup-opensearchDashboards'); - const actualLabel = await testSubjects.getAttribute( - 'collapsibleNavGroup-opensearchDashboards', - 'data-test-opensearch-logo' - ); - expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_custom_branding.ts b/test/functional/apps/visualize/_custom_branding.ts new file mode 100644 index 000000000000..37f07e932ee5 --- /dev/null +++ b/test/functional/apps/visualize/_custom_branding.ts @@ -0,0 +1,237 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const globalNav = getService('globalNav'); + const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'home', 'header', 'settings']); + const testSubjects = getService('testSubjects'); + + const expectedFullLogo = + 'https://opensearch.org/assets/brand/SVG/Logo/opensearch_logo_default.svg'; + const expectedFullLogoDarkMode = + 'https://opensearch.org/assets/brand/SVG/Logo/opensearch_logo_darkmode.svg'; + const expectedMarkLogo = + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg'; + const expectedMarkLogoDarkMode = + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg'; + const applicationTitle = 'OpenSearch'; + const expectedWelcomeMessage = 'Welcome to OpenSearch'; + + describe('OpenSearch Dashboards branding configuration', function customHomeBranding() { + describe('should render overview page', async () => { + this.tags('includeFirefox'); + + before(async function () { + await PageObjects.common.navigateToApp('home'); + await PageObjects.common.navigateToApp('opensearch_dashboards_overview'); + }); + + it('with customized logo for opensearch overview header in default mode', async () => { + await testSubjects.existOrFail('osdOverviewPageHeaderLogo'); + const actualLabel = await testSubjects.getAttribute( + 'osdOverviewPageHeaderLogo', + 'data-test-logo' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); + }); + + it('with customized logo for opensearch overview header in dark mode', async () => { + await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); + await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); + await PageObjects.common.navigateToApp('opensearch_dashboards_overview'); + await testSubjects.existOrFail('osdOverviewPageHeaderLogo'); + const actualLabel = await testSubjects.getAttribute( + 'osdOverviewPageHeaderLogo', + 'data-test-logo' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); + }); + }); + + describe('should render welcome page', async () => { + this.tags('includeFirefox'); + + // unloading any pre-existing settings so the welcome page will appear + before(async function () { + await opensearchArchiver.unload('logstash_functional'); + await opensearchArchiver.unload('long_window_logstash'); + await opensearchArchiver.unload('visualize'); + await PageObjects.common.navigateToApp('home'); + }); + + // loading the settings again for + after(async function () { + await browser.setWindowSize(1280, 800); + await opensearchArchiver.loadIfNeeded('logstash_functional'); + await opensearchArchiver.loadIfNeeded('long_window_logstash'); + await opensearchArchiver.load('visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + }); + + it('with customized logo', async () => { + await testSubjects.existOrFail('welcomeCustomLogo'); + const actualLabel = await testSubjects.getAttribute( + 'welcomeCustomLogo', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); + }); + + it('with customized title', async () => { + await testSubjects.existOrFail('welcomeCustomTitle'); + const actualLabel = await testSubjects.getAttribute( + 'welcomeCustomTitle', + 'data-test-title-message' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedWelcomeMessage.toUpperCase()); + }); + + it('with customized logo in dark mode', async () => { + await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); + await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); + await PageObjects.common.navigateToApp('home'); + await testSubjects.existOrFail('welcomeCustomLogo'); + const actualLabel = await testSubjects.getAttribute( + 'welcomeCustomLogo', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); + }); + }); + + describe('should render home page', async () => { + this.tags('includeFirefox'); + + before(async function () { + await PageObjects.common.navigateToApp('home'); + }); + + after(async function () { + await PageObjects.common.navigateToApp('home'); + }); + + describe('in default mode', async () => { + it('with customized logo in header bar', async () => { + await globalNav.logoExistsOrFail(expectedFullLogo); + }); + + it('with customized mark logo button in header bar', async () => { + await globalNav.homeMarkExistsOrFail(expectedMarkLogo); + }); + + it('with customized logo button that navigates to home page', async () => { + await PageObjects.common.navigateToApp('settings'); + await globalNav.clickLogo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const url = await browser.getCurrentUrl(); + expect(url.includes('/app/home')).to.be(true); + }); + + it('with customized mark logo button that navigates to home page', async () => { + await PageObjects.common.navigateToApp('settings'); + await globalNav.clickHomeButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const url = await browser.getCurrentUrl(); + expect(url.includes('/app/home')).to.be(true); + }); + + it('with customized mark logo in home dashboard card', async () => { + await testSubjects.existOrFail('dashboardCustomLogo'); + const actualLabel = await testSubjects.getAttribute( + 'dashboardCustomLogo', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); + }); + + it('with customized title in home dashboard card', async () => { + await testSubjects.existOrFail('dashboardCustomTitle'); + const actualLabel = await testSubjects.getAttribute( + 'dashboardCustomTitle', + 'data-test-title' + ); + expect(actualLabel.toUpperCase()).to.equal(applicationTitle.toUpperCase()); + }); + + it('with customized mark logo for opensearch in side menu', async () => { + await appsMenu.openCollapsibleNav(); + await testSubjects.existOrFail('collapsibleNavGroup-opensearchDashboards'); + const actualLabel = await testSubjects.getAttribute( + 'collapsibleNavGroup-opensearchDashboards', + 'data-test-opensearch-logo' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); + }); + }); + + describe('in dark mode', async () => { + before(async function () { + await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); + await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async function () { + await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); + await PageObjects.settings.clearAdvancedSettings('theme:darkMode'); + }); + + it('with customized logo in header bar', async () => { + await globalNav.logoExistsOrFail(expectedFullLogoDarkMode); + }); + + it('with customized mark logo button in header bar', async () => { + await globalNav.homeMarkExistsOrFail(expectedMarkLogoDarkMode); + }); + + it('with customized logo that navigates to home page', async () => { + await PageObjects.common.navigateToApp('settings'); + await globalNav.clickLogo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const url = await browser.getCurrentUrl(); + expect(url.includes('/app/home')).to.be(true); + }); + + it('with customized mark logo button that navigates to home page', async () => { + await PageObjects.common.navigateToApp('settings'); + await globalNav.clickHomeButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const url = await browser.getCurrentUrl(); + expect(url.includes('/app/home')).to.be(true); + }); + + it('with customized mark logo in home dashboard card', async () => { + await testSubjects.existOrFail('dashboardCustomLogo'); + const actualLabel = await testSubjects.getAttribute( + 'dashboardCustomLogo', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); + }); + + it('with customized mark logo for opensearch in side menu', async () => { + await appsMenu.openCollapsibleNav(); + await testSubjects.existOrFail('collapsibleNavGroup-opensearchDashboards'); + const actualLabel = await testSubjects.getAttribute( + 'collapsibleNavGroup-opensearchDashboards', + 'data-test-opensearch-logo' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); + }); + }); + }); + }); +} diff --git a/test/functional/apps/visualize/_metric_chart.js b/test/functional/apps/visualize/_metric_chart.js index d70454753347..b0cc2395a7d0 100644 --- a/test/functional/apps/visualize/_metric_chart.js +++ b/test/functional/apps/visualize/_metric_chart.js @@ -170,12 +170,15 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visEditor.clickGo(); await retry.try(async function tryingForTime() { const metricValue = await PageObjects.visChart.getMetric(); - expect(percentileMachineRam).to.eql(metricValue); + // TODO: Restore when inconsistent values are fixed from https://github.com/opensearch-project/OpenSearch/pull/3634 + // expect(percentileMachineRam).to.eql(metricValue); + expect(percentileMachineRam.slice(0, 5)).to.eql(metricValue.slice(0, 5)); + expect(percentileMachineRam.slice(13, 15)).to.eql(metricValue.slice(13, 15)); }); }); it('should show Percentile Ranks', async function () { - const percentileRankBytes = ['2.036%', 'Percentile rank 99 of "memory"']; + const percentileRankBytes = ['2.029%', 'Percentile rank 99 of "memory"']; log.debug('Aggregation = Percentile Ranks'); await PageObjects.visEditor.selectAggregation('Percentile Ranks', 'metrics'); log.debug('Field = bytes'); diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index 9a19264ba070..cbee197953ef 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -259,7 +259,7 @@ export default function ({ getService, getPageObjects }) { zoomWarningEnabled = await testSubjects.exists('zoomWarningEnabled'); log.debug(`Zoom warning enabled: ${zoomWarningEnabled}`); - const zoomLevel = 9; + const zoomLevel = 13; for (let i = 0; i < zoomLevel; i++) { await PageObjects.tileMap.clickMapZoomIn(); } @@ -276,7 +276,7 @@ export default function ({ getService, getPageObjects }) { } }); - it('should show warning at zoom 10', async () => { + it('should show warning at zoom 14', async () => { await testSubjects.existOrFail('maxZoomWarning'); }); diff --git a/test/functional/apps/wizard/_base.ts b/test/functional/apps/wizard/_base.ts new file mode 100644 index 000000000000..c8940704da5c --- /dev/null +++ b/test/functional/apps/wizard/_base.ts @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'wizard', 'visChart']); + const testSubjects = getService('testSubjects'); + const log = getService('log'); + const retry = getService('retry'); + + describe('Basic tests for wizard app ', function () { + before(async () => { + log.debug('navigateToApp wizard'); + await PageObjects.wizard.navigateToCreateWizard(); + }); + + it('should be able to switch data sources', async () => { + const dataSourceValue = await PageObjects.wizard.selectDataSource( + PageObjects.wizard.index.LOGSTASH_NON_TIME_BASED + ); + + expect(dataSourceValue).to.equal(PageObjects.wizard.index.LOGSTASH_NON_TIME_BASED); + // TODO: Switch with a datasource with unique fields to test if it exists + }); + + it('should show visualization when a field is added', async () => { + const expectedData = [2858, 2904, 2814, 1322, 2784]; + await PageObjects.wizard.addField('metric', 'Count'); + await PageObjects.wizard.addField('segment', 'Terms', 'machine.os.raw'); + + const data = await PageObjects.visChart.getBarChartData(); + expect(data).to.eql(expectedData); + }); + + it('should clear visualization when field is deleted', async () => { + await PageObjects.wizard.removeField('metric', 0); + + const isEmptyWorkspace = await PageObjects.wizard.isEmptyWorkspace(); + expect(isEmptyWorkspace).to.be(true); + }); + + it('should show warning before changing visualization type', async () => { + await PageObjects.wizard.selectVisType('metric', false); + const confirmModalExists = await testSubjects.exists('confirmVisChangeModal'); + expect(confirmModalExists).to.be(true); + + await testSubjects.click('confirmModalCancelButton'); + }); + + it('should change visualization type', async () => { + const pickerValue = await PageObjects.wizard.selectVisType('metric'); + + expect(pickerValue).to.eql('Metric'); + }); + }); +} diff --git a/test/functional/apps/wizard/_experimental_vis.ts b/test/functional/apps/wizard/_experimental_vis.ts new file mode 100644 index 000000000000..e36c0254e22a --- /dev/null +++ b/test/functional/apps/wizard/_experimental_vis.ts @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'wizard']); + const log = getService('log'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + + describe('experimental settings for wizard app ', function () { + it('should show an notification when creating wizard visualization', async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + + // Try to find the wizard Vis type. + const wizardVisTypeExists = await PageObjects.visualize.hasVisType('wizard'); + expect(wizardVisTypeExists).to.be(true); + + // Create a new visualization + await PageObjects.visualize.clickVisType('wizard'); + + // Check that the experimental banner is there and state that this is experimental + const info = await PageObjects.wizard.getExperimentalInfo(); + expect(await info.getVisibleText()).to.contain('experimental'); + }); + + it('should not be available in the picker when disabled', async () => { + log.debug('navigateToApp visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + [VISUALIZE_ENABLE_LABS_SETTING]: false, + }); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + + // Try to find the wizard Vis type. + const wizardVisTypeExists = await PageObjects.visualize.hasVisType('wizard'); + expect(wizardVisTypeExists).to.be(false); + }); + + after(async () => { + // unset the experimental ui setting + await opensearchDashboardsServer.uiSettings.unset(VISUALIZE_ENABLE_LABS_SETTING); + }); + }); +} diff --git a/test/functional/apps/wizard/index.ts b/test/functional/apps/wizard/index.ts new file mode 100644 index 000000000000..24c4eb50c263 --- /dev/null +++ b/test/functional/apps/wizard/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + + describe('wizard app', function () { + this.tags('ciGroup13'); + + before(async function () { + log.debug('Starting wizard before method'); + await browser.setWindowSize(1280, 800); + await opensearchArchiver.loadIfNeeded('logstash_functional'); + await opensearchArchiver.loadIfNeeded('long_window_logstash'); + await opensearchArchiver.loadIfNeeded('visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + }); + + after(async () => { + await opensearchArchiver.unload('logstash_functional'); + await opensearchArchiver.unload('long_window_logstash'); + await opensearchArchiver.unload('visualize'); + }); + + loadTestFile(require.resolve('./_base')); + loadTestFile(require.resolve('./_experimental_vis')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.js index 20d3360f0fe5..bb6be73ebd82 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -48,6 +48,7 @@ export default async function ({ readConfigFile }) { require.resolve('./apps/status_page'), require.resolve('./apps/timeline'), require.resolve('./apps/visualize'), + require.resolve('./apps/wizard'), ], pageObjects, services, @@ -91,6 +92,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/visualize', hash: '/', }, + wizard: { + pathname: '/app/wizard', + hash: '/', + }, dashboard: { pathname: '/app/dashboards', hash: '/list', diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 46f8e60e73db..d09445d47026 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -44,6 +44,7 @@ import { TimePickerProvider } from './time_picker'; import { TimelinePageProvider } from './timeline_page'; import { VisualBuilderPageProvider } from './visual_builder_page'; import { VisualizePageProvider } from './visualize_page'; +import { WizardPageProvider } from './wizard_page'; import { VisualizeEditorPageProvider } from './visualize_editor_page'; import { VisualizeChartPageProvider } from './visualize_chart_page'; import { TileMapPageProvider } from './tile_map_page'; @@ -68,6 +69,7 @@ export const pageObjects = { timePicker: TimePickerProvider, visualBuilder: VisualBuilderPageProvider, visualize: VisualizePageProvider, + wizard: WizardPageProvider, visEditor: VisualizeEditorPageProvider, visChart: VisualizeChartPageProvider, tileMap: TileMapPageProvider, diff --git a/test/functional/page_objects/wizard_page.ts b/test/functional/page_objects/wizard_page.ts new file mode 100644 index 000000000000..bd206f71fa2f --- /dev/null +++ b/test/functional/page_objects/wizard_page.ts @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function WizardPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const comboBox = getService('comboBox'); + const { common, header } = getPageObjects(['common', 'header']); + + /** + * This page object contains the visualization type selection, the landing page, + * and the open/save dialog functions + */ + class WizardPage { + index = { + LOGSTASH_TIME_BASED: 'logstash-*', + LOGSTASH_NON_TIME_BASED: 'logstash*', + }; + + public async navigateToCreateWizard() { + await common.navigateToApp('wizard'); + await header.waitUntilLoadingHasFinished(); + } + + public async getExperimentalInfo() { + return await testSubjects.find('experimentalVisInfo'); + } + + public async findFieldByName(name: string) { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.type(name); + } + + public async getDataSourceSelector() { + const dataSourceDropdown = await testSubjects.find('searchableDropdownValue'); + return await dataSourceDropdown.getVisibleText(); + } + + public async selectDataSource(dataSource: string) { + await testSubjects.click('searchableDropdownValue'); + await find.clickByCssSelector( + `[data-test-subj="searchableDropdownList"] [title="${dataSource}"]` + ); + const dataSourceDropdown = await testSubjects.find('searchableDropdownValue'); + return await dataSourceDropdown.getVisibleText(); + } + + public async selectVisType(type: string, confirm = true) { + const chartPicker = await testSubjects.find('chartPicker'); + await chartPicker.click(); + await testSubjects.click(`visType-${type}`); + + if (confirm) { + await testSubjects.click('confirmModalConfirmButton'); + } + + return chartPicker.getVisibleText(); + } + + public async addField( + dropBoxId: string, + aggValue: string, + fieldValue?: string, + returnToMainPanel = true + ) { + await testSubjects.click(`dropBoxAddField-${dropBoxId} > dropBoxAddBtn`); + await common.sleep(500); + const aggComboBoxElement = await testSubjects.find('defaultEditorAggSelect'); + await comboBox.setElement(aggComboBoxElement, aggValue); + await common.sleep(500); + + if (fieldValue) { + const fieldComboBoxElement = await testSubjects.find('visDefaultEditorField'); + await comboBox.setElement(fieldComboBoxElement, fieldValue); + await common.sleep(500); + } + + if (returnToMainPanel) { + await testSubjects.click('panelCloseBtn'); + await common.sleep(500); + } + } + + public async removeField(dropBoxId: string, aggNth: number) { + await testSubjects.click(`dropBoxField-${dropBoxId}-${aggNth} > dropBoxRemoveBtn`); + await common.sleep(500); + } + + // TODO: Fix. Currently it is not able to locate the dropbox location correctly, even if it identifies the element correctly + public async dragDropField(field: string, dropBoxId: string) { + const fieldEle = await testSubjects.find(`field-${field}-showDetails`); + const dropBoxEle = await testSubjects.find(`dropBoxAddField-${dropBoxId}`); + await browser.dragAndDrop({ location: fieldEle }, { location: dropBoxEle }); + } + + public async clearFieldSearchInput() { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.clearValue(); + } + + public async getMetric() { + const elements = await find.allByCssSelector( + '[data-test-subj="visualizationLoader"] .mtrVis__container' + ); + const values = await Promise.all( + elements.map(async (element) => { + const text = await element.getVisibleText(); + return text; + }) + ); + return values + .filter((item) => item.length > 0) + .reduce((arr: string[], item) => arr.concat(item.split('\n')), []); + } + + public async isEmptyWorkspace() { + const elements = await find.allByCssSelector('[data-test-subj="emptyWorkspace"]'); + return elements.length === 1; + } + } + + return new WizardPage(); +} diff --git a/test/functional/services/global_nav.ts b/test/functional/services/global_nav.ts index 026bfa6059f6..86aeb02e531d 100644 --- a/test/functional/services/global_nav.ts +++ b/test/functional/services/global_nav.ts @@ -39,10 +39,18 @@ export function GlobalNavProvider({ getService }: FtrProviderContext) { await testSubjects.moveMouseTo('headerGlobalNav > logo'); } + public async moveMouseToHomeButton(): Promise<void> { + await testSubjects.moveMouseTo('headerGlobalNav > homeLoader'); + } + public async clickLogo(): Promise<void> { return await testSubjects.click('headerGlobalNav > logo'); } + public async clickHomeButton(): Promise<void> { + return await testSubjects.click('headerGlobalNav > homeLoader'); + } + public async clickNewsfeed(): Promise<void> { return await testSubjects.click('headerGlobalNav > newsfeed'); } @@ -81,6 +89,15 @@ export function GlobalNavProvider({ getService }: FtrProviderContext) { ); expect(actualLabel.toUpperCase()).to.equal(expectedUrl.toUpperCase()); } + + public async homeMarkExistsOrFail(expectedUrl: string): Promise<void> { + await testSubjects.exists('headerGlobalNav > homeLoader > customMark'); + const actualLabel = await testSubjects.getAttribute( + 'headerGlobalNav > homeLoader > customMark', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedUrl.toUpperCase()); + } } return new GlobalNav(); 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 bad2fe7e3a57..80275e41e042 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": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "34.6.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", "@osd/plugin-helpers": "1.0.0", "react": "^16.14.0", "react-dom": "^16.12.0", diff --git a/test/mocha_decorations.d.ts b/test/mocha_decorations.d.ts index f2e9f2a2dcac..025fd6664c3d 100644 --- a/test/mocha_decorations.d.ts +++ b/test/mocha_decorations.d.ts @@ -42,7 +42,8 @@ type Tags = | 'ciGroup9' | 'ciGroup10' | 'ciGroup11' - | 'ciGroup12'; + | 'ciGroup12' + | 'ciGroup13'; // We need to use the namespace here to match the Mocha definition declare module 'mocha' { diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index e2f725c50bc9..e733a4e36368 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -49,6 +49,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/core_plugins'), require.resolve('./test_suites/management'), require.resolve('./test_suites/doc_views'), + require.resolve('./test_suites/doc_views_links'), require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), ], diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/opensearch_dashboards.json b/test/plugin_functional/plugins/doc_views_links_plugin/opensearch_dashboards.json new file mode 100644 index 000000000000..0cda322f6169 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/opensearch_dashboards.json @@ -0,0 +1,8 @@ +{ + "id": "docViewLinksPlugin", + "version": "0.0.1", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": false, + "ui": true, + "requiredPlugins": ["discover"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/package.json b/test/plugin_functional/plugins/doc_views_links_plugin/package.json new file mode 100644 index 000000000000..92cfca078156 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "docViewLinksPlugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/doc_views_links_plugin", + "opensearchDashboards": { + "version": "opensearchDashboards", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "osd": "node ../../../../scripts/osd.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/public/index.ts b/test/plugin_functional/plugins/doc_views_links_plugin/public/index.ts new file mode 100644 index 000000000000..047330d37b3e --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/public/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DocViewsLinksPlugin } from './plugin'; + +export const plugin = () => new DocViewsLinksPlugin(); diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/public/plugin.tsx b/test/plugin_functional/plugins/doc_views_links_plugin/public/plugin.tsx new file mode 100644 index 000000000000..174e46a529a7 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/public/plugin.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Plugin, CoreSetup } from 'opensearch-dashboards/public'; +import { DiscoverSetup } from '../../../../../src/plugins/discover/public'; + +export class DocViewsLinksPlugin implements Plugin<void, void> { + public setup(core: CoreSetup, { discover }: { discover: DiscoverSetup }) { + discover.docViewsLinks.addDocViewLink({ + href: 'http://some-url/', + order: 1, + label: 'href doc view link', + }); + + discover.docViewsLinks.addDocViewLink({ + generateCb: () => ({ + url: 'http://some-url/', + }), + order: 2, + label: 'generateCb doc view link', + }); + + discover.docViewsLinks.addDocViewLink({ + generateCb: () => ({ + url: 'http://some-url/', + hide: true, + }), + order: 3, + label: 'generateCbHidden doc view link', + }); + } + + public start() {} +} diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/tsconfig.json b/test/plugin_functional/plugins/doc_views_links_plugin/tsconfig.json new file mode 100644 index 000000000000..8a481ba6e642 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_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" } + ] +} \ No newline at end of file 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 b9c03d0c28fc..b1847e7d35f5 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": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "34.6.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", "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 4bc985019a88..e1f3698fdd38 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": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "34.6.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", "@osd/plugin-helpers": "1.0.0", "react": "^16.14.0", "typescript": "4.0.2" diff --git a/test/plugin_functional/test_suites/doc_views_links/doc_views_links.ts b/test/plugin_functional/test_suites/doc_views_links/doc_views_links.ts new file mode 100644 index 000000000000..2933e0add118 --- /dev/null +++ b/test/plugin_functional/test_suites/doc_views_links/doc_views_links.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + + describe('custom doc views links', function () { + beforeEach(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await testSubjects.click('docTableExpandToggleColumn'); + }); + + it('should show href and generateCb doc views link', async () => { + const hrefLink = await find.byLinkText('href doc view link'); + const generateCbLink = await find.byLinkText('generateCb doc view link'); + + expect(await hrefLink.isDisplayed()).to.be(true); + expect(await generateCbLink.isDisplayed()).to.be(true); + }); + + it('should not render generateCbHidden doc views link', async () => { + expect(await find.existsByLinkText('generateCbHidden doc view link')).to.eql(false); + }); + + it('should render href doc view link', async () => { + const hrefLink = await find.byLinkText('href doc view link'); + await hrefLink.click(); + expect(await browser.getCurrentUrl()).to.eql('http://some-url/'); + }); + + it('should render generateCb doc view link', async () => { + const generateCbLink = await find.byLinkText('generateCb doc view link'); + await generateCbLink.click(); + expect(await browser.getCurrentUrl()).to.eql('http://some-url/'); + }); + }); +} diff --git a/test/plugin_functional/test_suites/doc_views_links/index.ts b/test/plugin_functional/test_suites/doc_views_links/index.ts new file mode 100644 index 000000000000..b894edb4db0a --- /dev/null +++ b/test/plugin_functional/test_suites/doc_views_links/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) { + const opensearchArchiver = getService('opensearchArchiver'); + + describe('doc views links', function () { + before(async () => { + await opensearchArchiver.loadIfNeeded('../functional/fixtures/opensearch_archiver/discover'); + }); + + loadTestFile(require.resolve('./doc_views_links')); + }); +} diff --git a/test/plugin_functional/test_suites/panel_actions/index.js b/test/plugin_functional/test_suites/panel_actions/index.js index 3fddfd8261e8..a94cfbed1c53 100644 --- a/test/plugin_functional/test_suites/panel_actions/index.js +++ b/test/plugin_functional/test_suites/panel_actions/index.js @@ -30,11 +30,12 @@ import path from 'path'; -export const OPENSEARCH_DASHBOARDS_ARCHIVE_PATH = path.resolve( +const OPENSEARCH_DASHBOARDS_ARCHIVE_PATH = path.resolve( __dirname, '../../../functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards' ); -export const DATA_ARCHIVE_PATH = path.resolve( + +const DATA_ARCHIVE_PATH = path.resolve( __dirname, '../../../functional/fixtures/opensearch_archiver/dashboard/current/data' ); diff --git a/yarn.lock b/yarn.lock index e6d96c107877..6ec85a1424f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1120,10 +1120,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@34.6.0": - version "34.6.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-34.6.0.tgz#a7188bc97d9c3120cd65e52ed423377872b604bd" - integrity sha512-uVMSX0jPJU3LLwD4TRHllyJeTr+Uihh+R5qsFSAzKrCCRZjSfKMmMHKffWhzFyYjG97npdWlMvneXG5q0yobCw== +"@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== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -1149,7 +1149,7 @@ react-is "~16.3.0" react-virtualized-auto-sizer "^1.0.2" react-window "^1.8.5" - refractor "^3.4.0" + refractor "^3.6.0" rehype-raw "^5.0.0" rehype-react "^6.0.0" rehype-stringify "^8.0.0" @@ -1160,7 +1160,7 @@ text-diff "^1.0.1" unified "^9.2.0" unist-util-visit "^2.0.3" - url-parse "^1.5.0" + url-parse "^1.5.9" uuid "^8.3.0" vfile "^4.2.0" @@ -2511,6 +2511,16 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== +"@reduxjs/toolkit@^1.6.1": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.3.tgz#9c6a9c497bde43a67618d37a4175a00ae12efeb2" + integrity sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg== + dependencies: + immer "^9.0.7" + redux "^4.1.2" + redux-thunk "^2.4.1" + reselect "^4.1.5" + "@rushstack/node-core-library@3.45.1": version "3.45.1" resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.45.1.tgz#787361b61a48d616eb4b059641721a3dc138f001" @@ -2568,11 +2578,6 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - "@sindresorhus/is@^4.0.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" @@ -2614,13 +2619,6 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -2687,10 +2685,10 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@tsd/typescript@^4.2.4": - version "4.6.3" - resolved "https://registry.yarnpkg.com/@tsd/typescript/-/typescript-4.6.3.tgz#9b4c8198da7614fe1547436fbd5657cfe8327c1d" - integrity sha512-WjipklCf6qWQL4Hkw+FSwOXMA5JqKv04ro/c1aviYSzLJFdcFMrR/FHjOGBIEAIq7pb8Bw74wd+G45dWfC/Jnw== +"@tsd/typescript@~4.7.3": + version "4.7.4" + resolved "https://registry.yarnpkg.com/@tsd/typescript/-/typescript-4.7.4.tgz#f1e4e6c3099a174a0cb7aa51cf53f34f6494e528" + integrity sha512-jbtC+RgKZ9Kk65zuRZbKLTACf+tvFW4Rfq0JEMXrlmV3P3yme+Hm+pnb5fJRyt61SjIitcrC810wj7+1tgsEmg== "@types/angular-mocks@^1.7.1": version "1.7.1" @@ -4233,11 +4231,6 @@ ajv@^8.0.1, ajv@^8.6.2: require-from-string "^2.0.2" uri-js "^4.2.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - angular-aria@^1.8.0: version "1.8.2" resolved "https://registry.yarnpkg.com/angular-aria/-/angular-aria-1.8.2.tgz#3e5d546e549d8bddcf0b8031c677d3129d82a76d" @@ -4280,13 +4273,6 @@ angular@>=1.0.6, angular@^1.8.2: resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.2.tgz#5983bbb5a9fa63e213cb7749199e0d352de3a2f1" integrity sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw== -ansi-align@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - ansi-colors@3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" @@ -4684,11 +4670,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= - async-retry@^1.2.3: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" @@ -5078,20 +5059,6 @@ bottleneck@^2.15.3: resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== -boxen@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" - integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^3.0.0" - cli-boxes "^2.2.0" - string-width "^4.1.0" - term-size "^2.1.0" - type-fest "^0.8.1" - widest-line "^3.1.0" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5388,19 +5355,6 @@ cacheable-lookup@^5.0.3: resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - cacheable-request@^7.0.1, cacheable-request@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" @@ -5467,9 +5421,9 @@ camelize@^1.0.0: integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= caniuse-lite@^1.0.30001317: - version "1.0.30001322" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz#2e4c09d11e1e8f852767dab287069a8d0c29d623" - integrity sha512-neRmrmIrCGuMnxGSoh+x7zYtQFFgnSY2jaomjU56sCkTA6JINqQrxutF459JpWcWRajvoyn95sOXq4Pqrnyjew== + version "1.0.30001397" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz" + integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA== caseless@~0.12.0: version "0.12.0" @@ -5633,6 +5587,21 @@ chokidar@3.3.0: optionalDependencies: fsevents "~2.1.1" +"chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^2.1.2, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -5652,21 +5621,6 @@ chokidar@^2.1.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -5700,11 +5654,6 @@ chromedriver@^100.0.0: proxy-from-env "^1.1.0" tcp-port-used "^1.0.1" -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - ci-info@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" @@ -5751,11 +5700,6 @@ clean-webpack-plugin@^3.0.0: "@types/webpack" "^4.4.31" del "^4.1.1" -cli-boxes@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - cli-cursor@^2.0.0, cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -6075,18 +6019,6 @@ configstore@^1.0.0: write-file-atomic "^1.1.2" xdg-basedir "^2.0.0" -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -6354,11 +6286,6 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - cson-parser@^4.0.5: version "4.0.9" resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-4.0.9.tgz#eef0cf77edd057f97861ef800300c8239224eedb" @@ -6857,13 +6784,6 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -6943,11 +6863,6 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - defer-to-connect@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" @@ -7110,6 +7025,11 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-newline@2.X: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -7316,13 +7236,6 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - dotignore@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -7330,11 +7243,6 @@ dotignore@^0.1.2: dependencies: minimatch "^3.0.4" -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - duplexer@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -7776,11 +7684,6 @@ escalade@^3.0.2, escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -7815,7 +7718,7 @@ eslint-config-prettier@^6.11.0: dependencies: get-stdin "^6.0.0" -eslint-formatter-pretty@^4.0.0: +eslint-formatter-pretty@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz#7a6877c14ffe2672066c853587d89603e97c7708" integrity sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ== @@ -8508,6 +8411,13 @@ fetch-mock@^7.3.9: path-to-regexp "^2.2.1" whatwg-url "^6.5.0" +fibers@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/fibers/-/fibers-5.0.3.tgz#2fd03acb255db66fe693d15beafbf5ae92193fd7" + integrity sha512-/qYTSoZydQkM21qZpGLDLuCq8c+B8KhuCQ1kLPvnRNhxhVbvrpmH9l2+Lblf5neDuEsY4bfT7LeO553TXQDvJw== + dependencies: + detect-libc "^1.0.3" + figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -8970,13 +8880,6 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gaze@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - geckodriver@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.0.1.tgz#ded3512f3c6ddc490139b9d5e8fd6925d41c5631" @@ -9017,11 +8920,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -9032,7 +8930,7 @@ get-stdin@^8.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^4.0.0, get-stream@^4.1.0: +get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -9149,7 +9047,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.7, glob@~7.1.1, glob@~7.1.6: +glob@7.1.7, glob@~7.1.6: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -9184,13 +9082,6 @@ glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" - integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== - dependencies: - ini "1.3.7" - global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -9316,15 +9207,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= -globule@^1.0.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.3.tgz#811919eeac1ab7344e905f2e3be80a13447973c2" - integrity sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - got@11.8.2: version "11.8.2" resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" @@ -9375,23 +9257,6 @@ got@^3.2.0: read-all-stream "^3.0.0" timed-out "^2.0.0" -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" @@ -9685,11 +9550,6 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -10130,6 +9990,11 @@ immer@^9.0.6: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== +immer@^9.0.7: + version "9.0.15" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" + integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -10138,11 +10003,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - import-lazy@^4.0.0, import-lazy@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" @@ -10204,11 +10064,6 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" - integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== - ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -10450,13 +10305,6 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-core-module@^2.1.0, is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.0, is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" @@ -10576,14 +10424,6 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== -is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - is-integer@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -10629,11 +10469,6 @@ is-npm@^1.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= -is-npm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" - integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== - is-number-object@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" @@ -10653,11 +10488,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - is-observable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" @@ -10878,11 +10708,6 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - is2@^2.0.6: version "2.0.7" resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.7.tgz#d084e10cab3bd45d6c9dfde7a48599fcbb93fcac" @@ -11495,11 +11320,6 @@ jquery@^3.5.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== -js-base64@^2.1.8: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -11593,11 +11413,6 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -11755,13 +11570,6 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - keyv@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.1.1.tgz#02c538bfdbd2a9308cc932d4096f05ae42bfa06a" @@ -11808,13 +11616,6 @@ latest-version@^1.0.0: dependencies: package-json "^1.0.0" -latest-version@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -11839,10 +11640,10 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -leaflet-vega@^0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/leaflet-vega/-/leaflet-vega-0.8.6.tgz#dd4090a6123cb983c2b732d53ec9e4daa53736b2" - integrity sha1-3UCQphI8uYPCtzLVPsnk2qU3NrI= +"leaflet-vega@npm:@amoo-miki/leaflet-vega@0.8.7": + version "0.8.7" + resolved "https://registry.yarnpkg.com/@amoo-miki/leaflet-vega/-/leaflet-vega-0.8.7.tgz#8faca1b4b8e2ef7d48667ac6faad9204f4da7153" + integrity sha512-T4M5yziwj3Fi9Adsbce+cdWqPjON0BRwEjwqLlPMoirU1vhifA6YKrlZkVzJrK0IIm+hdfMCLkBz33gD8fdxzQ== dependencies: vega-spec-injector "^0.0.2" @@ -12263,7 +12064,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.21, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.21: +lodash@4.17.21, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12338,7 +12139,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0 || ^4.0.0" -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: +lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== @@ -12698,7 +12499,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0, mimic-response@^1.0.1: +mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -12761,7 +12562,7 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@~3.0.2, minimatch@~3.0.4: +minimatch@~3.0.4: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== @@ -12912,16 +12713,16 @@ module-details-from-path@^1.0.3: integrity sha1-EUyUlnPiqKNenTV4hSeqN7Z52is= moment-timezone@*, moment-timezone@^0.5.27: - version "0.5.34" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" - integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + version "0.5.37" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.37.tgz#adf97f719c4e458fdb12e2b4e87b8bec9f4eef1e" + integrity sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg== dependencies: moment ">= 2.9.0" "moment@>= 2.9.0", moment@^2.10.6, moment@^2.24.0: - version "2.29.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" - integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== monaco-editor@~0.17.0: version "0.17.1" @@ -13038,7 +12839,7 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.12.1, nan@^2.13.2, nan@^2.14.1, nan@^2.14.2: +nan@^2.12.1, nan@^2.14.1, nan@^2.14.2: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== @@ -13206,7 +13007,7 @@ node-gyp-build@^4.2.3: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-gyp@^7.0.0, node-gyp@^7.1.0: +node-gyp@^7.0.0: version "7.1.2" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== @@ -13283,27 +13084,6 @@ node-releases@^2.0.2: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== -node-sass@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe" - integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^7.0.3" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - lodash "^4.17.15" - meow "^9.0.0" - nan "^2.13.2" - node-gyp "^7.1.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "2.2.5" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - nopt@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.2.1.tgz#2aa09b7d1768487b3b89a9c5aa52335bff0baea7" @@ -13375,11 +13155,6 @@ normalize-selector@^0.2.0: resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM= -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - normalize-url@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" @@ -13411,7 +13186,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^4.0.0, npmlog@^4.1.2: +npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -13755,11 +13530,6 @@ p-all@^2.1.0: dependencies: p-map "^2.0.0" -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - p-cancelable@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" @@ -13880,16 +13650,6 @@ package-json@^1.0.0: got "^3.2.0" registry-url "^3.0.0" -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - pako@^1.0.5, pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -14374,11 +14134,6 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -14562,17 +14317,10 @@ punycode@^1.2.4, punycode@^1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= -pupa@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" - integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== - dependencies: - escape-goat "^2.0.0" - qs@6.9.3, qs@^6.10.1, qs@^6.10.3, qs@~6.5.2: - version "6.10.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" @@ -14696,7 +14444,7 @@ raw-loader@^4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" -rc@^1.0.1, rc@^1.2.8: +rc@^1.0.1: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -15193,7 +14941,7 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux-thunk@^2.3.0: +redux-thunk@^2.3.0, redux-thunk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== @@ -15205,12 +14953,19 @@ redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" +redux@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= -refractor@^3.4.0: +refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== @@ -15301,13 +15056,6 @@ regexpu-core@^5.0.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.0.0" -registry-auth-token@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" - integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== - dependencies: - rc "^1.2.8" - registry-url@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" @@ -15315,13 +15063,6 @@ registry-url@^3.0.0: dependencies: rc "^1.0.1" -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - regjsgen@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" @@ -15483,7 +15224,7 @@ replace-ext@^1.0.0: resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== -request@^2.88.0, request@^2.88.2: +request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -15558,6 +15299,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== +reselect@^4.1.5: + version "4.1.6" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" + integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== + resize-observer-polyfill@^1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b" @@ -15654,13 +15400,6 @@ resolve@~1.19.0: is-core-module "^2.1.0" path-parse "^1.0.6" -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - responselike@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" @@ -15825,16 +15564,6 @@ safefs@^6.12.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-graph@2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" - integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^13.3.2" - sass-loader@^10.2.0: version "10.2.1" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.1.tgz#17e51df313f1a7a203889ce8ff91be362651276e" @@ -15846,6 +15575,13 @@ sass-loader@^10.2.0: schema-utils "^3.0.0" semver "^7.3.2" +sass@~1.26.11: + version "1.26.12" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.12.tgz#79eddaa1773fff32ccf19e00d1ce380fc2afc7d0" + integrity sha512-hmSwtBOWoS9zwe0yAS+QmaseVCUELiGV22gXHDR7+9stEsVuEuxfY1GhC8XmUpC+Ir3Hwq7NxSUNbnmkznnF7g== + dependencies: + chokidar ">=2.0.0 <4.0.0" + sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -15906,14 +15642,6 @@ screenfull@^5.0.0: resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - secure-json-parse@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85" @@ -15935,13 +15663,6 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - "semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -15957,7 +15678,7 @@ semver@7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -16273,13 +15994,6 @@ source-map@0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -16546,13 +16260,6 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -stdout-stream@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" - integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== - dependencies: - readable-stream "^2.0.1" - stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -16652,7 +16359,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"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== @@ -17180,11 +16887,6 @@ tempy@^0.3.0: type-fest "^0.3.1" unique-string "^1.0.0" -term-size@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" - integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== - terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -17224,9 +16926,9 @@ terser-webpack-plugin@^2.1.2: webpack-sources "^1.4.3" terser@^4.1.2, terser@^4.6.12: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -17400,11 +17102,6 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -17548,13 +17245,6 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -"true-case-path@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" - integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== - dependencies: - glob "^7.1.2" - ts-debounce@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ts-debounce/-/ts-debounce-3.0.0.tgz#9beedf59c04de3b5bef8ff28bd6885624df357be" @@ -17575,18 +17265,17 @@ tsconfig-paths@^3.12.0: minimist "^1.2.6" strip-bom "^3.0.0" -tsd@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.16.0.tgz#378db004d97000433eaf4f5af1e114fdd4108350" - integrity sha512-VrdSjmG7vKTYu5bPgZ7554iphNA1DgTgCq7xqoCCgDHTL6XC/636vF8IbDo/Q66q4oFDB/xtL4/YIhCDoExR4g== +tsd@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.21.0.tgz#fb1a1aa022cfb4a6d1065bb569687e8ac8f72daf" + integrity sha512-6DugCw1Q4H8HYwDT3itzgALjeDxN4RO3iqu7gRdC/YNVSCRSGXRGQRRasftL1uKDuKxlFffYKHv5j5G7YnKGxQ== dependencies: - "@tsd/typescript" "^4.2.4" - eslint-formatter-pretty "^4.0.0" + "@tsd/typescript" "~4.7.3" + eslint-formatter-pretty "^4.1.0" globby "^11.0.1" meow "^9.0.0" path-exists "^4.0.0" read-pkg-up "^7.0.0" - update-notifier "^4.1.0" tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" @@ -17861,13 +17550,6 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - unist-builder@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" @@ -18026,25 +17708,6 @@ update-notifier@^0.5.0: semver-diff "^2.0.0" string-length "^1.0.0" -update-notifier@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" - integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== - dependencies: - boxen "^4.2.0" - chalk "^3.0.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.3.1" - is-npm "^4.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.0.0" - pupa "^2.0.1" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -18066,14 +17729,7 @@ url-loader@^2.2.0: mime "^2.4.4" schema-utils "^2.5.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-parse@^1.5.0: +url-parse@^1.5.9: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -18344,6 +18000,11 @@ vega-hierarchy@~4.1.0: vega-dataflow "^5.7.3" vega-util "^1.15.2" +"vega-interpreter@npm:@amoo-miki/vega-forced-csp-compliant-interpreter@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@amoo-miki/vega-forced-csp-compliant-interpreter/-/vega-forced-csp-compliant-interpreter-1.0.5.tgz#49970be9b00ca7e45ced0617fbf373c77a28aab4" + integrity sha512-lfeU77lVoUbSCC6N1ywdKg+I6K08xpkd82TLon+LebtKyC8aLCe7P5Dd/89zAPyFwRyobKftHu8z0xpV7R7a4Q== + vega-label@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.2.0.tgz#bcb2659aec54f890f9debab3e41ab87a58292dce" @@ -18985,13 +18646,6 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2 || 3 || 4" -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -19147,11 +18801,6 @@ xdg-basedir@^2.0.0: dependencies: os-homedir "^1.0.0" -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - xhr@^2.0.1: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" @@ -19260,7 +18909,7 @@ yargs-unparser@1.6.0: lodash "^4.17.15" yargs "^13.3.0" -yargs@13.3.2, yargs@^13.3.0, yargs@^13.3.2: +yargs@13.3.2, yargs@^13.3.0: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==