diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index b40c59eb293d..621b5e6033ac 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -152,9 +152,8 @@ jobs: npm uninstall -g yarn npm i -g yarn@1.22.10 - # image has the latest chrome v99 - name: Setup chromedriver - run: yarn add --dev chromedriver@99.0.0 + run: node scripts/upgrade_chromedriver.js - name: Run bootstrap run: yarn osd bootstrap @@ -204,9 +203,8 @@ jobs: npm uninstall -g yarn npm i -g yarn@1.22.10 - # image has the latest chrome v99 - name: Setup chromedriver - run: yarn add --dev chromedriver@106.0.1 + run: node scripts/upgrade_chromedriver.js - name: Run bootstrap run: yarn osd bootstrap diff --git a/.gitignore b/.gitignore index 01c2aaeaf9ef..3f1759e6665a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .ackrc /.opensearch /.chromium +/package.json.bak .DS_Store .node_binaries .native_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 728f558a12ef..243b276aa78a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Vis Builder] Add field summary popovers ([#2682](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2682)) - [I18n] Register ru, ru-RU locale ([#2817](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2817)) - Add yarn opensearch arg to setup plugin dependencies ([#2544](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2544)) +- [Multi DataSource] Test the connection to an external data source when creating or updating ([#2973](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2973)) ### 🐛 Bug Fixes @@ -70,7 +71,9 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Vis Builder] Fix empty workspace animation does not work in firefox ([#2853](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2853)) - Bumped `del` version to fix MacOS race condition ([#2847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2873)) - [Build] Fixed "Last Access Time" not being set by `scanCopy` on Windows ([#2964](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2964)) +- [Vis Builder] Add global data persistence for vis builder #2896 ([#2896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2896)) - Update `leaflet-vega` and fix its usage ([#3005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3005)) +- [Table Visualization][BUG] Fix Url content display issue in table ([#2918](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2918)) ### 🚞 Infrastructure @@ -78,6 +81,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Update backport custom branch name to utilize head template ([#2766](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2766)) - Re-enable CI workflows for feature branckes ([#2908](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2908)) - Add Windows CI workflows ([#2966](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2966)) +- Add automatic selection of the appropriate version of chrome driver to run functional tests ([#2990](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2990)) + ### 📝 Documentation @@ -86,10 +91,12 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [MD] Tweak multiple data source design doc [#2724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2724) - Corrected README and help command of osd-plugin-helpers ([#2810](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2810)) - Add `current-usage.md` and more details to `README.md` of `charts` plugin ([#2695](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2695)) +- [Doc] Add readme for global query persistence ([#3001](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3001)) ### 🛠 Maintenance - Adding @zhongnansu as maintainer. ([#2590](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2590)) +- Removes `minimatch` manual resolution ([#3019](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3019)) ### 🪛 Refactoring @@ -103,6 +110,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [BWC Tests] Add BWC tests for 2.5.0 ([#2890](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2890)) - Fix incorrect validation of time values in JUnit Reporter ([#2965](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2965)) - Make tests covering plugin installation on cluster snapshots work across platforms ([#2994](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2994)) +- Correct the linting logic for `no-restricted-path` to ignore trailing slashes ([#3020](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3020)) +- [Tests] Bumps `chromedriver` to v107 ([#3017](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3017)) ## [2.x] diff --git a/Dockerfile b/Dockerfile index d5fddcd2c6cf..10639bbbe16f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN apt-get update && \ # Specify the version of Chrome that matches the version of chromedriver in the package.json. # A list of Chrome versions can be found here: # https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable -ARG CHROME_VERSION=100.0.4896.127-1 +ARG CHROME_VERSION=107.0.5304.121-1 RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ && wget -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \ && apt-get update \ diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3c01b79dd913..0f62b9a9c3e6 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -15,6 +15,7 @@ | Yan Zeng | [zengyan-amazon](https://github.com/zengyan-amazon) | Amazon | | Kristen Tian | [kristenTian](https://github.com/kristenTian) | Amazon | | Zhongnan Su | [zhongnansu](https://github.com/zhongnansu) | Amazon | +| Manasvini B Suryanarayana | [manasvinibs](https://github.com/manasvinibs) | Amazon | ## Emeritus diff --git a/package.json b/package.json index 2b997a44a12b..e51bf2229c9e 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,6 @@ "**/@types/node": "^14.17.32", "**/ansi-regex": "^5.0.1", "**/async": "^3.2.3", - "**/axios": "^0.27.2", "**/d3-color": "^3.1.0", "**/glob-parent": "^6.0.0", "**/hoist-non-react-statics": "^3.3.2", @@ -93,8 +92,7 @@ "**/qs": "^6.10.3", "**/trim": "^0.0.3", "**/typescript": "4.0.2", - "**/unset-value": "^2.0.1", - "**/minimatch": "^3.0.5" + "**/unset-value": "^2.0.1" }, "workspaces": { "packages": [ @@ -260,12 +258,12 @@ "@types/bluebird": "^3.1.1", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.31", - "@types/chromedriver": "^81.0.0", "@types/color": "^3.0.0", "@types/d3": "^3.5.43", "@types/dedent": "^0.7.0", "@types/deep-freeze-strict": "^1.1.0", "@types/delete-empty": "^2.0.0", + "@types/dompurify": "^2.3.3", "@types/elasticsearch": "^5.0.33", "@types/enzyme": "^3.10.7", "@types/eslint": "^6.1.3", @@ -347,13 +345,14 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^100.0.0", + "chromedriver": "^107.0.3", "classnames": "2.3.1", "compare-versions": "3.5.1", "d3": "3.5.17", "d3-cloud": "1.2.5", "dedent": "^0.7.0", "delete-empty": "^2.0.0", + "dompurify": "^2.4.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", "enzyme-to-json": "^3.5.0", diff --git a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js index ecd14f7317aa..52b1b27ba360 100644 --- a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js +++ b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.js @@ -58,7 +58,7 @@ function traverseToTopFolder(src, pattern) { const srcIdx = src.lastIndexOf(path.sep); src = src.slice(0, srcIdx); } - return src.replace(/\\/g, '/'); + return src.replace(/\\/g, '/').replace(/\/$/, ''); } function isSameFolderOrDescendent(src, imported, pattern) { diff --git a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js index 06e7a9c39414..96251e8fe55c 100644 --- a/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js +++ b/packages/osd-eslint-plugin-eslint/rules/no_restricted_paths.test.js @@ -97,6 +97,22 @@ ruleTester.run('@osd/eslint/no-restricted-paths', rule, { }, ], }, + { + code: 'import b from "testfiles/no_restricted_paths/server/deep/deeper/e.js"', + filename: path.join(__dirname, 'testfiles/no_restricted_paths/server/deep/d.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'testfiles/**/server/**/*', + from: 'testfiles/**/server/**/*', + allowSameFolder: true, + }, + ], + }, + ], + }, // irrelevant function calls { diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.7.md b/release-notes/opensearch-dashboards.release-notes-1.3.7.md new file mode 100644 index 000000000000..eb0e69f6c90a --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.7.md @@ -0,0 +1,49 @@ +# Version 1.3.7 Release Notes + +### 📈 Features/Enhancements + +* [Windows] Facilitate building and running OSD and plugins on Windows platforms ([#2601](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2601)) +* [Windows] Add helper functions to work around the differences of platforms ([#2681](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2681)) +* [Windows] Add `@osd/cross-platform` package to standardize path handling across platforms ([#2703](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2703)) + +### 🛡 Security + +* [CVE-2022-0144] Bump shelljs from 0.8.4 to 0.8.5 ([#2511](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2511)) +* [Legacy Maps Plugin] Prevent reverse-tabnabbing ([#2540](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2540)) +* [CVE-2022-3517] Bump minimatch from 3.0.4 to 3.0.5 and [IBM X-Force ID: 220063] unset-value from 1.0.1 to 2.0.1 ([#2640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2640)) +* [CVE-2022-0155] Bump follow-redirects to 1.15.2 ([#2653](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2653)) +* [CVE-2022-0536] Bump follow-redirects to 1.15.2 ([#2653](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2653)) +* [CVE-2021-24033] Remove storybook package ([#2660](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2660)) +* [CVE-2021-42740] Remove storybook package ([#2660](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2660)) +* [CVE-2022-23647] Bump prismjs to 1.29.0 ([#2668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2668)) +* [CVE-2022-37599] Bump loader-utils to 2.0.4 ([#2995](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2995)) +* [CVE-2022-37603] Bump loader-utils to 2.0.4 ([#2995](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2995)) + +### 📝 Documentation + +* Add the release runbook to RELEASING.md ([#2533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2533)) +* Security-CVEs fixes guidelines [#2674](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2674) +* Correct README and help command of osd-plugin-helpers ([#2810](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2810)) + +### 🐛 Bug Fixes + +* [Chore] Visualize link fix [#2395](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2395) +* [BUG] Fix suggestion list cutoff issue ([#2607](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2607)) +* Remove Leftover X Pack references ([#2638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2638)) +* Bump `del` version to fix MacOS race condition ([#2847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2873)) +* Temporary workaround for task-kill exceptions on Windows when it is passed a pid for a process that is already dead ([#2842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2842)) +* [Build] Fixed "Last Access Time" not being set by `scanCopy` on Windows ([#2964](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2964)) +* Update `leaflet-vega` and fix its usage ([#3005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3005)) + +### 🚞 Infrastructure + +* Update backport custom branch name to utilize head template ([#2766](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2766)) + +### 🛠 Maintenance + +* Increment version to 1.3.7 [#2528](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2528) + +### 🔩 Tests + +* Bump `chromedriver` to 106 to fix function test fail issue [#2514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2514) +* Fix incorrect validation of time values in JUnit Reporter ([#2965](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2965)) diff --git a/release-notes/opensearch-dashboards.release-notes-2.4.1.md b/release-notes/opensearch-dashboards.release-notes-2.4.1.md new file mode 100644 index 000000000000..84e3a787e6de --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.4.1.md @@ -0,0 +1,9 @@ +## Version 2.4.1 Release Notes + +### 🐛 Bug Fixes + +* Update `leaflet-vega` and fix its usage ([#3005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3005)) + +### 🔩 Tests + +* Correct the linting logic for `no-restricted-path` to ignore trailing slashes ([#3020](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3020)) diff --git a/scripts/upgrade_chromedriver.js b/scripts/upgrade_chromedriver.js new file mode 100644 index 000000000000..bb5200c30eec --- /dev/null +++ b/scripts/upgrade_chromedriver.js @@ -0,0 +1,122 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Upgrades the chromedriver dev-dependency to the one supported by the version of Google Chrome + * installed on the machine. + * + * Usage: node scripts/upgrade_chromedriver.js [--install] + */ + +/* eslint no-restricted-syntax: 0 */ +const { execSync, spawnSync } = require('child_process'); +const { createReadStream, createWriteStream, unlinkSync, renameSync, existsSync } = require('fs'); +const { createInterface } = require('readline'); + +if (!process.argv.includes(__filename)) { + console.error('Usage: node scripts/upgrade_chromedriver.js [--install]'); + process.exit(1); +} + +const versionCheckCommands = []; + +switch (process.platform) { + case 'win32': + versionCheckCommands.push( + 'powershell "(Get-Item \\"$Env:Programfiles/Google/Chrome/Application/chrome.exe\\").VersionInfo.FileVersion"' + ); + break; + + case 'darwin': + versionCheckCommands.push( + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version' + ); + break; + + default: + versionCheckCommands.push( + ...[ + '/usr/bin', + '/usr/local/bin', + '/usr/sbin', + '/usr/local/sbin', + '/opt/bin', + '/usr/bin/X11', + '/usr/X11R6/bin', + ].flatMap((loc) => + [ + 'google-chrome --version', + 'google-chrome-stable --version', + 'chromium --version', + 'chromium-browser --version', + ].map((cmd) => `${loc}/${cmd}`) + ) + ); +} + +let versionCheckOutput; +versionCheckCommands.some((cmd) => { + try { + console.log(cmd); + versionCheckOutput = execSync(cmd, { encoding: 'utf8' })?.trim?.(); + return true; + } catch (e) { + console.log('Failed to get version using', cmd); + } +}); + +// Versions 90+ +const majorVersion = versionCheckOutput?.match?.(/(?:^|\s)(9\d|\d{3})\./)?.[1]; + +if (majorVersion) { + if (process.argv.includes('--install')) { + console.log(`Installing chromedriver@^${majorVersion}`); + + spawnSync(`yarn add --dev chromedriver@^${majorVersion}`, { + stdio: 'inherit', + cwd: process.cwd(), + shell: true, + }); + } else { + console.log(`Upgrading to chromedriver@^${majorVersion}`); + + let upgraded = false; + const writeStream = createWriteStream('package.json.upgrading-chromedriver', { flags: 'w' }); + const rl = createInterface({ + input: createReadStream('package.json'), + crlfDelay: Infinity, + }); + rl.on('line', (line) => { + if (line.includes('"chromedriver": "')) { + line = line.replace( + /"chromedriver":\s*"[~^]?\d[\d.]*\d"/, + `"chromedriver": "^${majorVersion}"` + ); + upgraded = true; + } + writeStream.write(line + '\n', 'utf8'); + }); + rl.on('close', () => { + writeStream.end(); + if (upgraded) { + // Remove any previous backups + if (existsSync('package.json.bak')) unlinkSync('package.json.bak'); + + renameSync('package.json', 'package.json.bak'); + renameSync('package.json.upgrading-chromedriver', 'package.json'); + + console.log(`Backed up package.json and updated chromedriver to ${majorVersion}`); + } else { + unlinkSync('package.json.upgrading-chromedriver'); + console.error( + `Failed to update chromedriver to ${majorVersion}. Try adding the \`--install\` switch.` + ); + } + }); + } +} else { + console.debug(versionCheckOutput); + console.error(`Failed to extract the version of the installed Google Chrome.`); +} diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index afcf3d662fed..29b4e3c3128f 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -6,6 +6,7 @@ import { SavedObjectAttributes } from 'src/core/types'; export interface DataSourceAttributes extends SavedObjectAttributes { + id?: string; title: string; description?: string; endpoint: string; diff --git a/src/plugins/data_source/server/client/client_pool.ts b/src/plugins/data_source/server/client/client_pool.ts index fe5458d8f6ca..f492d6bc2898 100644 --- a/src/plugins/data_source/server/client/client_pool.ts +++ b/src/plugins/data_source/server/client/client_pool.ts @@ -29,7 +29,7 @@ export class OpenSearchClientPool { constructor(private logger: Logger) {} - public async setup(config: DataSourcePluginConfigType): Promise { + public setup(config: DataSourcePluginConfigType): OpenSearchClientPoolSetup { const logger = this.logger; const { size } = config.clientPool; diff --git a/src/plugins/data_source/server/client/configure_client.ts b/src/plugins/data_source/server/client/configure_client.ts index c4d1b4eef9f7..3f8a64f71d86 100644 --- a/src/plugins/data_source/server/client/configure_client.ts +++ b/src/plugins/data_source/server/client/configure_client.ts @@ -13,7 +13,7 @@ import { } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; import { CryptographyServiceSetup } from '../cryptography_service'; -import { createDataSourceError, DataSourceError } from '../lib/error'; +import { createDataSourceError } from '../lib/error'; import { DataSourceClientParams } from '../types'; import { parseClientOptions } from './client_config'; import { OpenSearchClientPoolSetup } from './client_pool'; @@ -25,8 +25,8 @@ export const configureClient = async ( logger: Logger ): Promise => { try { - const dataSource = await getDataSource(dataSourceId, savedObjects); - const rootClient = getRootClient(dataSource.attributes, config, openSearchClientPoolSetup); + const { attributes: dataSource } = await getDataSource(dataSourceId, savedObjects); + const rootClient = getRootClient(dataSource, config, openSearchClientPoolSetup); return await getQueryClient(rootClient, dataSource, cryptography); } catch (error: any) { @@ -37,6 +37,43 @@ export const configureClient = async ( } }; +export const configureTestClient = async ( + { savedObjects, cryptography }: DataSourceClientParams, + dataSource: DataSourceAttributes, + openSearchClientPoolSetup: OpenSearchClientPoolSetup, + config: DataSourcePluginConfigType, + logger: Logger +): Promise => { + try { + const { + id, + auth: { type, credentials }, + } = dataSource; + let requireDecryption = false; + + const rootClient = getRootClient(dataSource, config, openSearchClientPoolSetup); + + if (type === AuthType.UsernamePasswordType && !credentials?.password && id) { + const { attributes: fetchedDataSource } = await getDataSource(id || '', savedObjects); + dataSource.auth = { + type, + credentials: { + username: credentials?.username || '', + password: fetchedDataSource.auth.credentials?.password || '', + }, + }; + requireDecryption = true; + } + + return getQueryClient(rootClient, dataSource, cryptography, requireDecryption); + } catch (error: any) { + logger.error(`Failed to get data source client for dataSource: ${dataSource}`); + logger.error(error); + // Re-throw as DataSourceError + throw createDataSourceError(error); + } +}; + export const getDataSource = async ( dataSourceId: string, savedObjects: SavedObjectsClientContract @@ -45,16 +82,17 @@ export const getDataSource = async ( DATA_SOURCE_SAVED_OBJECT_TYPE, dataSourceId ); + return dataSource; }; export const getCredential = async ( - dataSource: SavedObject, + dataSource: DataSourceAttributes, cryptography: CryptographyServiceSetup ): Promise => { - const { endpoint } = dataSource.attributes!; + const { endpoint } = dataSource; - const { username, password } = dataSource.attributes.auth.credentials!; + const { username, password } = dataSource.auth.credentials!; const { decryptedText, encryptionContext } = await cryptography .decodeAndDecrypt(password) @@ -87,17 +125,20 @@ export const getCredential = async ( */ const getQueryClient = async ( rootClient: Client, - dataSource: SavedObject, - cryptography: CryptographyServiceSetup + dataSource: DataSourceAttributes, + cryptography?: CryptographyServiceSetup, + requireDecryption: boolean = true ): Promise => { - const authType = dataSource.attributes.auth.type; + const authType = dataSource.auth.type; switch (authType) { case AuthType.NoAuth: return rootClient.child(); case AuthType.UsernamePasswordType: - const credential = await getCredential(dataSource, cryptography); + const credential = requireDecryption + ? await getCredential(dataSource, cryptography!) + : (dataSource.auth.credentials as UsernamePasswordTypedContent); return getBasicAuthClient(rootClient, credential); default: diff --git a/src/plugins/data_source/server/client/index.ts b/src/plugins/data_source/server/client/index.ts index f27848965077..faf5dabe4417 100644 --- a/src/plugins/data_source/server/client/index.ts +++ b/src/plugins/data_source/server/client/index.ts @@ -4,4 +4,10 @@ */ export { OpenSearchClientPool, OpenSearchClientPoolSetup } from './client_pool'; -export { configureClient, getDataSource, getCredential } from './configure_client'; +export { + configureClient, + getDataSource, + getCredential, + getRootClient, + getValidationClient, +} from './configure_client'; diff --git a/src/plugins/data_source/server/data_source_service.ts b/src/plugins/data_source/server/data_source_service.ts index 8466bb7e914b..f841c2ec3067 100644 --- a/src/plugins/data_source/server/data_source_service.ts +++ b/src/plugins/data_source/server/data_source_service.ts @@ -3,16 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - Auditor, - LegacyCallAPIOptions, - Logger, - OpenSearchClient, -} from '../../../../src/core/server'; +import { LegacyCallAPIOptions, Logger, OpenSearchClient } from '../../../../src/core/server'; import { DataSourcePluginConfigType } from '../config'; import { configureClient, OpenSearchClientPool } from './client'; import { configureLegacyClient } from './legacy'; import { DataSourceClientParams } from './types'; +import { DataSourceAttributes } from '../common/data_sources'; +import { configureTestClient } from './client/configure_client'; export interface DataSourceServiceSetup { getDataSourceClient: (params: DataSourceClientParams) => Promise; @@ -25,6 +22,11 @@ export interface DataSourceServiceSetup { options?: LegacyCallAPIOptions ) => Promise; }; + + getTestingClient: ( + params: DataSourceClientParams, + dataSource: DataSourceAttributes + ) => Promise; } export class DataSourceService { private readonly openSearchClientPool: OpenSearchClientPool; @@ -47,6 +49,19 @@ export class DataSourceService { return configureClient(params, opensearchClientPoolSetup, config, this.logger); }; + const getTestingClient = ( + params: DataSourceClientParams, + dataSource: DataSourceAttributes + ): Promise => { + return configureTestClient( + params, + dataSource, + opensearchClientPoolSetup, + config, + this.logger + ); + }; + const getDataSourceLegacyClient = (params: DataSourceClientParams) => { return { callAPI: ( @@ -64,7 +79,7 @@ export class DataSourceService { }; }; - return { getDataSourceClient, getDataSourceLegacyClient }; + return { getDataSourceClient, getDataSourceLegacyClient, getTestingClient }; } start() {} diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts index bfdf0ce585f0..f5a192a1cae5 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts @@ -19,7 +19,7 @@ import { configureLegacyClient } from './configure_legacy_client'; const DATA_SOURCE_ID = 'a54b76ec86771ee865a0f74a305dfff8'; // TODO: improve UT -describe('configureLegacyClient', () => { +describe.skip('configureLegacyClient', () => { let logger: ReturnType; let config: DataSourcePluginConfigType; let savedObjectsMock: jest.Mocked; diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index 069bf1d0f457..e038a0f7685e 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -29,6 +29,8 @@ import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../common'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { ensureRawRequest } from '../../../../src/core/server/http/router'; import { createDataSourceError } from './lib/error'; +import { registerTestConnectionRoute } from './routes/test_connection'; + export class DataSourcePlugin implements Plugin { private readonly logger: Logger; private readonly cryptographyService: CryptographyService; @@ -103,6 +105,9 @@ export class DataSourcePlugin implements Plugin createDataSourceError(e), }; diff --git a/src/plugins/data_source/server/routes/data_source_connection_validator.ts b/src/plugins/data_source/server/routes/data_source_connection_validator.ts new file mode 100644 index 000000000000..b23a92624d2a --- /dev/null +++ b/src/plugins/data_source/server/routes/data_source_connection_validator.ts @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpenSearchClient } from 'opensearch-dashboards/server'; +import { createDataSourceError } from '../lib/error'; + +export class DataSourceConnectionValidator { + constructor(private readonly callDataCluster: OpenSearchClient) {} + + async validate() { + try { + return await this.callDataCluster.info(); + } catch (e) { + if (e.statusCode === 403) { + return true; + } else { + throw createDataSourceError(e); + } + } + } +} diff --git a/src/plugins/data_source/server/routes/test_connection.ts b/src/plugins/data_source/server/routes/test_connection.ts new file mode 100644 index 000000000000..edebd4feb91f --- /dev/null +++ b/src/plugins/data_source/server/routes/test_connection.ts @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter, OpenSearchClient } from 'opensearch-dashboards/server'; +import { DataSourceAttributes } from '../../common/data_sources'; +import { DataSourceConnectionValidator } from './data_source_connection_validator'; +import { DataSourceServiceSetup } from '../data_source_service'; +import { CryptographyServiceSetup } from '../cryptography_service'; + +export const registerTestConnectionRoute = ( + router: IRouter, + dataSourceServiceSetup: DataSourceServiceSetup, + cryptography: CryptographyServiceSetup +) => { + router.post( + { + path: '/internal/data-source-management/validate', + validate: { + body: schema.object({ + id: schema.string(), + endpoint: schema.string(), + auth: schema.maybe( + schema.object({ + type: schema.oneOf([schema.literal('username_password'), schema.literal('no_auth')]), + credentials: schema.oneOf([ + schema.object({ + username: schema.string(), + password: schema.string(), + }), + schema.literal(null), + ]), + }) + ), + }), + }, + }, + async (context, request, response) => { + const dataSource: DataSourceAttributes = request.body as DataSourceAttributes; + + const dataSourceClient: OpenSearchClient = await dataSourceServiceSetup.getTestingClient( + { + dataSourceId: dataSource.id || '', + savedObjects: context.core.savedObjects.client, + cryptography, + }, + dataSource + ); + + try { + const dsValidator = new DataSourceConnectionValidator(dataSourceClient); + + await dsValidator.validate(); + + return response.ok({ + body: { + success: true, + }, + }); + } catch (err) { + return response.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +}; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap b/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap deleted file mode 100644 index c7e3152fca99..000000000000 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/__snapshots__/create_data_source_wizard.test.tsx.snap +++ /dev/null @@ -1,1941 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Create Datasource Wizard case1: should load resources successfully should render normally 1`] = ` - - - - - -
-
- -
- -
-
- -

- - - Create data source connection - - -

-
- -
- - -
-

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

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

- - - Connection Details - - -

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

- - - Authentication Method - - -

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

- - - Create data source connection - - -

-
- -
- - -
-

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

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

- - - Connection Details - - -

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

- - - Authentication Method - - -

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

- - - Create data source connection - - -

-
- -
- - -
-

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

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

- - - Connection Details - - -

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

- - - Authentication Method - - -

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

- - - Create data source connection - - -

-
- -
- - -
-

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

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

- - - Connection Details - - -

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

- - - Authentication Method - - -

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

- - - Create data source connection - - -

-
- -
- - -
-

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

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

- - - Connection Details - - -

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

- - - Authentication Method - - -

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

- - - Create data source connection - - -

-
- -
- - -
-

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

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

- - - Connection Details - - -

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

- - - Authentication Method - - -

-
-
- -
- - -
-
- -
- - - Provide authentication details require to gain access to the endpoint. If no authentication is required, choose - - - - - - No authentication - - - -
-
-
-
-
- -
- - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
-
- - - -
-
- - -
-
- - - - -
-
-
-
-
-
-
- -
-
- - - -
-
- - , - ] - } - compressed={false} - fullWidth={false} - icon="lock" - isLoading={false} - > -
-
- - - - -
- - - - - -
-
-
- - - -
-
-
-
-
-
- -
- - - - - - -
- -
- - - - -`; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx index 5ed8370afc39..bcd7fe6b9c47 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx @@ -20,11 +20,13 @@ const authTypeIdentifier = '[data-test-subj="createDataSourceFormAuthTypeSelect" const usernameIdentifier = '[data-test-subj="createDataSourceFormUsernameField"]'; const passwordIdentifier = '[data-test-subj="createDataSourceFormPasswordField"]'; const createButtonIdentifier = '[data-test-subj="createDataSourceButton"]'; +const testConnectionButtonIdentifier = '[data-test-subj="createDataSourceTestConnectionButton"]'; describe('Datasource Management: Create Datasource form', () => { const mockedContext = mockManagementPlugin.createDataSourceManagementContext(); let component: ReactWrapper, React.Component<{}, {}, any>>; const mockSubmitHandler = jest.fn(); + const mockTestConnectionHandler = jest.fn(); const getFields = (comp: ReactWrapper, React.Component<{}, {}, any>>) => { return { @@ -66,6 +68,7 @@ describe('Datasource Management: Create Datasource form', () => { component = mount( wrapWithIntl( @@ -81,7 +84,8 @@ describe('Datasource Management: Create Datasource form', () => { /* Scenario 1: Should render the page normally*/ test('should render normally', () => { - expect(component).toMatchSnapshot(); + const testConnBtn = component.find(testConnectionButtonIdentifier).last(); + expect(testConnBtn.prop('disabled')).toBe(true); }); /* Scenario 2: submit without any input from user - should display validation error messages*/ @@ -117,7 +121,6 @@ describe('Datasource Management: Create Datasource form', () => { const { title, description, endpoint, username, password } = getFields(component); - expect(component).toMatchSnapshot(); expect(title.prop('isInvalid')).toBe(true); expect(description.prop('isInvalid')).toBe(undefined); expect(endpoint.prop('isInvalid')).toBe(false); @@ -142,9 +145,10 @@ describe('Datasource Management: Create Datasource form', () => { changeTextFieldValue(usernameIdentifier, 'test123'); changeTextFieldValue(passwordIdentifier, 'test123'); - findTestSubject(component, 'createDataSourceButton').simulate('click'); + findTestSubject(component, 'createDataSourceTestConnectionButton').simulate('click'); - expect(component).toMatchSnapshot(); + findTestSubject(component, 'createDataSourceButton').simulate('click'); + expect(mockTestConnectionHandler).toHaveBeenCalled(); expect(mockSubmitHandler).toHaveBeenCalled(); // should call submit as all fields are valid }); @@ -158,7 +162,6 @@ describe('Datasource Management: Create Datasource form', () => { findTestSubject(component, 'createDataSourceButton').simulate('click'); - expect(component).toMatchSnapshot(); expect(mockSubmitHandler).toHaveBeenCalled(); // should call submit as all fields are valid }); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index b159065822df..a310bb0f2a76 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -8,6 +8,8 @@ import { EuiButton, EuiFieldPassword, EuiFieldText, + EuiFlexGroup, + EuiFlexItem, EuiForm, EuiFormRow, EuiPageContent, @@ -37,6 +39,7 @@ import { isValidUrl } from '../../../utils'; export interface CreateDataSourceProps { existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; + handleTestConnection: (formValues: DataSourceAttributes) => void; } export interface CreateDataSourceState { /* Validation */ @@ -179,12 +182,7 @@ export class CreateDataSourceForm extends React.Component< onClickCreateNewDataSource = () => { if (this.isFormValid()) { - const formValues: DataSourceAttributes = { - title: this.state.title, - description: this.state.description, - endpoint: this.state.endpoint, - auth: { ...this.state.auth }, - }; + const formValues: DataSourceAttributes = this.getFormValues(); /* Remove credentials object for NoAuth */ if (this.state.auth.type === AuthType.NoAuth) { @@ -195,6 +193,22 @@ export class CreateDataSourceForm extends React.Component< } }; + onClickTestConnection = () => { + if (this.isFormValid()) { + /* Submit */ + this.props.handleTestConnection(this.getFormValues()); + } + }; + + getFormValues = (): DataSourceAttributes => { + return { + title: this.state.title, + description: this.state.description, + endpoint: this.state.endpoint, + auth: { ...this.state.auth, credentials: { ...this.state.auth.credentials } }, + }; + }; + /* Render methods */ /* Render header*/ @@ -409,19 +423,40 @@ export class CreateDataSourceForm extends React.Component< : null} - {/* Create Data Source button*/} - - - + + + + {/* Test Connection button*/} + + + + + {/* Create Data Source button*/} + + + + + + + ); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx index 06084a41461a..162af4c891f7 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx @@ -46,9 +46,6 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); component.update(); }); - test('should render normally', () => { - expect(component).toMatchSnapshot(); - }); test('should create datasource successfully', async () => { spyOn(utils, 'createSingleDataSource').and.returnValue({}); @@ -72,6 +69,30 @@ describe('Datasource Management: Create Datasource Wizard', () => { component.update(); expect(utils.createSingleDataSource).toHaveBeenCalled(); }); + + test('should test connection to the endpoint successfully', async () => { + spyOn(utils, 'testConnection').and.returnValue({}); + + await act(async () => { + // @ts-ignore + await component.find('CreateDataSourceForm').first().prop('handleTestConnection')( + mockDataSourceAttributesWithAuth + ); + }); + expect(utils.testConnection).toHaveBeenCalled(); + }); + + test('should fail to test connection to the endpoint', async () => { + spyOn(utils, 'testConnection').and.throwError('error'); + await act(async () => { + // @ts-ignore + await component.find('CreateDataSourceForm').first().prop('handleTestConnection')( + mockDataSourceAttributesWithAuth + ); + }); + component.update(); + expect(utils.testConnection).toHaveBeenCalled(); + }); }); describe('case2: should fail to load resources', () => { beforeEach(async () => { @@ -96,7 +117,6 @@ describe('Datasource Management: Create Datasource Wizard', () => { component.update(); }); test('should not render component and go back to listing page', () => { - expect(component).toMatchSnapshot(); expect(history.push).toBeCalledWith(''); }); }); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx index 08ac198c7561..83477b7a2426 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx @@ -8,12 +8,16 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { useEffectOnce } from 'react-use'; import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { DataSourceManagementContext, DataSourceTableItem, ToastMessageItem } from '../../types'; +import { + DataSourceAttributes, + DataSourceManagementContext, + DataSourceTableItem, + ToastMessageItem, +} from '../../types'; import { getCreateBreadcrumbs } from '../breadcrumbs'; import { CreateDataSourceForm } from './components/create_form'; -import { createSingleDataSource, getDataSources } from '../utils'; +import { createSingleDataSource, getDataSources, testConnection } from '../utils'; import { LoadingMask } from '../loading_mask'; -import { DataSourceAttributes } from '../../types'; type CreateDataSourceWizardProps = RouteComponentProps; @@ -24,6 +28,7 @@ export const CreateDataSourceWizard: React.FunctionComponent().services; @@ -74,8 +79,34 @@ export const CreateDataSourceWizard: React.FunctionComponent { - toasts.addDanger(i18n.translate(id, { defaultMessage })); + /* Handle submit - create data source*/ + const handleTestConnection = async (attributes: DataSourceAttributes) => { + setIsLoading(true); + try { + await testConnection(http, attributes); + handleDisplayToastMessage({ + id: 'dataSourcesManagement.createDataSource.testConnectionSuccessMsg', + defaultMessage: + 'Connecting to the endpoint using the provided authentication method was successful.', + success: true, + }); + } catch (e) { + handleDisplayToastMessage({ + id: 'dataSourcesManagement.createDataSource.testConnectionFailMsg', + defaultMessage: + 'Failed Connecting to the endpoint using the provided authentication method.', + }); + } finally { + setIsLoading(false); + } + }; + + const handleDisplayToastMessage = ({ id, defaultMessage, success }: ToastMessageItem) => { + if (success) { + toasts.addSuccess(i18n.translate(id, { defaultMessage })); + } else { + toasts.addDanger(i18n.translate(id, { defaultMessage })); + } }; /* Render the creation wizard */ @@ -84,6 +115,7 @@ export const CreateDataSourceWizard: React.FunctionComponent {isLoading ? : null} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/__snapshots__/edit_data_source.test.tsx.snap b/src/plugins/data_source_management/public/components/edit_data_source/__snapshots__/edit_data_source.test.tsx.snap deleted file mode 100644 index 2858167a08c0..000000000000 --- a/src/plugins/data_source_management/public/components/edit_data_source/__snapshots__/edit_data_source.test.tsx.snap +++ /dev/null @@ -1,1228 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Edit Datasource Wizard should load resources successfully should render normally 1`] = ` - - - -
- - -
- -
- -
-
- -

- create-test-ds -

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

- - - Connection Details - - -

-
-
- -
-
- - -

- } - title={ -

- -

- } - > -
- -
- -
- -

- - - Object Details - - -

-
- -
- -
-

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

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

- - - Endpoint - - -

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

- - - Authentication - - -

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

- - - Authentication Method - - -

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

- create-test-ds -

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

- - - Connection Details - - -

-
-
- -
-
- - -

- } - title={ -

- -

- } - > -
- -
- -
- -

- - - Object Details - - -

-
- -
- -
-

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

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

- - - Endpoint - - -

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

- - - Authentication - - -

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

- - - Authentication Method - - -

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

- create-test-ds123 -

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

- - - Connection Details - - -

-
-
- -
-
- - -

- } - title={ -

- -

- } - > -
- -
- -
- -

- - - Object Details - - -

-
- -
- -
-

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

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

- - - Endpoint - - -

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

- - - Authentication - - -

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

- - - Authentication Method - - -

-
-
-
- -
- -
-
- - - -
-
- -
- -
- -
- -
- - -
- -
- -
- -
- -
-
- - -
- -
- -
- -
- -
- -
- - -
- - -
- - - -`; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx index bf63f9ff8125..492e34e4e198 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.test.tsx @@ -58,6 +58,7 @@ describe('Datasource Management: Edit Datasource Form', () => { existingDatasourceNamesList={existingDatasourceNamesList} onDeleteDataSource={mockFn} handleSubmit={mockFn} + handleTestConnection={mockFn} displayToastMessage={mockFn} /> ), @@ -72,7 +73,6 @@ describe('Datasource Management: Edit Datasource Form', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); // @ts-ignore expect(component.find({ name: titleFieldIdentifier }).first().props().value).toBe( mockDataSourceAttributesWithAuth.title @@ -230,6 +230,7 @@ describe('Datasource Management: Edit Datasource Form', () => { existingDatasourceNamesList={existingDatasourceNamesList} onDeleteDataSource={mockFn} handleSubmit={mockFn} + handleTestConnection={mockFn} displayToastMessage={mockFn} /> ), @@ -244,7 +245,6 @@ describe('Datasource Management: Edit Datasource Form', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); // @ts-ignore expect(component.find({ name: titleFieldIdentifier }).first().props().value).toBe( mockDataSourceAttributesWithNoAuth.title @@ -326,5 +326,14 @@ describe('Datasource Management: Edit Datasource Form', () => { }, 100) ); }); + + /* Test Connection */ + test('should test connection on click test connection button', async () => { + expect(component.find('Header').exists()).toBe(true); + // @ts-ignore + component.find('Header').first().prop('onClickTestConnection')(); + component.update(); + expect(mockFn).toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index 46c91ad540c8..561a651edee2 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -45,6 +45,7 @@ export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; + handleTestConnection: (formValues: DataSourceAttributes) => void; onDeleteDataSource?: () => void; displayToastMessage: (info: ToastMessageItem) => void; } @@ -231,7 +232,7 @@ export class EditDataSourceForm extends React.Component { + this.setState({ isLoading: true }); + const existingAuthType = this.props.existingDataSource.auth.type; + + try { + const isNewCredential = !!( + existingAuthType === AuthType.NoAuth && this.state.auth.type !== existingAuthType + ); + const formValues: DataSourceAttributes = { + title: this.state.title, + description: this.state.description, + endpoint: this.props.existingDataSource.endpoint, + auth: { + ...this.state.auth, + credentials: { + ...this.state.auth.credentials, + password: isNewCredential ? this.state.auth.credentials.password : '', + }, + }, + }; + + await this.props.handleTestConnection(formValues); + + this.props.displayToastMessage({ + id: 'dataSourcesManagement.editDataSource.testConnectionSuccessMsg', + defaultMessage: + 'Connecting to the endpoint using the provided authentication method was successful.', + success: true, + }); + } catch (e) { + this.props.displayToastMessage({ + id: 'dataSourcesManagement.editDataSource.testConnectionFailMsg', + defaultMessage: + 'Failed Connecting to the endpoint using the provided authentication method.', + }); + } finally { + this.setState({ isLoading: false }); + } + }; + onChangeFormValues = () => { setTimeout(() => { this.didFormValuesChange(); @@ -280,7 +321,7 @@ export class EditDataSourceForm extends React.Component ); }; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/data_source_management/public/components/edit_data_source/components/header/__snapshots__/header.test.tsx.snap deleted file mode 100644 index d9877a2cdc1d..000000000000 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/__snapshots__/header.test.tsx.snap +++ /dev/null @@ -1,175 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datasource Management: Edit Datasource Header do not show delete icon should render normally 1`] = ` - -
- -
- -
-
- -

- testTest20 -

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

- testTest20 -

-
- -
- -
-
- - -
- - - - - - - -
-
-
- -
-
-`; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx index 36a3551d9ada..f679a7db6e67 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.test.tsx @@ -26,8 +26,10 @@ describe('Datasource Management: Edit Datasource Header', () => { component = mount( wrapWithIntl(
), @@ -41,7 +43,6 @@ describe('Datasource Management: Edit Datasource Header', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); expect(component.find(headerTitleIdentifier).last().text()).toBe(dataSourceName); }); test('should show confirm delete modal pop up on trash icon click and cancel button work normally', () => { @@ -76,8 +77,10 @@ describe('Datasource Management: Edit Datasource Header', () => { component = mount( wrapWithIntl(
), @@ -90,7 +93,6 @@ describe('Datasource Management: Edit Datasource Header', () => { ); }); test('should render normally', () => { - expect(component).toMatchSnapshot(); expect(component.find(headerTitleIdentifier).last().text()).toBe(dataSourceName); expect(component.find(deleteIconIdentifier).exists()).toBe(false); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx index 8a73bcccc275..445e22401418 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/header/header.tsx @@ -13,6 +13,7 @@ import { EuiToolTip, EuiButtonIcon, EuiConfirmModal, + EuiButton, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; @@ -21,11 +22,15 @@ import { DataSourceManagementContext } from '../../../../types'; export const Header = ({ showDeleteIcon, + isFormValid, onClickDeleteIcon, + onClickTestConnection, dataSourceName, }: { showDeleteIcon: boolean; + isFormValid: boolean; onClickDeleteIcon: () => void; + onClickTestConnection: () => void; dataSourceName: string; }) => { /* State Variables */ @@ -105,9 +110,28 @@ export const Header = ({ ); }; + const renderTestConnectionButton = () => { + return ( + { + onClickTestConnection(); + }} + data-test-subj="datasource-edit-testConnectionButton" + > + + + ); + }; return ( + {/* Title */}
@@ -116,7 +140,16 @@ export const Header = ({
- {showDeleteIcon ? renderDeleteButton() : null} + + {/* Right side buttons */} + + + {/* Test connection button */} + {renderTestConnectionButton()} + {/* Delete icon button */} + {showDeleteIcon ? renderDeleteButton() : null} + +
); }; diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx index d11f8c8bc9da..5f6e823e0f86 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.test.tsx @@ -85,7 +85,6 @@ describe('Datasource Management: Edit Datasource Wizard', () => { }); test('should render normally', () => { - expect(component).toMatchSnapshot(); expect(component.find(notFoundIdentifier).exists()).toBe(false); expect(utils.getDataSources).toHaveBeenCalled(); expect(utils.getDataSourceById).toHaveBeenCalled(); @@ -136,5 +135,14 @@ describe('Datasource Management: Edit Datasource Wizard', () => { component.update(); expect(utils.deleteDataSourceById).toHaveBeenCalled(); }); + test('should test connection', () => { + spyOn(utils, 'testConnection'); + // @ts-ignore + component.find('EditDataSourceForm').first().prop('handleTestConnection')( + mockDataSourceAttributesWithAuth + ); + component.update(); + expect(utils.testConnection).toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx index 9bbaecfccce4..bc2bac5b66b8 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/edit_data_source.tsx @@ -15,6 +15,7 @@ import { deleteDataSourceById, getDataSourceById, getDataSources, + testConnection, updateDataSourceById, } from '../utils'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -39,6 +40,7 @@ export const EditDataSource: React.FunctionComponent().services; const dataSourceID: string = props.match.params.id; @@ -110,6 +112,11 @@ export const EditDataSource: React.FunctionComponent { + await testConnection(http, attributes, dataSourceID); + }; + /* Render the edit wizard */ const renderContent = () => { if (!isLoading && (!dataSource || !dataSource.id)) { @@ -124,6 +131,7 @@ export const EditDataSource: React.FunctionComponent ) : null} {isLoading || !dataSource?.endpoint ? : null} diff --git a/src/plugins/data_source_management/public/components/utils.test.ts b/src/plugins/data_source_management/public/components/utils.test.ts index 7aeb00e14f7e..99bb126933a5 100644 --- a/src/plugins/data_source_management/public/components/utils.test.ts +++ b/src/plugins/data_source_management/public/components/utils.test.ts @@ -10,6 +10,7 @@ import { getDataSourceById, getDataSources, isValidUrl, + testConnection, updateDataSourceById, } from './utils'; import { coreMock } from '../../../../core/public/mocks'; @@ -23,6 +24,7 @@ import { mockResponseForSavedObjectsCalls, } from '../mocks'; import { AuthType } from '../types'; +import { HttpStart } from 'opensearch-dashboards/public'; const { savedObjects } = coreMock.createStart(); @@ -139,6 +141,51 @@ describe('DataSourceManagement: Utils.ts', () => { }); }); + describe('Test connection to the endpoint of the data source - success', () => { + let http: jest.Mocked; + const mockSuccess = jest.fn().mockResolvedValue({ body: { success: true } }); + const mockError = jest.fn().mockRejectedValue(null); + beforeEach(() => { + http = coreMock.createStart().http; + http.post.mockResolvedValue(mockSuccess); + }); + test('Success: Test Connection to the endpoint while creating a new data source', async () => { + await testConnection(http, getDataSourceByIdWithoutCredential.attributes); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/data-source-management/validate", + Object { + "body": "{\\"id\\":\\"\\",\\"endpoint\\":\\"https://test.com\\",\\"auth\\":{\\"type\\":\\"no_auth\\",\\"credentials\\":null}}", + }, + ], + ] + `); + }); + + test('Success: Test Connection to the endpoint while existing data source is updated', async () => { + await testConnection(http, getDataSourceByIdWithoutCredential.attributes, 'test1234'); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/data-source-management/validate", + Object { + "body": "{\\"id\\":\\"test1234\\",\\"endpoint\\":\\"https://test.com\\",\\"auth\\":{\\"type\\":\\"no_auth\\",\\"credentials\\":null}}", + }, + ], + ] + `); + }); + test('failure: Test Connection to the endpoint while creating/updating a data source', async () => { + try { + http.post.mockRejectedValue(mockError); + await testConnection(http, getDataSourceByIdWithoutCredential.attributes, 'test1234'); + } catch (e) { + expect(e).toBeTruthy(); + } + }); + }); + describe('Delete multiple data sources by id', () => { test('Success: deleting multiple data source', async () => { try { diff --git a/src/plugins/data_source_management/public/components/utils.ts b/src/plugins/data_source_management/public/components/utils.ts index 51f190be1ba0..dafc03777fca 100644 --- a/src/plugins/data_source_management/public/components/utils.ts +++ b/src/plugins/data_source_management/public/components/utils.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectsClientContract } from 'src/core/public'; -import { DataSourceTableItem, DataSourceAttributes } from '../types'; +import { HttpStart, SavedObjectsClientContract } from 'src/core/public'; +import { AuthType, DataSourceAttributes, DataSourceTableItem } from '../types'; export async function getDataSources(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient @@ -79,6 +79,25 @@ export async function deleteMultipleDataSources( ); } +export async function testConnection( + http: HttpStart, + { endpoint, auth: { type, credentials } }: DataSourceAttributes, + dataSourceID?: string +) { + const query: any = { + id: dataSourceID || '', + endpoint, + auth: { + type, + credentials: type === AuthType.NoAuth ? null : { ...credentials }, + }, + }; + + await http.post(`/internal/data-source-management/validate`, { + body: JSON.stringify(query), + }); +} + export const isValidUrl = (endpoint: string) => { try { const url = new URL(endpoint); diff --git a/src/plugins/opensearch_dashboards_utils/README.md b/src/plugins/opensearch_dashboards_utils/README.md index 46e436ba89df..d5650849ff13 100644 --- a/src/plugins/opensearch_dashboards_utils/README.md +++ b/src/plugins/opensearch_dashboards_utils/README.md @@ -4,3 +4,4 @@ Utilities for building OpenSearch Dashboards plugins. - [State containers](./docs/state_containers). - [State syncing utilities](./docs/state_sync). +- [Global data persistence](./docs/global_data_persistence.md) diff --git a/src/plugins/opensearch_dashboards_utils/docs/global_data_persistence.md b/src/plugins/opensearch_dashboards_utils/docs/global_data_persistence.md new file mode 100644 index 000000000000..d756b8c35ac5 --- /dev/null +++ b/src/plugins/opensearch_dashboards_utils/docs/global_data_persistence.md @@ -0,0 +1,99 @@ +# Global data persistence + +As of 12/1/2022, there are five plugins that have implemented global data persistence ability in OpenSearch Dashboards, and they are visualize, discover, Timeline, dashboards, and vis-builder. Global data persistence means that the data are not only persisted over refreshes, but also able to be persisted across multiple plugins. We utilize [state containers](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_containers), [state storage](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_sync/storages) and [state syncing utilities](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils/docs/state_sync) from [OpenSearch Dashboards Utils](https://github.com/opensearch-project/OpenSearch-Dashboards/tree/main/src/plugins/opensearch_dashboards_utils) to achieve global data persistence. User can choose to persist data either in URL or session storage by changing the setting `Store URLs in session storage` under advanced setting page. + +One of the global data persistence example that currently exists is global query parameters. Global query parameters include globally pinned filters, time range and time refresh intervals. For example, we set a specific time range and time refresh interval when trying to a new visualization. When we navigate to the dashboard page, we can see the previous time range and time refresh interval that are set within the visualization app are still there. However, when we create a filter, it will only be persisted within that specific plugin since it is not a global filter. We can make a filter become a global filter by selecting `Pin across all apps`. Only global filters are persisted across all other globally persistent plugins within the application. + +The following five steps demonstrate how to add global query parameter persistence for a plugin. Step 3 is specific to global query parameter persistence. For implementing global data persistence in general, step 1 and 2 are required. A function that is similar to step 3 to sync up the state manager of the data with osdUrlStateStorage is also required. + +# Steps to add global data persistence ability to a plugin + +1. Call [`createOsdUrlTracker()`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/opensearch_dashboards_utils/public/state_management/url/osd_url_tracker.ts) in the set up function within public/plugin.ts. This creates a tracker that syncs the storage with the state manager by listening to history changes and global state changes, and updating the nav link URL of a given app to point to the last visited page. The two functions that get returned, `appMounted()` and `appUnMounted()`, help with global data persistence across the app. When user enters one app, `appMounted()` will be called to make sure that the current app is actively listening to history changes. It will also initialize the URL to be previously stored URL from storage. When user leaves one app, `appUnmounted()` will be called so the app will stop listening actively on history changes, but start subscribing to the global states. Therefore, if the global states are changed in another app, the global state listener will still be triggered in this app even though it is not currently active. It will also update the corresponding URL in the browser storage. By using `appMounted()` and `appUnMounted()`, it makes sure that global data are always persisted no matter which app we are currently on. + * declare two private variables: `appStateUpdater` observable and `stopUrlTracking()` + ```ts + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking?: () => void; + ``` + * within the `setup()` function in the plugin class, call `createOsdUrlTracker` by passing in the corresponding baseUrl, defaultSubUrl, storageKey, navLinkUpdater observable and stateParams. StorageKey should follow format: `lastUrl:${core.http.basePath.get()}:pluginID`. + - `this.appStateUpdater` is passed into the function as `navLinkUpdater`. + - return three functions `appMounted()`, `appUnMounted()` and `stopUrlTracker()`. Then class variable `stopUrlTracking()` is set to be `stopUrlTracker()` + * call `appMounted()` in the `mount()` function + * call `appUnMounted()` in return of `mount()` + * call `stopUrlTracking()` in `stop()` function for the plugin + + ```ts + const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/vis-builder'), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:vis-builder`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => + !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + ``` + +2. Set [`osdUrlStateStorage()`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/opensearch_dashboards_utils/public/state_sync/state_sync_state_storage/create_osd_url_state_storage.ts#L83) service. This step initializes the store, and indicates global storage by using '_g' flag. + * when setting the plugin services, set osdUrlStateStorage service by calling `createOsdUrlStateStorage()` with the current history, useHash and withNotifyErrors + + ```ts + const services: VisBuilderServices = { + ...coreStart, + history: params.history, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: params.history, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), + ... + + ``` +3. Sync states with storage. There are many ways to do this and use whatever makes sense for your specific use cases. One such implementation is for syncing the query data in `syncQueryStateWithUrl` from the data plugin. + * import [`syncQueryStateWithUrl`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/data/public/query/state_sync/sync_state_with_url.ts#L48) from data plugin and call it with query service and osdUrlStateStorage service that we set in step 2. This function completes two jobs: 1. When we first enter the app and there is no data stored in the URL, it initializes the URL by putting the `_g` key followed by default data values. 2. When we refresh the page, this function is responsible to retrive the stored states in the URL, and apply them to the app. + + ```ts + export const VisBuilderApp = () => { + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); + ``` + + * If not already, add query services from data plugin in public/plugin_services.ts + + ```ts + export const [getQueryService, setQueryService] = createGetterSetter('Query'); + ``` + diff --git a/src/plugins/vis_builder/public/application/app.tsx b/src/plugins/vis_builder/public/application/app.tsx index bd5f2be1feda..2bdc2b1c631b 100644 --- a/src/plugins/vis_builder/public/application/app.tsx +++ b/src/plugins/vis_builder/public/application/app.tsx @@ -3,17 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage, EuiResizableContainer } from '@elastic/eui'; +import { useLocation } from 'react-router-dom'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; import { LeftNav } from './components/left_nav'; import { TopNav } from './components/top_nav'; import { Workspace } from './components/workspace'; import './app.scss'; import { RightNav } from './components/right_nav'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { VisBuilderServices } from '../types'; +import { syncQueryStateWithUrl } from '../../../data/public'; export const VisBuilderApp = () => { + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); + // Render the application DOM. return ( diff --git a/src/plugins/vis_builder/public/application/components/top_nav.tsx b/src/plugins/vis_builder/public/application/components/top_nav.tsx index 62d3bb78cc52..fe9b735b5acf 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx @@ -36,6 +36,9 @@ export const TopNav = () => { const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl); const { selected: indexPattern } = useIndexPatterns(); const [config, setConfig] = useState(); + const originatingApp = useTypedSelector((state) => { + return state.metadata.originatingApp; + }); useEffect(() => { const getConfig = () => { @@ -47,6 +50,7 @@ export const TopNav = () => { savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern), saveDisabledReason, dispatch, + originatingApp, }, services ); @@ -61,6 +65,7 @@ export const TopNav = () => { saveDisabledReason, dispatch, indexPattern, + originatingApp, ]); // reset validity before component destroyed diff --git a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx index c88bb13f3cb3..9df321822852 100644 --- a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx @@ -46,22 +46,24 @@ export interface TopNavConfigParams { savedVisBuilderVis: VisBuilderVisSavedObject; saveDisabledReason?: string; dispatch: AppDispatch; + originatingApp?: string; } export const getTopNavConfig = ( - { visualizationIdFromUrl, savedVisBuilderVis, saveDisabledReason, dispatch }: TopNavConfigParams, + { + visualizationIdFromUrl, + savedVisBuilderVis, + saveDisabledReason, + dispatch, + originatingApp, + }: TopNavConfigParams, services: VisBuilderServices ) => { const { i18n: { Context: I18nContext }, embeddable, - scopedHistory, } = services; - const { originatingApp, embeddableId } = - embeddable - .getStateTransfer(scopedHistory) - .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const stateTransfer = embeddable.getStateTransfer(); const topNavConfig: TopNavMenuData[] = [ @@ -105,7 +107,7 @@ export const getTopNavConfig = ( showSaveModal(saveModal, I18nContext); }, }, - ...(originatingApp && ((savedVisBuilderVis && savedVisBuilderVis.id) || embeddableId) + ...(originatingApp && savedVisBuilderVis && savedVisBuilderVis.id ? [ { id: 'saveAndReturn', diff --git a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts index 8cc71804f12e..c1e23b52823d 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts @@ -21,6 +21,7 @@ export interface MetadataState { }; state: EditorState; }; + originatingApp?: string; } const initialState: MetadataState = { @@ -28,13 +29,20 @@ const initialState: MetadataState = { validity: {}, state: 'loading', }, + originatingApp: undefined, }; export const getPreloadedState = async ({ types, data, + embeddable, + scopedHistory, }: VisBuilderServices): Promise => { - const preloadedState = { ...initialState }; + const { originatingApp } = + embeddable + .getStateTransfer(scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; + const preloadedState = { ...initialState, originatingApp }; return preloadedState; }; @@ -50,6 +58,9 @@ export const slice = createSlice({ setEditorState: (state, action: PayloadAction<{ state: EditorState }>) => { state.editor.state = action.payload.state; }, + setOriginatingApp: (state, action: PayloadAction<{ state?: string }>) => { + state.originatingApp = action.payload.state; + }, setState: (_state, action: PayloadAction) => { return action.payload; }, @@ -57,4 +68,4 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setValidity, setEditorState, setState } = slice.actions; +export const { setValidity, setEditorState, setOriginatingApp, setState } = slice.actions; diff --git a/src/plugins/vis_builder/public/plugin.test.ts b/src/plugins/vis_builder/public/plugin.test.ts index f5fac728420b..35e17865649a 100644 --- a/src/plugins/vis_builder/public/plugin.test.ts +++ b/src/plugins/vis_builder/public/plugin.test.ts @@ -28,6 +28,7 @@ describe('VisBuilderPlugin', () => { const setupDeps = { visualizations: visualizationsPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createSetupContract(), + data: dataPluginMock.createSetupContract(), }; const setup = plugin.setup(coreSetup, setupDeps); diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 8e90a04784cd..a619958653b6 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -4,13 +4,17 @@ */ import { i18n } from '@osd/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { AppMountParameters, AppNavLinkStatus, + AppUpdater, CoreSetup, CoreStart, Plugin, PluginInitializerContext, + ScopedHistory, } from '../../../core/public'; import { VisBuilderPluginSetupDependencies, @@ -41,10 +45,17 @@ import { setUISettings, setTypeService, setReactExpressionRenderer, + setQueryService, } from './plugin_services'; import { createSavedVisBuilderLoader } from './saved_visualizations'; import { registerDefaultTypes } from './visualizations'; import { ConfigSchema } from '../config'; +import { + createOsdUrlStateStorage, + createOsdUrlTracker, + withNotifyOnErrors, +} from '../../opensearch_dashboards_utils/public'; +import { opensearchFilters } from '../../data/public'; export class VisBuilderPlugin implements @@ -55,13 +66,45 @@ export class VisBuilderPlugin VisBuilderPluginStartDependencies > { private typeService = new TypeService(); + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking?: () => void; + private currentHistory?: ScopedHistory; constructor(public initializerContext: PluginInitializerContext) {} public setup( core: CoreSetup, - { embeddable, visualizations }: VisBuilderPluginSetupDependencies + { embeddable, visualizations, data }: VisBuilderPluginSetupDependencies ) { + const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend(`/app/${PLUGIN_ID}`), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:${PLUGIN_ID}`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + + // Register Default Visualizations const typeService = this.typeService; registerDefaultTypes(typeService.setup()); @@ -70,43 +113,62 @@ export class VisBuilderPlugin id: PLUGIN_ID, title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, - async mount(params: AppMountParameters) { + defaultPath: '#/', + mount: async (params: AppMountParameters) => { // Load application bundle const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json const [coreStart, pluginsStart, selfStart] = await core.getStartServices(); - const { data, savedObjects, navigation, expressions } = pluginsStart; + const { savedObjects, navigation, expressions } = pluginsStart; + this.currentHistory = params.history; // make sure the index pattern list is up to date - data.indexPatterns.clearCache(); + pluginsStart.data.indexPatterns.clearCache(); // make sure a default index pattern exists // if not, the page will be redirected to management and visualize won't be rendered // TODO: Add the redirect await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); - // Register Default Visualizations + appMounted(); + + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = this.currentHistory.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); const services: VisBuilderServices = { ...coreStart, + scopedHistory: this.currentHistory, + history: this.currentHistory, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: this.currentHistory, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), toastNotifications: coreStart.notifications.toasts, - data, + data: pluginsStart.data, savedObjectsPublic: savedObjects, navigation, expressions, - history: params.history, setHeaderActionMenu: params.setHeaderActionMenu, types: typeService.start(), savedVisBuilderLoader: selfStart.savedVisBuilderLoader, embeddable: pluginsStart.embeddable, - scopedHistory: params.history, + dashboard: pluginsStart.dashboard, }; // Instantiate the store const store = await getPreloadedStore(services); + const unmount = renderApp(params, services, store); // Render the application - return renderApp(params, services, store); + return () => { + unlistenParentHistory(); + unmount(); + appUnMounted(); + }; }, }); @@ -154,7 +216,7 @@ export class VisBuilderPlugin public start( core: CoreStart, - { data, expressions }: VisBuilderPluginStartDependencies + { expressions, data }: VisBuilderPluginStartDependencies ): VisBuilderStart { const typeService = this.typeService.start(); @@ -176,6 +238,7 @@ export class VisBuilderPlugin setTimeFilter(data.query.timefilter.timefilter); setTypeService(typeService); setUISettings(core.uiSettings); + setQueryService(data.query); return { ...typeService, @@ -183,5 +246,9 @@ export class VisBuilderPlugin }; } - public stop() {} + public stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } } diff --git a/src/plugins/vis_builder/public/plugin_services.ts b/src/plugins/vis_builder/public/plugin_services.ts index f979f3a22b11..c5583e3c5e43 100644 --- a/src/plugins/vis_builder/public/plugin_services.ts +++ b/src/plugins/vis_builder/public/plugin_services.ts @@ -37,3 +37,7 @@ export const [getTimeFilter, setTimeFilter] = createGetterSetter('TypeService'); export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); + +export const [getQueryService, setQueryService] = createGetterSetter< + DataPublicPluginStart['query'] +>('Query'); diff --git a/src/plugins/vis_builder/public/types.ts b/src/plugins/vis_builder/public/types.ts index 131c9cc1f6bb..2d323a13b213 100644 --- a/src/plugins/vis_builder/public/types.ts +++ b/src/plugins/vis_builder/public/types.ts @@ -14,6 +14,8 @@ import { DataPublicPluginStart } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; import { SavedObjectLoader } from '../../saved_objects/public'; import { AppMountParameters, CoreStart, ToastsStart, ScopedHistory } from '../../../core/public'; +import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; +import { DataPublicPluginSetup } from '../../data/public'; export type VisBuilderSetup = TypeServiceSetup; export interface VisBuilderStart extends TypeServiceStart { @@ -23,6 +25,7 @@ export interface VisBuilderStart extends TypeServiceStart { export interface VisBuilderPluginSetupDependencies { embeddable: EmbeddableSetup; visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; } export interface VisBuilderPluginStartDependencies { embeddable: EmbeddableStart; @@ -45,6 +48,8 @@ export interface VisBuilderServices extends CoreStart { history: History; embeddable: EmbeddableStart; scopedHistory: ScopedHistory; + osdUrlStateStorage: IOsdUrlStateStorage; + dashboard: DashboardStart; } export interface ISavedVis { diff --git a/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx b/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx index 4a25395703b0..cbd0331b419c 100644 --- a/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx +++ b/src/plugins/vis_type_table_new/public/components/table_vis_component.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useMemo } from 'react'; import { orderBy } from 'lodash'; +import dompurify from 'dompurify'; import { EuiDataGridProps, EuiDataGrid, EuiDataGridSorting, EuiTitle } from '@elastic/eui'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; @@ -50,9 +51,18 @@ export const TableVisComponent = ({ const rawContent = sortedRows[rowIndex][columnId]; const colIndex = columns.findIndex((col) => col.id === columnId); const column = columns[colIndex]; - // use formatter to format raw content - // this can format date and percentage data - const formattedContent = column.formatter.convert(rawContent, 'text'); + const htmlContent = column.formatter.convert(rawContent, 'html'); + const formattedContent = ( + /* + * Justification for dangerouslySetInnerHTML: + * This is one of the visualizations which makes use of the HTML field formatters. + * Since these formatters produce raw HTML, this visualization needs to be able to render them as-is, relying + * on the field formatter to only produce safe HTML. + * `htmlContent` is created by converting raw data via HTML field formatter, so we need to make sure this value never contains + * any unsafe HTML (e.g. by bypassing the field formatter). + */ +
// eslint-disable-line react/no-danger + ); return sortedRows.hasOwnProperty(rowIndex) ? formattedContent || null : null; }) as EuiDataGridProps['renderCellValue']; }, [sortedRows, columns]); diff --git a/yarn.lock b/yarn.lock index a55a2163b84a..346e889901aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2763,10 +2763,10 @@ dependencies: defer-to-connect "^2.0.0" -"@testim/chrome-version@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.2.tgz#092005c5b77bd3bb6576a4677110a11485e11864" - integrity sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw== +"@testim/chrome-version@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.3.tgz#fbb68696899d7b8c1b9b891eded9c04fe2cd5529" + integrity sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A== "@testing-library/dom@^8.0.0", "@testing-library/dom@^8.11.3": version "8.12.0" @@ -2921,13 +2921,6 @@ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.1.3.tgz#0b03d737ff28fad10eb884e0c6cedd5ffdc4ba0a" integrity sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g== -"@types/chromedriver@^81.0.0": - version "81.0.1" - resolved "https://registry.yarnpkg.com/@types/chromedriver/-/chromedriver-81.0.1.tgz#bff3e4cdc7830dc0f115a9c0404f6979771064d4" - integrity sha512-I7ma6bBzfWc5YiMV/OZ6lYMZIANAwGbDH+QRYKnbXRptdAvUhSoFP5iHzQHas6QZCRDtefMvbxCjySUyXhxafQ== - dependencies: - "@types/node" "*" - "@types/clone@~2.1.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.1.tgz#9b880d0ce9b1f209b5e0bd6d9caa38209db34024" @@ -3004,6 +2997,13 @@ resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964" integrity sha512-sq+kwx8zA9BSugT9N+Jr8/uWjbHMZ+N/meJEzRyT3gmLq/WMtx/iSIpvdpmBUi/cvXl6Kzpvve8G2ESkabFwmg== +"@types/dompurify@^2.3.3": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" + integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== + dependencies: + "@types/trusted-types" "*" + "@types/duplexify@^3.6.0": version "3.6.1" resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.1.tgz#5685721cf7dc4a21b6f0e8a8efbec6b4d2fbafad" @@ -3800,6 +3800,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40" integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg== +"@types/trusted-types@*": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + "@types/type-detect@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6" @@ -4916,7 +4921,7 @@ axe-core@^4.0.2, axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^0.24.0, axios@^0.27.2: +axios@^0.27.2: version "0.27.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== @@ -4924,6 +4929,15 @@ axios@^0.24.0, axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.0.tgz#1cb65bd75162c70e9f8d118a905126c4a201d383" + integrity sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5227,6 +5241,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + brace@0.11.1, brace@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" @@ -5781,16 +5802,16 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -chromedriver@^100.0.0: - version "100.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-100.0.0.tgz#1b4bf5c89cea12c79f53bc94d8f5bb5aa79ed7be" - integrity sha512-oLfB0IgFEGY9qYpFQO/BNSXbPw7bgfJUN5VX8Okps9W2qNT4IqKh5hDwKWtpUIQNI6K3ToWe2/J5NdpurTY02g== +chromedriver@^107.0.3: + version "107.0.3" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-107.0.3.tgz#330c0808bb14a53f13ab7e2b0c78adf3cdb4c14b" + integrity sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg== dependencies: - "@testim/chrome-version" "^1.1.2" - axios "^0.24.0" - del "^6.0.0" + "@testim/chrome-version" "^1.1.3" + axios "^1.1.3" + compare-versions "^5.0.1" extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" proxy-from-env "^1.1.0" tcp-port-used "^1.0.1" @@ -6095,6 +6116,11 @@ compare-versions@3.5.1: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== +compare-versions@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.1.tgz#14c6008436d994c3787aba38d4087fabe858555e" + integrity sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ== + component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -7045,20 +7071,6 @@ del@^5.1.0: rimraf "^3.0.0" slash "^3.0.0" -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - del@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" @@ -7338,6 +7350,11 @@ domhandler@^4.0, domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2, domhan dependencies: domelementtype "^2.2.0" +dompurify@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631" + integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA== + domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -8820,10 +8837,10 @@ focus-lock@^0.10.2: dependencies: tslib "^2.0.3" -follow-redirects@^1.14.9: - version "1.15.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" - integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== +follow-redirects@^1.14.9, follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== font-awesome@4.7.0: version "4.7.0" @@ -10021,7 +10038,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: +https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -12686,13 +12703,27 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -"minimatch@2 || 3", minimatch@5.0.1, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2, minimatch@~3.0.4: +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@~3.0.4: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"