diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a90e3c4ac223..fd4e2984aa07 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @ananzh @kavilla @seanneumann @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @kristenTian @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @BSFishy +* @ananzh @kavilla @seanneumann @AMoo-Miki @ashwin-pc @joshuarrrr @abbyhu2000 @zengyan-amazon @kristenTian @zhongnansu @manasvinibs @ZilongX @Flyingliuhub @BSFishy @curq diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index a12d6f3ae87e..a9175ad23f8b 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -318,7 +318,7 @@ jobs: working-directory: ./artifacts strategy: matrix: - version: [osd-2.0.0, osd-2.1.0, osd-2.2.0, osd-2.3.0, osd-2.4.0, osd-2.5.0, osd-2.6.0, osd-2.7.0] + version: [osd-2.0.0, osd-2.1.0, osd-2.2.0, osd-2.3.0, osd-2.4.0, osd-2.5.0, osd-2.6.0, osd-2.7.0, osd-2.8.0, osd-2.9.0] steps: - name: Checkout code uses: actions/checkout@v3 @@ -346,7 +346,7 @@ jobs: - name: Set OpenSearch URL run: | - echo "OPENSEARCH_URL=https://ci.opensearch.org/ci/dbc/distribution-build-opensearch/${{ env.VERSION }}/latest/linux/x64/tar/dist/opensearch/opensearch-${{ env.VERSION }}-linux-x64.tar.gz" >> $GITHUB_ENV + echo "OPENSEARCH_URL=https://artifacts.opensearch.org/snapshots/core/opensearch/${{ env.VERSION }}-SNAPSHOT/opensearch-min-${{ env.VERSION }}-SNAPSHOT-linux-x64-latest.tar.gz" >> $GITHUB_ENV - name: Verify if OpenSearch is available for version id: verify-opensearch-exists diff --git a/.lycheeexclude b/.lycheeexclude index 5bb9c969dad1..67ed88344a25 100644 --- a/.lycheeexclude +++ b/.lycheeexclude @@ -124,3 +124,4 @@ http://helpmenow.com/problem2 https://sass-lang.com/* http://api.jquery.com/* http://brandonaaron.net +https://www.circl.lu/doc/misp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 00061357535d..395f608e85ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [CVE-2023-26115] Bump `word-wrap` from `1.2.3` to `1.2.4` ([#4589](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4589)) - Bump `node-sass` to a version that uses a newer `libsass` ([#4649](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4649)) - [CVE-2019-11358] Bump version of tinygradient from 0.4.3 to 1.1.5 ([#4742](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4742)) +- [CVE-2021-3520] Bump `lmdb` from `2.8.0` to `2.8.5` ([#4804](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4804)) +- Remove examples and other unwanted artifacts from installed dependencies ([#4896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4896)) ### ๐Ÿ“ˆ Features/Enhancements @@ -24,9 +26,18 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add resource ID filtering in fetch `augment-vis` obj queries ([#4608](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4608)) - Reduce the amount of comments in compiled CSS ([#4648](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4648)) - [Saved Object Service] Customize saved objects service status ([#4696](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4696)) +- Remove minimum constraint on opensearch hosts to allow empty host ([#4701](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4701)) - [Discover] Update styles to compatible with OUI `next` theme ([#4644](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4644)) +- [Home] Add modal to introduce the `next` theme ([#4715](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4715)) +- [Home] Add new theme sample dashboard screenshots ([#4906](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4906)) - Remove visualization editor sidebar background ([#4719](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4719)) - [Vis Colors] Remove customized colors from sample visualizations and dashboards ([#4741](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4741)) +- [Vis Colors] Update color mapper to prioritize unique colors per visualization rather than across entire dashboard ([#4890](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4890)) +- [Decouple] Allow plugin manifest config to define semver compatible OpenSearch plugin and verify if it is installed on the cluster([#4612](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4612)) +- [Advanced Settings] Consolidate settings into new "Appearance" category and add category IDs ([#4845](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4845)) +- Adds Data explorer framework and implements Discover using it ([#4806](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4806)) +- [Theme] Use themes' definitions to render the initial view ([#4936](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4936/)) +- [Theme] Make `next` theme the default ([#4854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4854/)) ### ๐Ÿ› Bug Fixes @@ -34,7 +45,14 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Cleanup unused url ([#3847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3847)) - Fix Node.js download link ([#4556](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4556)) - [TSVB, Dashboards] Fix inconsistent dark mode code editor themes ([#4609](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4609)) +- [Table Visualization] Fix width of multiple tables when rendered in column view ([#4638](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4638)) - [Legacy Maps] Fix dark mode style overrides ([#4658](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4658)) +- [BUG] Fix management overview page duplicate rendering ([#4636](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4636)) +- [Table Vis] Fix filter actions on data table vis cells ([#4837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4837)) +- Fix broken app when management is turned off ([#4891](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4891)) +- Correct the generated path for downloading plugins by their names on Windows ([#4953](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4953)) +- [BUG] Fix buildPointSeriesData unit test fails due to local timezone ([#4992](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4992)) +- [BUG][Data Explorer][Discover] Fix total hits issue for no time based data ([#5087](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5087)) ### ๐Ÿšž Infrastructure @@ -50,11 +68,14 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### ๐Ÿ›  Maintenance +- Remove angular html extractor ([#4680](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4680)) - Removes `minimatch` manual resolution ([#3019](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3019)) - Upgrade `vega-lite` dependency from `4.17.0` to `^5.6.0` ([#3076](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3076)). Backwards-compatible version included in v2.5.0 release. - Bump `js-yaml` from `3.14.0` to `4.1.0` ([#3770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3770)) - [@osd/pm] Automate multi-target bootstrap and build ([#4650](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4650)) - Update webpack environment targets ([#4649](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4649)) +- Add @curq as maintainer ([#4760](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4760)) +- Bump `oui` to `1.3.0` ([#4941](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4941)) ### ๐Ÿช› Refactoring @@ -69,9 +90,12 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Migrate `/lib/autocomplete/` module to TypeScript ([#4148](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4148)) - [Console] Migrate `/lib/!autocomplete/` module to TypeScript ([#4150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4150)) - [Dashboard] Restructure the `Dashboard` plugin folder to be more cohesive with the project ([#4575](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4575)) +- Refactor logo usage to centralize and optimize assets and improve tests ([#4702](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4702)) ### ๐Ÿ”ฉ Tests +- [Tests] Add BWC tests for 2.9 and 2.10 ([#4762](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4762)) + ## [1.3.12 - 2023-08-10](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/1.3.12) ### ๐Ÿ›ก Security @@ -126,6 +150,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Enable plugins to augment visualizations with additional data and context ([#4361](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4361)) - Dashboard De-Angularization ([#4502](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4502)) - New management overview page and rename stack management to dashboard management ([#4287](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4287)) +- [Console] Add support for JSON with long numerals ([#4562](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4562)) - [Vis Augmenter] Update base vis height in view events flyout ([#4535](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4535)) - [Dashboard De-Angular] Add more unit tests for utils folder ([#4641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4641)) - [Dashboard De-Angular] Add unit tests for dashboard_listing and dashboard_top_nav ([#4640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4640)) @@ -145,6 +170,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Upgrade the backport workflow ([#4343](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4343)) - [Lint] Add custom stylelint rules and config to prevent unintended style overrides ([#4290](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4290)) - [Lint] Add stylelint rule to define properties that are restricted from being used ([#4374](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4374)) +- [Lint] Add stylelint rule to define values that are restricted from being used ([#4413](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4413)) - [Lint] Add typing to Stylelint rules ([#4392](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4392)) - [CI] Split build and verify into parallel jobs ([#4467](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4467)) @@ -176,6 +202,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Migrate `/lib/autocomplete/` module to TypeScript ([#4148](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4148)) - [Dashboard] Restructure the `Dashboard` plugin folder to be more cohesive with the project ([#4575](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4575)) - [Chrome] Remove breadcrumb style overrrides ([#4621](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4621)) +- Replace tinymath with math.js ([#4492](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4492)) ### ๐Ÿ”ฉ Tests @@ -304,6 +331,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [VisBuilder] Add metric to metric, bucket to bucket aggregation persistence ([#3495](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3495)) - [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) +- [Console] Add support for exporting and restoring commands in Dev Tools ([#3810](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3810)) ### ๐Ÿ› Bug Fixes diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index ac26f4bcd8b2..24c7851c21eb 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -64,15 +64,17 @@ If it's the only version of node installed, it will automatically be set to the #### Install `yarn` -Take a look at the [latest Yarn release](https://github.com/yarnpkg/berry/releases/latest), note the version number, and run: +OpenSearch Dashboards is set up using yarn, which can be installed through corepack. To install yarn, run: ```bash +$ # Update corepack to the latest version $ npm i -g corepack -$ corepack prepare yarn@ --activate +$ # Install the correct version of yarn +$ corepack install ``` -(See the [Yarn installation documentation](https://yarnpkg.com/getting-started/install) for more information.) +(See the [corepack documentation](https://github.com/nodejs/corepack#-corepack) for more information.) ### Fork and clone OpenSearch Dashboards diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 1deb190bdd19..a7ee5ce2fcdc 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -20,6 +20,7 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Tao Liu | [Flyingliuhub](https://github.com/Flyingliuhub) | Amazon | | Zilong Xia | [ZilongX](https://github.com/ZilongX) | Amazon | | Matt Provost | [BSFishy](https://github.com/BSFishy) | Amazon | +| Sirazh Gabdullin | [curq](https://github.com/curq) | External contributor | ## Emeritus diff --git a/README.md b/README.md index 70bc8200f61c..5bafaf4c7c1a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Set up your [OpenSearch Dashboards development environment](ttps://github.com/op * [Project Website](https://opensearch.org/) * [Downloads](https://opensearch.org/downloads.html) * [Documentation](https://opensearch.org/docs/) -* Need help? See the [communication guide](COMMUNICATION.md) for various options +* Need help? See the [communication guide](COMMUNICATIONS.md) for various options * [Project Principles](https://opensearch.org/#principles) * [Developer Guide](DEVELOPER_GUIDE.md) * [Contributing to OpenSearch](CONTRIBUTING.md) @@ -53,4 +53,4 @@ Copyright OpenSearch Contributors. See [NOTICE](NOTICE.txt) for details. [codecov-badge]: https://codecov.io/gh/opensearch-project/OpenSearch-Dashboards/branch/main/graphs/badge.svg [codecov-link]: https://app.codecov.io/gh/opensearch-project/OpenSearch-Dashboards [link-checker-badge]: https://github.com/opensearch-project/OpenSearch-Dashboards/actions/workflows/links_checker.yml/badge.svg -[link-checker-link]: https://github.com/opensearch-project/OpenSearch-Dashboards/actions/workflows/links_checker.yml \ No newline at end of file +[link-checker-link]: https://github.com/opensearch-project/OpenSearch-Dashboards/actions/workflows/links_checker.yml diff --git a/bwctest.sh b/bwctest.sh index f85764f9f68b..89030e38c192 100755 --- a/bwctest.sh +++ b/bwctest.sh @@ -13,7 +13,7 @@ set -e -DEFAULT_VERSIONS="osd-2.0.0,osd-2.1.0,osd-2.2.0,osd-2.3.0,osd-2.4.0,osd-2.5.0,osd-2.6.0,osd-2.7.0" +DEFAULT_VERSIONS="osd-2.0.0,osd-2.1.0,osd-2.2.0,osd-2.3.0,osd-2.4.0,osd-2.5.0,osd-2.6.0,osd-2.7.0,osd-2.8.0,osd-2.9.0" function usage() { echo "" diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 73f31233a783..9c6c040433b4 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -32,6 +32,12 @@ # The default application to load. #opensearchDashboards.defaultAppId: "home" +# Set the value to true to disable the welcome screen +#home.disableWelcomeScreen: false + +# Set the value to true to disable the new theme introduction modal +#home.disableNewThemeModal: false + # Setting for an optimized healthcheck that only uses the local OpenSearch node to do Dashboards healthcheck. # This settings should be used for large clusters or for clusters with ingest heavy nodes. # It allows Dashboards to only healthcheck using the local OpenSearch node rather than fan out requests across all nodes. @@ -267,4 +273,3 @@ # Set the value of this setting to true to enable plugin augmentation on Dashboard # vis_augmenter.pluginAugmentationEnabled: true - diff --git a/cypress/test-data/without-security/osd-2.8.0.tar.gz b/cypress/test-data/without-security/osd-2.8.0.tar.gz new file mode 100644 index 000000000000..b0f95eb5894d Binary files /dev/null and b/cypress/test-data/without-security/osd-2.8.0.tar.gz differ diff --git a/cypress/test-data/without-security/osd-2.9.0.tar.gz b/cypress/test-data/without-security/osd-2.9.0.tar.gz new file mode 100644 index 000000000000..a1a67d05737d Binary files /dev/null and b/cypress/test-data/without-security/osd-2.9.0.tar.gz differ diff --git a/package.json b/package.json index 190da4d3c6c5..1e66bbfbd290 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bugs": { "url": "http://github.com/opensearch-project/OpenSearch-Dashboards/issues" }, + "packageManager": "yarn@1.22.19", "opensearchDashboards": { "clean": { "extraPatterns": [ @@ -40,6 +41,7 @@ "author": "opensearch-project", "scripts": { "preinstall": "scripts/use_node ./preinstall_check", + "postinstall": "scripts/use_node scripts/postinstall", "osd": "scripts/use_node scripts/osd", "opensearch": "scripts/use_node scripts/opensearch", "test": "grunt test", @@ -122,7 +124,7 @@ "dependencies": { "@aws-crypto/client-node": "^3.1.1", "@elastic/datemath": "5.0.3", - "@elastic/eui": "npm:@opensearch-project/oui@1.3.0-alpha.2", + "@elastic/eui": "npm:@opensearch-project/oui@1.3.0", "@elastic/good": "^9.0.1-kibana3", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "2.0.0", @@ -138,7 +140,7 @@ "@hapi/podium": "^4.1.3", "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", - "@opensearch-project/opensearch": "^2.2.0", + "@opensearch-project/opensearch": "^2.3.1", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", "@osd/apm-config-loader": "1.0.0", @@ -192,6 +194,7 @@ "json-stringify-safe": "5.0.1", "lodash": "^4.17.21", "lru-cache": "^4.1.5", + "mathjs": "^11.8.2", "minimatch": "^3.0.4", "moment": "^2.24.0", "moment-timezone": "^0.5.27", @@ -218,7 +221,6 @@ "symbol-observable": "^1.2.0", "tar": "^6.1.11", "tinygradient": "^1.1.5", - "tinymath": "1.2.1", "tslib": "^2.0.0", "type-detect": "^4.0.8", "uuid": "3.3.2", diff --git a/packages/osd-dev-utils/src/serializers/flat_object_serializer.ts b/packages/osd-dev-utils/src/serializers/flat_object_serializer.ts new file mode 100644 index 000000000000..e3dec842ad9f --- /dev/null +++ b/packages/osd-dev-utils/src/serializers/flat_object_serializer.ts @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const walk = (value: any, path: string[] = [], collector: string[] = []) => { + let objValue; + switch (Object.prototype.toString.call(value)) { + case '[object Map]': + case '[object WeakMap]': + // Turn into an Object so it can be iterated + objValue = Object.fromEntries(value); + break; + + case '[object Set]': + case '[object WeakSet]': + // Turn into an Array so it can be iterated + objValue = Array.from(value); + break; + + case '[object Object]': + case '[object Array]': + objValue = value; + break; + + case '[object RegExp]': + case '[object Function]': + case '[object Date]': + case '[object Boolean]': + case '[object Number]': + case '[object Symbol]': + case '[object Error]': + collector.push(`${path.join('.')} = ${value.toString()}`); + break; + + case '[object Null]': + collector.push(`${path.join('.')} = null`); + break; + + case '[object Undefined]': + collector.push(`${path.join('.')} = undefined`); + break; + + case '[object String]': + collector.push(`${path.join('.')} = ${JSON.stringify(value)}`); + break; + + case '[object BigInt]': + collector.push(`${path.join('.')} = ${value.toString()}n`); + break; + + default: + // if it is a TypedArray, turn it into an array + if (value instanceof Object.getPrototypeOf(Uint8Array)) { + objValue = Array.from(value); + } + } + + // If objValue is set, it is an Array or Object that can be iterated; else bail. + if (!objValue) return collector; + + if (Array.isArray(objValue)) { + objValue.forEach((v, i) => { + walk(v, [...path, i.toString()], collector); + }); + } else { + // eslint-disable-next-line guard-for-in + for (const key in objValue) { + walk(objValue[key], [...path, key], collector); + } + } + + return collector; +}; + +/** + * The serializer flattens objects into dotified key-value pairs, each on a line, and + * sorts them to aid in diff-ing. + * + * Example: + * { K: ["a", "b", { X: 1n }], Y: 1} + * + * Serialized: + * K.0 = "a" + * K.1 = "b" + * K.2.X = 1n + * Y = 1 + */ +export const flatObjectSerializer = { + test: (value: any) => + ['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(value)), + serialize: (value: any) => walk(value).sort().join('\n'), +}; diff --git a/packages/osd-dev-utils/src/serializers/index.ts b/packages/osd-dev-utils/src/serializers/index.ts index 2755a5a79147..e31ef04898bc 100644 --- a/packages/osd-dev-utils/src/serializers/index.ts +++ b/packages/osd-dev-utils/src/serializers/index.ts @@ -34,3 +34,4 @@ export * from './recursive_serializer'; export * from './any_instance_serizlizer'; export * from './replace_serializer'; export * from './strip_promises_serizlizer'; +export * from './flat_object_serializer'; diff --git a/packages/osd-opensearch-archiver/package.json b/packages/osd-opensearch-archiver/package.json index 1c036dc10c50..4246817d4daf 100644 --- a/packages/osd-opensearch-archiver/package.json +++ b/packages/osd-opensearch-archiver/package.json @@ -12,7 +12,8 @@ }, "dependencies": { "@osd/dev-utils": "1.0.0", - "@opensearch-project/opensearch": "^2.2.0" + "@osd/std": "1.0.0", + "@opensearch-project/opensearch": "^2.3.1" }, "devDependencies": {} } diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index 44404a9ae5a3..1675b0ef134a 100644 --- a/packages/osd-opensearch/package.json +++ b/packages/osd-opensearch/package.json @@ -12,7 +12,7 @@ "osd:watch": "../../scripts/use_node scripts/build --watch" }, "dependencies": { - "@opensearch-project/opensearch": "^2.2.0", + "@opensearch-project/opensearch": "^2.3.1", "@osd/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/packages/osd-optimizer/package.json b/packages/osd-optimizer/package.json index aa9fae9b0d9f..d1e0edbe59e6 100644 --- a/packages/osd-optimizer/package.json +++ b/packages/osd-optimizer/package.json @@ -35,7 +35,6 @@ "rxjs": "^6.5.5", "source-map-support": "^0.5.19", "terser-webpack-plugin": "^2.1.2", - "tinymath": "1.2.1", "watchpack": "^2.1.1", "webpack-merge": "^4.2.2" }, diff --git a/packages/osd-optimizer/src/worker/webpack.config.ts b/packages/osd-optimizer/src/worker/webpack.config.ts index 66e726961879..a66ad06de032 100644 --- a/packages/osd-optimizer/src/worker/webpack.config.ts +++ b/packages/osd-optimizer/src/worker/webpack.config.ts @@ -235,6 +235,21 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: }, }, }, + { + test: /\.js$/, + /* reactflow and some of its dependencies don't have es5 builds + * so we need to build from source and transpile for webpack v4 + */ + include: /node_modules[\\/]@?reactflow/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + envName: worker.dist ? 'production' : 'development', + presets: [BABEL_PRESET_PATH], + }, + }, + }, { test: /\.(html|md|txt|tmpl)$/, use: { @@ -248,7 +263,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: extensions: ['.js', '.ts', '.tsx', '.json'], mainFields: ['browser', 'main'], alias: { - tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), core_app_image_assets: Path.resolve(worker.repoRoot, 'src/core/public/core_app/images'), }, }, diff --git a/packages/osd-std/README.md b/packages/osd-std/README.md index 3730735cb5a4..24f888979d25 100644 --- a/packages/osd-std/README.md +++ b/packages/osd-std/README.md @@ -1,3 +1,73 @@ # `@osd/std` โ€” OpenSearch Dashboards standard library -This package is a set of utilities that can be used both on server-side and client-side. \ No newline at end of file +This package is a set of utilities that can be used both on server-side and client-side. + +## API + +#### `assertNever` + +Can be used in switch statements to ensure we perform exhaustive checks. + +#### `deepFreeze` + +Apply `Object.freeze` to a value recursively and convert the return type to `Readonly` variant recursively. + +#### `get` + +Retrieve the value for the specified path of an object. + +#### `getFlattenedObject` + +Flatten a deeply nested object to a map of dot-separated paths, pointing to all of its primitive values and arrays. + +#### `stringify` and `parse` + +Drop-in replacement for `JSON.stringify` and `JSON.parse`, capable of handling long numerals and `BigInt` values. + +#### `mapToObject` + +Convert a map to an object. + +#### `mapValuesOfMap` + +Create a new `Map` populated with the results of calling a provided function on every element in the input `Map`. + +#### `groupIntoMap` + +Group elements of an `Array` into a `Map` based on a provided function. + +#### `merge` + +Deeply merge two objects, omitting undefined values, and not deeply merging arrays. + +#### `pick` + +Create a new `Object` of specified keys and their values from an input `Object`. + +#### `withTimeout` + +Apply a `timeout` duration to a `Promise` before throwing an `Error` with the provided message. + +#### `firstValueFrom` and `lastValueFrom` + +Get a `Promise` that resolves as soon as the first or last value arrives from an observable. + +#### `unset` + +Unset a (potentially nested) key from given object. + +#### `modifyUrl` + +Get an `Object` resulting from applying a provided function to the meaningful parts of a URL. + +#### `isRelativeUrl` + +Determine if a url is relative. + +#### `getUrlOrigin` + +Get the origin URL of a provided URL. + +#### `validateObject` + +Deeply validate that an `Object` does not contain any `__proto__` or `constructor.prototype` keys, or circular references. \ No newline at end of file diff --git a/packages/osd-std/src/__snapshots__/json.test.ts.snap b/packages/osd-std/src/__snapshots__/json.test.ts.snap new file mode 100644 index 000000000000..42854db1dd78 --- /dev/null +++ b/packages/osd-std/src/__snapshots__/json.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`json can apply a replacer and spaces values while stringifying BigInts 1`] = ` +"{ + \\"\\\\\\": 18014398509481982\\": \\"\\", + \\"positive\\": 54043195528445946, + \\"negative\\": -54043195528445946, + \\"array\\": [ + -54043195528445946, + 54043195528445946, + [ + \\"]]>\\" + ] + ], + \\"number\\": \\"5d9d89cc6b13\\" +}" +`; + +exports[`json can handle BigInt values while stringifying 1`] = `"{\\"\\\\\\": 18014398509481982\\":\\"[ -18014398509481982, 18014398509481982 ]\\",\\"positive\\":18014398509481982,\\"negative\\":-18014398509481982,\\"array\\":[-18014398509481982,18014398509481982],\\"number\\":102931203123987}"`; diff --git a/packages/osd-std/src/index.ts b/packages/osd-std/src/index.ts index 0b3c65d8cc04..170c819a2b0a 100644 --- a/packages/osd-std/src/index.ts +++ b/packages/osd-std/src/index.ts @@ -42,3 +42,4 @@ export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; export { validateObject } from './validate_object'; export * from './rxjs_7'; +export { parse, stringify } from './json'; diff --git a/packages/osd-std/src/json.test.ts b/packages/osd-std/src/json.test.ts new file mode 100644 index 000000000000..33abd71d91d2 --- /dev/null +++ b/packages/osd-std/src/json.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { stringify, parse } from './json'; + +describe('json', () => { + it('can parse', () => { + const input = { + a: [ + { A: 1 }, + { B: '2' }, + { C: [1, 2, 3, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'] }, + ], + b: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + c: { + i: {}, + ii: [], + iii: '', + iv: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + }; + const result = parse(JSON.stringify(input)); + expect(result).toEqual(input); + }); + + it('can stringify', () => { + const input = { + a: [ + { A: 1 }, + { B: '2' }, + { C: [1, 2, 3, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'] }, + ], + b: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + c: { + i: {}, + ii: [], + iii: '', + iv: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + }; + const result = stringify(input); + expect(result).toEqual(JSON.stringify(input)); + }); + + it('can apply a reviver while parsing', () => { + const input = { + A: 255, + B: { + i: [[]], + ii: 'Lorem ipsum', + iii: {}, + rand: Math.random(), + }, + }; + const text = JSON.stringify(input); + function reviver(this: any, key: string, val: any) { + if (Array.isArray(val) && toString.call(this) === '[object Object]') this._hasArrays = true; + else if (typeof val === 'string') val = ``; + else if (typeof val === 'number') val = val.toString(16); + else if (toString.call(this) === '[object Object]' && key === 'rand' && val === input.B.rand) + this._found = true; + return val; + } + + expect(parse(text, reviver)).toEqual(JSON.parse(text, reviver)); + }); + + it('can apply a replacer and spaces while stringifying', () => { + const input = { + A: 255, + B: { + i: [[]], + ii: 'Lorem ipsum', + iii: {}, + rand: Math.random(), + }, + }; + + function replacer(this: any, key: string, val: any) { + if (Array.isArray(val) && val.length === 0) val.push(''); + else if (typeof val === 'string') val = ``; + else if (typeof val === 'number') val = val.toString(16); + else if (toString.call(this) === '[object Object]' && key === 'rand' && val === input.B.rand) + val = 1; + return val; + } + + expect(stringify(input, replacer, 2)).toEqual(JSON.stringify(input, replacer, 2)); + }); + + it('can handle long numerals while parsing', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + const text = + `{` + + // The space before and after the values, and the lack of spaces before comma are intentional + `"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` + + `"positive": ${longPositive.toString()}, ` + + `"array": [ ${longNegative.toString()}, ${longPositive.toString()} ], ` + + `"negative": ${longNegative.toString()},` + + `"number": 102931203123987` + + `}`; + + const result = parse(text); + expect(result.positive).toBe(longPositive); + expect(result.negative).toBe(longNegative); + expect(result.array).toEqual([longNegative, longPositive]); + expect(result['":' + longPositive]).toBe( + `[ ${longNegative.toString()}, ${longPositive.toString()} ]` + ); + expect(result.number).toBe(102931203123987); + }); + + it('can handle BigInt values while stringifying', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + const input = { + [`": ${longPositive}`]: `[ ${longNegative.toString()}, ${longPositive.toString()} ]`, + positive: longPositive, + negative: longNegative, + array: [longNegative, longPositive], + number: 102931203123987, + }; + + expect(stringify(input)).toMatchSnapshot(); + }); + + it('can apply a reviver on long numerals while parsing', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + const text = + `{` + + // The space before and after the values, and the lack of spaces before comma are intentional + `"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` + + `"positive": ${longPositive.toString()}, ` + + `"array": [ ${longNegative.toString()}, ${longPositive.toString()} ], ` + + `"negative": ${longNegative.toString()},` + + `"number": 102931203123987` + + `}`; + + const reviver = (key: string, val: any) => (typeof val === 'bigint' ? val * 3n : val); + + const result = parse(text, reviver); + expect(result.positive).toBe(longPositive * 3n); + expect(result.negative).toBe(longNegative * 3n); + expect(result.array).toEqual([longNegative * 3n, longPositive * 3n]); + expect(result['":' + longPositive]).toBe( + `[ ${longNegative.toString()}, ${longPositive.toString()} ]` + ); + expect(result.number).toBe(102931203123987); + }); + + it('can apply a replacer and spaces values while stringifying BigInts', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + const input = { + [`": ${longPositive}`]: `[ ${longNegative.toString()}, ${longPositive.toString()} ]`, + positive: longPositive, + negative: longNegative, + array: [longNegative, longPositive, []], + number: 102931203123987, + }; + + function replacer(this: any, key: string, val: any) { + if (typeof val === 'bigint') val = val * 3n; + else if (Array.isArray(val) && val.length === 0) val.push(''); + else if (typeof val === 'string') val = ``; + else if (typeof val === 'number') val = val.toString(16); + return val; + } + + expect(stringify(input, replacer, 4)).toMatchSnapshot(); + }); +}); diff --git a/packages/osd-std/src/json.ts b/packages/osd-std/src/json.ts new file mode 100644 index 000000000000..7c619dcd1656 --- /dev/null +++ b/packages/osd-std/src/json.ts @@ -0,0 +1,321 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* In JavaScript, a `Number` is a 64-bit floating-point value which can store 16 digits. However, the + * serializer and deserializer will need to cater to numeric values generated by other languages which + * can have up to 19 digits. Native JSON parser and stringifier, incapable of handling the extra + * digits, corrupt the values, making them unusable. + * + * To work around this limitation, the deserializer converts long sequences of digits into strings and + * marks them before applying the parser. During the parsing, string values that begin with the mark + * are converted to `BigInt` values. + * Similarly, during stringification, the serializer converts `BigInt` values to marked strings and + * when done, it replaces them with plain numerals. + * + * `Number.MAX_SAFE_INTEGER`, 9,007,199,254,740,991, is the largest number that the native methods can + * parse and stringify, and any numeral greater than that would need to be translated using the + * workaround; all 17-digits or longer and only tail-end of the 16-digits need translation. It would + * be unfair to all the 16-digit numbers if the translation applied to `\d{16,}` only to cover the + * less than 10%. Hence, a RegExp is created to only match numerals too long to be a number. + * + * To make the explanation simpler, let's assume that MAX_SAFE_INTEGER is 8921 which has 4 digits. + * Starting from the right, we take each digit onwards, `[-9]`: + * 1) 7922 - 7929: 792[2-9]\d{0} + * 2) 7930 - 7999: 79[3-9]\d{1} + * 9) 9 + 1 = 10 which results in a rollover; no need to do anything. + * 8) 9000 - 9999: [9-9]\d{3} + * Finally we add anything 5 digits or longer: `\d{5,} + * + * Note: A better solution would use AST but considering its performance penalty, RegExp is the next + * best thing. + */ +const maxIntAsString = String(Number.MAX_SAFE_INTEGER); +const maxIntLength = maxIntAsString.length; +// Sub-patterns for each digit +const longNumeralMatcherTokens = [`\\d{${maxIntAsString.length + 1},}`]; +for (let i = 0; i < maxIntLength; i++) { + if (maxIntAsString[i] !== '9') { + longNumeralMatcherTokens.push( + maxIntAsString.substring(0, i) + + `[${parseInt(maxIntAsString[i], 10) + 1}-9]` + + `\\d{${maxIntLength - i - 1}}` + ); + } +} + +/* The matcher that looks for `": , ...}` and `[..., , ...]` + * + * The pattern starts by looking for `":` not immediately preceded by a `\`. That should be + * followed by any of the numeric sub-patterns. A comma, end of an array, end of an object, or + * the end of the input are the only acceptable elements after it. + * + * Note: This RegExp can result in false-positive hits on the likes of `{"key": "[ ]"}` and + * those are cleaned out during parsing. + */ +const longNumeralMatcher = new RegExp( + `((?:\\[|,|(? { + // coverage:ignore-line + if (!length || length < 0) return []; + const choices = []; + const arr = markerChars; + const arrLength = arr.length; + const temp = Array(length); + + (function fill(pos, start) { + if (pos === length) return choices.push(temp.join('')); + + for (let i = start; i < arrLength; i++) { + temp[pos] = arr[i]; + fill(pos + 1, i); + } + })(0, 0); + + return choices; +}; + +/* Experiments with different combinations of various lengths, until one is found to not be in + * the input string. + */ +const getMarker = (text: string): { marker: string; length: number } => { + let marker; + let length = 0; + do { + length++; + getMarkerChoices(length).some((markerChoice) => { + if (text.indexOf(markerChoice) === -1) { + marker = markerChoice; + return true; + } + }); + } while (!marker); + + return { + marker, + length, + }; +}; + +const parseStringWithLongNumerals = ( + text: string, + reviver?: ((this: any, key: string, value: any) => any) | null +): any => { + const { marker, length } = getMarker(text); + + let hadException; + let obj; + let markedJSON = text.replace(longNumeralMatcher, `$1"${marker}$2"$3`); + const markedValueMatcher = new RegExp(`^${marker}-?\\d+$`); + + /* Convert marked values to BigInt values. + * The `startsWith` is purely for performance, to avoid running `test` if not needed. + */ + const convertMarkedValues = (val: any) => + typeof val === 'string' && val.startsWith(marker) && markedValueMatcher.test(val) + ? BigInt(val.substring(length)) + : val; + + /* For better performance, instead of testing for existence of `reviver` on each value, two almost + * identical functions are used. + */ + const parseMarkedText = reviver + ? (markedText: string) => + JSON.parse(markedText, function (key, val) { + return reviver.call(this, key, convertMarkedValues(val)); + }) + : (markedText: string) => JSON.parse(markedText, (key, val) => convertMarkedValues(val)); + + /* RegExp cannot replace AST and the process of marking adds quotes. So, any false-positive hit + * will make the JSON string unparseable. + * + * To find those instances, we try to parse and watch for the location of any errors. If an error + * is caused by the marking, we remove that single marking and try again. + */ + do { + try { + hadException = false; + obj = parseMarkedText(markedJSON); + } catch (e) { + hadException = true; + /* There are two types of exception objects that can be raised: + * 1) a proper object with lineNumber and columnNumber which we can use + * 2) a textual message with the position that we need to parse + */ + let { lineNumber, columnNumber } = e; + if (!lineNumber || !columnNumber) { + const match = e?.message?.match?.(/^Unexpected token.*at position (\d+)$/); + if (match) { + lineNumber = 1; + // The position is zero-indexed; adding 1 to normalize it for the -2 that comes later + columnNumber = parseInt(match[1], 10) + 1; + } + } + + if (lineNumber < 1 || columnNumber < 2) { + /* The problem is not with this replacement. + * Note: This will never happen because the outer parse would have already thrown. + */ + // coverage:ignore-line + throw e; + } + + /* We need to skip e.lineNumber - 1 number of `\n` occurrences. + * Then, we need to go to e.columnNumber - 2 to look for `"\d+"`; we need to `-1` to + * account for the quote but an additional `-1` is needed because columnNumber starts from 1. + */ + const re = new RegExp( + `^((?:.*\\n){${lineNumber - 1}}[^\\n]{${columnNumber - 2}})"${marker}(-?\\d+)"` + ); + if (!re.test(markedJSON)) { + /* The exception is not caused by adding the marker. + * Note: This will never happen because the outer parse would have already thrown. + */ + // coverage:ignore-line + throw e; + } + + // We have found a bad replacement; let's remove it. + markedJSON = markedJSON.replace(re, '$1$2'); + } + } while (hadException); + + return obj; +}; + +const stringifyObjectWithBigInts = ( + obj: any, + candidate: string, + replacer?: ((this: any, key: string, value: any) => any) | null, + space?: string | number +): string => { + const { marker } = getMarker(candidate); + + /* The matcher that looks for "" + * Because we have made sure that `marker` was never present in the original object, we can + * carelessly assume every "" is due to our marking. + */ + const markedBigIntMatcher = new RegExp(`"${marker}(-?\\d+)"`, 'g'); + + /* Convert BigInt values to a string and mark them. + * Can't be bothered with Number values outside the safe range because they are already corrupted. + * + * For better performance, instead of testing for existence of `replacer` on each value, two almost + * identical functions are used. + */ + const addMarkerToBigInts = replacer + ? function (this: any, key: string, val: any) { + // replacer is called before marking because marking changes the type + const newVal = replacer.call(this, key, val); + return typeof newVal === 'bigint' ? `${marker}${newVal.toString()}` : newVal; + } + : (key: string, val: any) => (typeof val === 'bigint' ? `${marker}${val.toString()}` : val); + + return ( + JSON.stringify(obj, addMarkerToBigInts, space) + // Replace marked substrings with just the numerals + .replace(markedBigIntMatcher, '$1') + ); +}; + +export const stringify = ( + obj: any, + replacer?: ((this: any, key: string, value: any) => any) | null, + space?: string | number +): string => { + let text; + let numeralsAreNumbers = true; + /* For better performance, instead of testing for existence of `replacer` on each value, two almost + * identical functions are used. + * + * Note: Converting BigInt values to numbers, `Number()` is much faster that `parseInt()`. Since we + * check the `type`, it is safe to just use `Number()`. + */ + const checkForBigInts = replacer + ? function (this: any, key: string, val: any) { + if (typeof val === 'bigint') { + numeralsAreNumbers = false; + return replacer.call(this, key, Number(val)); + } + return replacer.call(this, key, val); + } + : (key: string, val: any) => { + if (typeof val === 'bigint') { + numeralsAreNumbers = false; + return Number(val); + } + return val; + }; + + /* While this is a check for possibly having BigInt values, if none were found, the results is + * sufficient to fulfill the purpose of the function. However, if BigInt values were found, we will + * use `stringifyObjectWithBigInts` to do this again. + * + * The goal was not to punish every object that doesn't have a BigInt with the more expensive + * `stringifyObjectWithBigInts`. Those with BigInt values are also not unduly burdened because we + * still need it in its string form to find a suitable marker. + */ + text = JSON.stringify(obj, checkForBigInts, space); + + if (!numeralsAreNumbers) { + text = stringifyObjectWithBigInts(obj, text, replacer, space); + } + + return text; +}; + +export const parse = ( + text: string, + reviver?: ((this: any, key: string, value: any) => any) | null +): any => { + let obj; + let numeralsAreNumbers = true; + const inspectValueForLargeNumerals = (val: any) => { + if ( + numeralsAreNumbers && + typeof val === 'number' && + (val < Number.MAX_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) + ) { + numeralsAreNumbers = false; + } + + // This function didn't have to have a return value but having it makes the rest cleaner + return val; + }; + + /* For better performance, instead of testing for existence of `reviver` on each value, two almost + * identical functions are used. + */ + const checkForLargeNumerals = reviver + ? function (this: any, key: string, val: any) { + return inspectValueForLargeNumerals(reviver.call(this, key, val)); + } + : (key: string, val: any) => inspectValueForLargeNumerals(val); + + /* While this is a check for possibly having BigInt values, if none were found, the results is + * sufficient to fulfill the purpose of the function. However, if BigInt values were found, we will + * use `stringifyObjectWithBigInts` to do this again. + * + * The goal was not to punish every object that doesn't have a BigInt with the more expensive + * `stringifyObjectWithBigInts`. Those with BigInt values are also not unduly burdened because we + * still need it in its string form to find a suitable marker. + */ + obj = JSON.parse(text, checkForLargeNumerals); + + if (!numeralsAreNumbers) { + obj = parseStringWithLongNumerals(text, reviver); + } + + return obj; +}; diff --git a/packages/osd-stylelint-config/.stylelintrc.js b/packages/osd-stylelint-config/.stylelintrc.js index aa06f1a72f08..4f780d112e43 100644 --- a/packages/osd-stylelint-config/.stylelintrc.js +++ b/packages/osd-stylelint-config/.stylelintrc.js @@ -15,26 +15,34 @@ module.exports = { ], rules: { - '@osd/stylelint/no_restricted_properties': [ + '@osd/stylelint/no_modifying_global_selectors': [ { - config: "./../../../osd-stylelint-config/config/restricted_properties.json" + config: "./../../../osd-stylelint-config/config/global_selectors.json" }, { severity: "error" } ], - '@osd/stylelint/no_modifying_global_selectors': [ + '@osd/stylelint/no_custom_colors': [ { - config: "./../../../osd-stylelint-config/config/global_selectors.json" + config: './../../../osd-stylelint-config/config/colors.json' + }, + ], + '@osd/stylelint/no_restricted_properties': [ + { + config: "./../../../osd-stylelint-config/config/restricted_properties.json" }, { severity: "error" } ], - '@osd/stylelint/no_custom_colors': [ + '@osd/stylelint/no_restricted_values': [ { - config: './../../../osd-stylelint-config/config/colors.json' + config: "./../../../osd-stylelint-config/config/restricted_values.json" }, - ] + { + severity: "error" + } + ], } } diff --git a/packages/osd-stylelint-config/config/global_selectors.json b/packages/osd-stylelint-config/config/global_selectors.json index 19451066878c..760717c8dab5 100644 --- a/packages/osd-stylelint-config/config/global_selectors.json +++ b/packages/osd-stylelint-config/config/global_selectors.json @@ -23,8 +23,9 @@ "src/plugins/vis_builder/public/application/components/searchable_dropdown.scss", "src/plugins/vis_builder/public/application/components/side_nav.scss", "packages/osd-ui-framework/src/components/button/button_group/_button_group.scss", - "src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss", - "src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss" + "src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss", + "src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_open.scss", + "src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.scss" ] } } diff --git a/packages/osd-stylelint-config/config/restricted_properties.json b/packages/osd-stylelint-config/config/restricted_properties.json index ff79c6fed46b..d229764c8d88 100644 --- a/packages/osd-stylelint-config/config/restricted_properties.json +++ b/packages/osd-stylelint-config/config/restricted_properties.json @@ -1,7 +1,8 @@ { "font-family": { + "explanation": "All \"font-family\" styles should be inherited from OUI themes and components. Remove the rule.", "approved": [ - "src/plugins/discover/public/application/_discover.scss", + "src/plugins/discover_legacy/public/application/_discover.scss", "src/plugins/maps_legacy/public/map/_leaflet_overrides.scss", "src/plugins/maps_legacy/public/map/_legend.scss", "src/plugins/opensearch_dashboards_legacy/public/font_awesome/font_awesome.scss", @@ -11,7 +12,8 @@ "src/plugins/data/public/ui/typeahead/_suggestion.scss", "src/plugins/vis_type_timeseries/public/application/components/_error.scss", "packages/osd-ui-framework/src/components/form/check_box/_check_box.scss", - "src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss" + "src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss", + "src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss" ] } -} +} \ No newline at end of file diff --git a/packages/osd-stylelint-config/config/restricted_values.json b/packages/osd-stylelint-config/config/restricted_values.json new file mode 100644 index 000000000000..2d925bd67e2d --- /dev/null +++ b/packages/osd-stylelint-config/config/restricted_values.json @@ -0,0 +1,25 @@ +{ + "/#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?/": { + "explanation": "All colors should be inherited from the OUI theme, not hard-coded. Either remove the custom color rule or use OUI SASS variables instead.", + "approved": [ + "src/core/public/_variables.scss", + "src/core/public/styles/_ace_overrides.scss", + "packages/osd-ui-framework/src/components/tool_bar/_tool_bar_search.scss", + "packages/osd-ui-framework/src/components/view/_index.scss", + "src/core/public/chrome/ui/header/header_breadcrumbs.scss", + "src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss", + "src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss" + ] + }, + "/(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\\([^)]*\\)/": { + "explanation": "All colors should be inherited from the OUI theme, not hard-coded. Either remove the custom color rule or use OUI SASS variables instead.", + "approved": [ + "src/core/public/styles/_ace_overrides.scss", + "src/plugins/opensearch_dashboards_react/public/markdown/_markdown.scss", + "packages/osd-ui-framework/src/components/info_panel/_info_panel.scss", + "packages/osd-ui-framework/src/components/local_nav/_local_menu.scss", + "packages/osd-ui-framework/src/global_styling/mixins/_shadow.scss", + "packages/osd-ui-framework/src/global_styling/variables/_colors.scss" + ] + } +} diff --git a/packages/osd-stylelint-plugin-stylelint/src/rules/index.ts b/packages/osd-stylelint-plugin-stylelint/src/rules/index.ts index 6dc26fe93f26..21c06b81e9e7 100644 --- a/packages/osd-stylelint-plugin-stylelint/src/rules/index.ts +++ b/packages/osd-stylelint-plugin-stylelint/src/rules/index.ts @@ -9,13 +9,15 @@ * GitHub history for details. */ -import noRestrictedProperties from './no_restricted_properties'; import noCustomColors from './no_custom_colors'; import noModifyingGlobalSelectors from './no_modifying_global_selectors'; +import noRestrictedProperties from './no_restricted_properties'; +import noRestrictedValues from './no_restricted_values'; // eslint-disable-next-line import/no-default-export export default { no_custom_colors: noCustomColors, no_modifying_global_selectors: noModifyingGlobalSelectors, no_restricted_properties: noRestrictedProperties, + no_restricted_values: noRestrictedValues, }; diff --git a/packages/osd-stylelint-plugin-stylelint/src/rules/no_modifying_global_selectors/index.ts b/packages/osd-stylelint-plugin-stylelint/src/rules/no_modifying_global_selectors/index.ts index d58e7819688f..b4bf6ac7da15 100644 --- a/packages/osd-stylelint-plugin-stylelint/src/rules/no_modifying_global_selectors/index.ts +++ b/packages/osd-stylelint-plugin-stylelint/src/rules/no_modifying_global_selectors/index.ts @@ -79,7 +79,10 @@ const ruleFunction: stylelint.Rule = ( } reportInfo.message = messages.expected( - getNotCompliantMessage(`Modifying global selector "${rule.selector}" not allowed.`) + getNotCompliantMessage( + `Modifying the global selector "${rule.selector}" is not allowed.`, + selectorRule.explanation + ) ); report(reportInfo); }); diff --git a/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_properties/index.ts b/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_properties/index.ts index 9da587545b1d..dbe20baac0ae 100644 --- a/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_properties/index.ts +++ b/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_properties/index.ts @@ -73,7 +73,10 @@ const ruleFunction: stylelint.Rule = ( } reportInfo.message = messages.expected( - getNotCompliantMessage(`Usage of property "${decl.prop}" is not allowed.`) + getNotCompliantMessage( + `Specifying the "${decl.prop}" property is not allowed.`, + propertyRule.explanation + ) ); report(reportInfo); }); diff --git a/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_values/index.ts b/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_values/index.ts new file mode 100644 index 000000000000..ae99eacfba13 --- /dev/null +++ b/packages/osd-stylelint-plugin-stylelint/src/rules/no_restricted_values/index.ts @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import stylelint from 'stylelint'; +import { NAMESPACE } from '../..'; +import { + getNotCompliantMessage, + getRuleFromConfig, + getRulesFromConfig, + isValidOptions, + FileBasedConfig, +} from '../../utils'; + +const { ruleMessages, report } = stylelint.utils; + +const ruleName = 'no_restricted_values'; +const messages = ruleMessages(ruleName, { + expected: (message) => `${message}`, +}); + +const ruleFunction: stylelint.Rule = ( + primaryOption: Record, + secondaryOptionObject: Record, + context +) => { + return (postcssRoot, postcssResult) => { + const validOptions = isValidOptions(postcssResult, ruleName, primaryOption); + if (!validOptions) { + return; + } + + const rules: FileBasedConfig = getRulesFromConfig(primaryOption.config); + + const isAutoFixing = Boolean(context.fix); + + postcssRoot.walkDecls((decl) => { + const valueRule = getRuleFromConfig(rules, decl.value); + if (!valueRule) { + return; + } + + let shouldReport = false; + + const file = postcssRoot.source?.input.file; + if (!file) { + return; + } + + const approvedFiles = valueRule.approved; + + const reportInfo = { + ruleName: `${NAMESPACE}/${ruleName}`, + result: postcssResult, + node: decl, + message: '', + }; + + if (approvedFiles) { + shouldReport = !approvedFiles.some((inspectedFile) => { + return file.includes(inspectedFile); + }); + } + + if (shouldReport && isAutoFixing) { + decl.remove(); + return; + } + + if (!shouldReport) { + return; + } + + reportInfo.message = messages.expected( + getNotCompliantMessage( + `Using the value "${decl.value}" is not allowed.`, + valueRule.explanation + ) + ); + report(reportInfo); + }); + }; +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; + +// eslint-disable-next-line import/no-default-export +export default ruleFunction; diff --git a/packages/osd-stylelint-plugin-stylelint/src/utils/get_message.ts b/packages/osd-stylelint-plugin-stylelint/src/utils/get_message.ts index 65e236aaffad..b966f775e4d6 100644 --- a/packages/osd-stylelint-plugin-stylelint/src/utils/get_message.ts +++ b/packages/osd-stylelint-plugin-stylelint/src/utils/get_message.ts @@ -15,4 +15,10 @@ export const getUntrackedMessage = (nodeInfo: { selector: string; prop: string; export const getTrackedMessage = (nodeInfo: { selector: string; prop: string; value: string }) => `Tracked but missing approval: "${nodeInfo.selector}.${nodeInfo.prop}: ${nodeInfo.value}"`; -export const getNotCompliantMessage = (message: string) => `${message}`; +export const getNotCompliantMessage = (message: string, explanation?: string) => { + if (explanation) { + return `${message} ${explanation}`; + } + + return message; +}; diff --git a/packages/osd-stylelint-plugin-stylelint/src/utils/get_rules_from_config.ts b/packages/osd-stylelint-plugin-stylelint/src/utils/get_rules_from_config.ts index cc97d13a030c..5467915cbabb 100644 --- a/packages/osd-stylelint-plugin-stylelint/src/utils/get_rules_from_config.ts +++ b/packages/osd-stylelint-plugin-stylelint/src/utils/get_rules_from_config.ts @@ -13,7 +13,7 @@ import path from 'path'; import { readFileSync } from 'fs'; import { matches } from './matches'; -export type FileBasedConfig = Record; +export type FileBasedConfig = Record; export type ValueBasedConfig = Record< string, Record> diff --git a/packages/osd-ui-framework/package.json b/packages/osd-ui-framework/package.json index 8375cc3b8295..156ccbbdfcd6 100644 --- a/packages/osd-ui-framework/package.json +++ b/packages/osd-ui-framework/package.json @@ -23,7 +23,7 @@ "enzyme-adapter-react-16": "^1.9.1" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.3.0-alpha.2", + "@elastic/eui": "npm:@opensearch-project/oui@1.3.0", "@osd/babel-preset": "1.0.0", "@osd/optimizer": "1.0.0", "comment-stripper": "^0.0.4", diff --git a/packages/osd-ui-shared-deps/package.json b/packages/osd-ui-shared-deps/package.json index 3e73fa0c8575..fca9abd7c537 100644 --- a/packages/osd-ui-shared-deps/package.json +++ b/packages/osd-ui-shared-deps/package.json @@ -10,8 +10,9 @@ }, "dependencies": { "@elastic/charts": "31.1.0", - "@elastic/eui": "npm:@opensearch-project/oui@1.3.0-alpha.2", + "@elastic/eui": "npm:@opensearch-project/oui@1.3.0", "@elastic/numeral": "^2.5.0", + "@opensearch/datemath": "5.0.3", "@osd/i18n": "1.0.0", "@osd/monaco": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/packages/osd-ui-shared-deps/theme.ts b/packages/osd-ui-shared-deps/theme.ts index 45a4f68cfd4b..c803a5e37ef7 100644 --- a/packages/osd-ui-shared-deps/theme.ts +++ b/packages/osd-ui-shared-deps/theme.ts @@ -28,6 +28,7 @@ * under the License. */ +// ToDo: Use `THEME_SOURCES` from `src/core/server/rendering/views/theme` to generate the logic below. import LightTheme from '@elastic/eui/dist/eui_theme_light.json'; const globals: any = typeof window === 'undefined' ? {} : window; @@ -36,7 +37,7 @@ export type Theme = typeof LightTheme; // in the OpenSearch Dashboards app we can rely on this global being defined, but in // some cases (like jest) the global is undefined -export const tag: string = globals.__osdThemeTag__ || 'v7light'; +export const tag: string = globals.__osdThemeTag__ || 'v8light'; export const version = tag.startsWith('v7') ? 7 : 8; export const darkMode = tag.endsWith('dark'); diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.13.md b/release-notes/opensearch-dashboards.release-notes-1.3.13.md new file mode 100644 index 000000000000..8ad93c19ba22 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.13.md @@ -0,0 +1,24 @@ +# Version 1.3.13 Release Notes + +### ๐Ÿ›ก Security + +- [CVE-2019-11358] Bump version of `tinygradient` from `0.4.3` to `1.1.5` ([#4571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4751)) +- [CVE-2023-26136] Bump `word-wrap` from `1.2.3` to `1.2.4` ([#5002](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5002)) +- [CVE-2022-21670] Bump `markdown-it` from `10.0.0` to `12.3.2` ([#5016](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5016)) +- [CVE-2022-33987] Partially fix security issues for `got` by bumping `@elastic/makelogs` from `6.0.0` to `6.1.1` and updating yarn.lock ([#5006](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5006)) +- Bump `yo` from `2.0.6` to `3.1.1` ([#5005]( https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5005)) +- [CVE-2023-0842] Bump `xml2js` from `0.4.22` to `0.6.2` ([#5024](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5024)) + +### ๐Ÿ“ˆ Features/Enhancements + +### ๐Ÿ› Bug Fixes + +### ๐Ÿšž Infrastructure + +### ๐Ÿ“ Documentation + +### ๐Ÿ›  Maintenance + +- [Version] Increment version to 1.3.13 ([#4721](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4721)) +- [Chore] Add company.net to exclusion list in linkchecker ([#4704](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4704)) +- [Chore] Exclude checking dead link in linkchecker ([#4868](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4868)) diff --git a/release-notes/opensearch-dashboards.release-notes-2.10.0.md b/release-notes/opensearch-dashboards.release-notes-2.10.0.md new file mode 100644 index 000000000000..6cdfffdba513 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.10.0.md @@ -0,0 +1,84 @@ +## Version 2.10.0 Release Notes + +### ๐Ÿ›ก Security + +- Bump word-wrap from 1.2.3 to 1.2.4 ([#4589](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4589)) +- Bump version of tinygradient from 0.4.3 to 1.1.5 ([#4742](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4742)) +- Bump lmdb from 2.8.0 to 2.8.5 ([#4804](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4804)) +- Alias and bump mocha ([#4874](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4874)) +- Remove examples and other unwanted artifacts from installed dependencies ([#4896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4896)) + +### ๐Ÿ“ˆ Features/Enhancements + +- [Vis colors] Update legacy mapped colors in charts plugin to use ouiPaletteColorBlind() ([#4398](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4398)) +- [Saved Objects Management] Add new or remove extra tags and styles ([#4069](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4069)) +- Chore (home): Update visual consistency dashboard TSVB colors ([#4501](http://github.com/opensearch-project/OpenSearch-Dashboards/pull/4501)) +- Feature (home): Update visual consistency sample dashboard with more vis ([#4581](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4581)) +- Add resource ID filtering in fetch augment-vis obj queries ([#4608](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4608)) +- Enable theme-switching via Advanced Settings to preview the Next theme ([#4475](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4475)) +- Feat (home): Add remaining vis type examples ([#4619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4619)) +- Feat (Discover): Update styles to be compatible with next theme ([#4644](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4644)) +- Update webpack environment targets ([#4649](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4649)) +- [Table Visualization] Replace div containers with OuiFlex components ([#4272](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4272)) +- Reduce the amount of comments in compiled CSS ([#4648](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4648)) +- Feat (home): Remove color customizations from sample dashboards ([#4741](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4741)) +- Remove visualization editor background ([#4719](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4719)) +- Add saved objects service status api ([#4696](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4696)) +- Allow plugin manifest config to define semver compatible OpenSearch plugin and verify if it is installed on the cluster ([#4612](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4612)) +- Eliminate duplicate dashboard breadcrumb text ID ([#4805](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4805)) +- [@osd/pm] Automate multi-target bootstrap and build ([#4650](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4650)) +- [Home] Add modal to introduce the `next` theme ([#4715](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4715)) +- [Home] Add new theme sample dashboard screenshots ([#4906](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4906)) +- Change color fn used to calculate icon colors for search typeahead suggestions ([#4884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4884)) +- [Next Theme] Make next theme the default ([#4854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4854)) +- [Vis Colors] Update color mapper to prioritize unique colors per vis ([#4890](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4890)) +- [Advanced Settings] Consolidate settings into new "Appearance" category and add category IDs ([#4845](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4845)) +- Adds Data explorer framework and implements Discover using it ([#4806](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4806)) +- Use themes' definitions to render the initial view. This impacts the loading screen font and colors ([#4936](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4936)) + +### ๐Ÿ› Bug Fixes + +- [VisLib] Replace legend color palette with OUI color palette ([#4365](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4365)) +- Fix (styles): Make ace code editor themes consistent ([#4609](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4609)) +- [i18n] fix generation scripts ([#4252](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4252)) +- Fix (Legacy Maps): Add necessary specificity for dark mode style overrides ([#4658](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4658)) +- Fix --font-text CSS var usage and add more leaflet font overrides ([#4674](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4674)) +- Fix snapshots that didn't get updated between PRs ([#4863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4863)) +- [BUG] Fix management overview page duplicate rendering ([#4636](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4636)) +- Fixes broken app when management is turned off ([#4891](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4891)) +- [CCI] Fix EUI/OUI type errors ([#3798](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3798)) +- Correct the generated path for downloading plugins by their names on Windows ([#4953](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4953)) + +### ๐Ÿ“ Documentation + +- Add missing 1.3.x patch release notes to 2.x branch ([#4771](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4771)) +- [Vis Augmenter] Add documentation to `vis_augmenter` plugin ([#4527](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4527)) + +### ๐Ÿ›  Maintenance + +- Version increment from 2.9 to 2.10 ([#4545](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4545)) +- Bump OpenSearch-Dashboards 2.10.0 to use nodejs 18.16.0 version ([#4948](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4948)) +- Bump `oui` to `1.3.0` ([#4941](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4941)) + +### ๐Ÿช› Refactoring + +- [Markdown] Replace custom css styles and native html with OUI ([#4390](http://github.com/opensearch-project/OpenSearch-Dashboards/pull/4390)) +- Removed KUI usage in `maps_legacy` plugin ([#3998](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3998)) +- [Console] Converted all `/lib/autocomplete/**/*.js` files to typescript ([#4148](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4148)) +- [Console] Convert all non-autocomplete lib files to typescript ([#4150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4150)) +- Refactor/remove breadcrumb styling main ([#4621](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4621)) +- Bump `node-sass` to a version that uses a newer `libsass` ([#4651](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4651)) +- [Dashboards] restructure folder to be more cohesive with the project ([#4575](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4575)) +- Remove minimum constraint on opensearch hosts ([#4701](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4701)) +- [CCI] Remove unused tags in the navigation plugin ([#3964](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3964)) +- Refactor logo usage ([#4702](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4702)) + +### ๐Ÿ”ฉ Tests + +- [CI] Fix BWC related CI failures by swapping dist url with snapshot url ([#4828](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4828)) +- [Dashboard De-Angular] Add unit tests for `dashboard_listing` and `dashboard_top_nav` ([#4640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4640)) +- [Tests] Add BWC tests for 2.9 and 2.10 versions ([#4762](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4762)) +- [Stylelint] Add `no_restricted_values` linter rule ([#4413](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4413)) +- Units test for utils folder ([#4641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4641)) +- Test (linkchecker): Exclude checking dead link ([#4720](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4720)) +- Update baseline images for functional tests ([#4879](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4879)) diff --git a/scripts/bwc/opensearch_service.sh b/scripts/bwc/opensearch_service.sh index ecd95990de7e..09f3b5d50c8c 100755 --- a/scripts/bwc/opensearch_service.sh +++ b/scripts/bwc/opensearch_service.sh @@ -24,7 +24,12 @@ function setup_opensearch() { function run_opensearch() { echo "[ Attempting to start OpenSearch... ]" cd "$OPENSEARCH_DIR" - spawn_process_and_save_PID "./opensearch-tar-install.sh > ${LOGS_DIR}/opensearch.log 2>&1 &" + # Check if opensearch-tar-install.sh exists + if [ -f "./opensearch-tar-install.sh" ]; then + spawn_process_and_save_PID "./opensearch-tar-install.sh > ${LOGS_DIR}/opensearch.log 2>&1 &" + else + spawn_process_and_save_PID "./bin/opensearch > ${LOGS_DIR}/opensearch.log 2>&1 &" + fi } # Checks the running status of OpenSearch diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 000000000000..e84ee6b38ac8 --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint no-restricted-syntax: 0 */ + +const fs = require('fs/promises'); + +/** + * Some libraries pack their demos and examples into their release artifacts. + * This unwanted content makes our release artifacts larger but more importantly, + * some contain in-browser references to outdated and vulnerable versions of + * libraries that are not even mentioned in the dependency tree. This is a + * problem when vulnerability scanners point them out, and we have no way to fix + * them. This function looks for folders that are unwanted and deletes them. + */ +const removeUnwantedFolders = async (root, unwantedNames) => { + const items = await fs.readdir(root, { withFileTypes: true }); + const promises = []; + for (const item of items) { + if (!item.isDirectory()) continue; + + if (unwantedNames.includes(item.name)) { + promises.push(fs.rm(`${root}/${item.name}`, { recursive: true, force: true })); + } else { + promises.push(...(await removeUnwantedFolders(`${root}/${item.name}`, unwantedNames))); + } + } + + return promises; +}; +const run = async () => { + const promises = await removeUnwantedFolders('node_modules', ['demo', 'example', 'examples']); + await Promise.all(promises); +}; + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/cli_plugin/install/settings.js b/src/cli_plugin/install/settings.js index 2b0c34bfcd37..cfd576f8ff5a 100644 --- a/src/cli_plugin/install/settings.js +++ b/src/cli_plugin/install/settings.js @@ -42,10 +42,11 @@ function generateUrls({ version, plugin }) { } function generatePluginUrl(version, plugin) { - const platform = process.platform === 'win32' ? 'windows' : process.platform; + const [platform, type] = + process.platform === 'win32' ? ['windows', 'zip'] : [process.platform, 'tar']; const arch = process.arch === 'arm64' ? 'arm64' : 'x64'; - return `${LATEST_PLUGIN_BASE_URL}/${version}/latest/${platform}/${arch}/tar/builds/opensearch-dashboards/plugins/${plugin}-${version}.zip`; + return `${LATEST_PLUGIN_BASE_URL}/${version}/latest/${platform}/${arch}/${type}/builds/opensearch-dashboards/plugins/${plugin}-${version}.zip`; } export function parseMilliseconds(val) { diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index ac7cf94e6761..60313e3fe9b8 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -157,7 +157,7 @@ describe('parse function', function () { "timeout": 0, "urls": Array [ "plugin name", - "https://ci.opensearch.org/ci/dbc/distribution-build-opensearch-dashboards/1234/latest/windows/x64/tar/builds/opensearch-dashboards/plugins/plugin name-1234.zip", + "https://ci.opensearch.org/ci/dbc/distribution-build-opensearch-dashboards/1234/latest/windows/x64/zip/builds/opensearch-dashboards/plugins/plugin name-1234.zip", ], "version": 1234, "workingPath": /plugins/.plugin.installing, diff --git a/src/core/common/index.ts b/src/core/common/index.ts new file mode 100644 index 000000000000..64fb2c6f4353 --- /dev/null +++ b/src/core/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { ImageType, ColorScheme, getLogos } from './logos'; +export type { Logos } from './types'; diff --git a/src/core/common/logos/__snapshots__/get_logos.test.ts.snap b/src/core/common/logos/__snapshots__/get_logos.test.ts.snap new file mode 100644 index 000000000000..6c7746f3533a --- /dev/null +++ b/src/core/common/logos/__snapshots__/get_logos.test.ts.snap @@ -0,0 +1,613 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getLogos when branding has both light and dark logos and only light spinners returns the correct logos 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has both light and dark logos and only light spinners returns the correct logos when dark color scheme is requested 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo-darkmode.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark-darkmode.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark-darkmode.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo-darkmode.svg" +colorScheme = "dark" +`; + +exports[`getLogos when branding has both light and dark logos and only light spinners returns the correct logos when light color scheme is requested 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has both light and dark logos and spinners returns the correct logos 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner-darkmode.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has both light and dark logos and spinners returns the correct logos when dark color scheme is requested 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner-darkmode.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner-darkmode.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo-darkmode.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark-darkmode.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark-darkmode.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo-darkmode.svg" +colorScheme = "dark" +`; + +exports[`getLogos when branding has both light and dark logos and spinners returns the correct logos when light color scheme is requested 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner-darkmode.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has both light and dark logos returns the correct logos 1`] = ` +AnimatedMark.dark.type = "alternative" +AnimatedMark.dark.url = "/custom/branded/mark-darkmode.svg" +AnimatedMark.light.type = "alternative" +AnimatedMark.light.url = "/custom/branded/mark.svg" +AnimatedMark.type = "alternative" +AnimatedMark.url = "/custom/branded/mark.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has both light and dark logos returns the correct logos when dark color scheme is requested 1`] = ` +AnimatedMark.dark.type = "alternative" +AnimatedMark.dark.url = "/custom/branded/mark-darkmode.svg" +AnimatedMark.light.type = "alternative" +AnimatedMark.light.url = "/custom/branded/mark.svg" +AnimatedMark.type = "alternative" +AnimatedMark.url = "/custom/branded/mark-darkmode.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo-darkmode.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark-darkmode.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark-darkmode.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo-darkmode.svg" +colorScheme = "dark" +`; + +exports[`getLogos when branding has both light and dark logos returns the correct logos when light color scheme is requested 1`] = ` +AnimatedMark.dark.type = "alternative" +AnimatedMark.dark.url = "/custom/branded/mark-darkmode.svg" +AnimatedMark.light.type = "alternative" +AnimatedMark.light.url = "/custom/branded/mark.svg" +AnimatedMark.type = "alternative" +AnimatedMark.url = "/custom/branded/mark.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo-darkmode.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark-darkmode.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark-darkmode.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo-darkmode.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has only light logos and spinner returns the correct logos 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has only light logos and spinner returns the correct logos when dark color scheme is requested 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "dark" +`; + +exports[`getLogos when branding has only light logos and spinner returns the correct logos when light color scheme is requested 1`] = ` +AnimatedMark.dark.type = "custom" +AnimatedMark.dark.url = "/custom/branded/spinner.svg" +AnimatedMark.light.type = "custom" +AnimatedMark.light.url = "/custom/branded/spinner.svg" +AnimatedMark.type = "custom" +AnimatedMark.url = "/custom/branded/spinner.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has only light logos returns the correct logos 1`] = ` +AnimatedMark.dark.type = "alternative" +AnimatedMark.dark.url = "/custom/branded/mark.svg" +AnimatedMark.light.type = "alternative" +AnimatedMark.light.url = "/custom/branded/mark.svg" +AnimatedMark.type = "alternative" +AnimatedMark.url = "/custom/branded/mark.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when branding has only light logos returns the correct logos when dark color scheme is requested 1`] = ` +AnimatedMark.dark.type = "alternative" +AnimatedMark.dark.url = "/custom/branded/mark.svg" +AnimatedMark.light.type = "alternative" +AnimatedMark.light.url = "/custom/branded/mark.svg" +AnimatedMark.type = "alternative" +AnimatedMark.url = "/custom/branded/mark.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "dark" +`; + +exports[`getLogos when branding has only light logos returns the correct logos when light color scheme is requested 1`] = ` +AnimatedMark.dark.type = "alternative" +AnimatedMark.dark.url = "/custom/branded/mark.svg" +AnimatedMark.light.type = "alternative" +AnimatedMark.light.url = "/custom/branded/mark.svg" +AnimatedMark.type = "alternative" +AnimatedMark.url = "/custom/branded/mark.svg" +Application.dark.type = "custom" +Application.dark.url = "/custom/branded/logo.svg" +Application.light.type = "custom" +Application.light.url = "/custom/branded/logo.svg" +Application.type = "custom" +Application.url = "/custom/branded/logo.svg" +CenterMark.dark.type = "custom" +CenterMark.dark.url = "/custom/branded/mark.svg" +CenterMark.light.type = "custom" +CenterMark.light.url = "/custom/branded/mark.svg" +CenterMark.type = "custom" +CenterMark.url = "/custom/branded/mark.svg" +Mark.dark.type = "custom" +Mark.dark.url = "/custom/branded/mark.svg" +Mark.light.type = "custom" +Mark.light.url = "/custom/branded/mark.svg" +Mark.type = "custom" +Mark.url = "/custom/branded/mark.svg" +OpenSearch.dark.type = "custom" +OpenSearch.dark.url = "/custom/branded/logo.svg" +OpenSearch.light.type = "custom" +OpenSearch.light.url = "/custom/branded/logo.svg" +OpenSearch.type = "custom" +OpenSearch.url = "/custom/branded/logo.svg" +colorScheme = "light" +`; + +exports[`getLogos when unbranded returns the correct logos 1`] = ` +AnimatedMark.dark.type = "default" +AnimatedMark.dark.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_dark.svg" +AnimatedMark.light.type = "default" +AnimatedMark.light.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_light.svg" +AnimatedMark.type = "default" +AnimatedMark.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_light.svg" +Application.dark.type = "default" +Application.dark.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_dark.svg" +Application.light.type = "default" +Application.light.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_light.svg" +Application.type = "default" +Application.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_light.svg" +CenterMark.dark.type = "default" +CenterMark.dark.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_dark.svg" +CenterMark.light.type = "default" +CenterMark.light.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_light.svg" +CenterMark.type = "default" +CenterMark.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_light.svg" +Mark.dark.type = "default" +Mark.dark.url = "/mocked/base/path/ui/logos/opensearch_mark_on_dark.svg" +Mark.light.type = "default" +Mark.light.url = "/mocked/base/path/ui/logos/opensearch_mark_on_light.svg" +Mark.type = "default" +Mark.url = "/mocked/base/path/ui/logos/opensearch_mark_on_light.svg" +OpenSearch.dark.type = "default" +OpenSearch.dark.url = "/mocked/base/path/ui/logos/opensearch_on_dark.svg" +OpenSearch.light.type = "default" +OpenSearch.light.url = "/mocked/base/path/ui/logos/opensearch_on_light.svg" +OpenSearch.type = "default" +OpenSearch.url = "/mocked/base/path/ui/logos/opensearch_on_light.svg" +colorScheme = "light" +`; + +exports[`getLogos when unbranded returns the correct logos when dark color scheme is requested 1`] = ` +AnimatedMark.dark.type = "default" +AnimatedMark.dark.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_dark.svg" +AnimatedMark.light.type = "default" +AnimatedMark.light.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_light.svg" +AnimatedMark.type = "default" +AnimatedMark.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_dark.svg" +Application.dark.type = "default" +Application.dark.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_dark.svg" +Application.light.type = "default" +Application.light.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_light.svg" +Application.type = "default" +Application.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_dark.svg" +CenterMark.dark.type = "default" +CenterMark.dark.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_dark.svg" +CenterMark.light.type = "default" +CenterMark.light.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_light.svg" +CenterMark.type = "default" +CenterMark.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_dark.svg" +Mark.dark.type = "default" +Mark.dark.url = "/mocked/base/path/ui/logos/opensearch_mark_on_dark.svg" +Mark.light.type = "default" +Mark.light.url = "/mocked/base/path/ui/logos/opensearch_mark_on_light.svg" +Mark.type = "default" +Mark.url = "/mocked/base/path/ui/logos/opensearch_mark_on_dark.svg" +OpenSearch.dark.type = "default" +OpenSearch.dark.url = "/mocked/base/path/ui/logos/opensearch_on_dark.svg" +OpenSearch.light.type = "default" +OpenSearch.light.url = "/mocked/base/path/ui/logos/opensearch_on_light.svg" +OpenSearch.type = "default" +OpenSearch.url = "/mocked/base/path/ui/logos/opensearch_on_dark.svg" +colorScheme = "dark" +`; + +exports[`getLogos when unbranded returns the correct logos when light color scheme is requested 1`] = ` +AnimatedMark.dark.type = "default" +AnimatedMark.dark.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_dark.svg" +AnimatedMark.light.type = "default" +AnimatedMark.light.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_light.svg" +AnimatedMark.type = "default" +AnimatedMark.url = "/mocked/base/path/ui/logos/opensearch_spinner_on_light.svg" +Application.dark.type = "default" +Application.dark.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_dark.svg" +Application.light.type = "default" +Application.light.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_light.svg" +Application.type = "default" +Application.url = "/mocked/base/path/ui/logos/opensearch_dashboards_on_light.svg" +CenterMark.dark.type = "default" +CenterMark.dark.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_dark.svg" +CenterMark.light.type = "default" +CenterMark.light.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_light.svg" +CenterMark.type = "default" +CenterMark.url = "/mocked/base/path/ui/logos/opensearch_center_mark_on_light.svg" +Mark.dark.type = "default" +Mark.dark.url = "/mocked/base/path/ui/logos/opensearch_mark_on_dark.svg" +Mark.light.type = "default" +Mark.light.url = "/mocked/base/path/ui/logos/opensearch_mark_on_light.svg" +Mark.type = "default" +Mark.url = "/mocked/base/path/ui/logos/opensearch_mark_on_light.svg" +OpenSearch.dark.type = "default" +OpenSearch.dark.url = "/mocked/base/path/ui/logos/opensearch_on_dark.svg" +OpenSearch.light.type = "default" +OpenSearch.light.url = "/mocked/base/path/ui/logos/opensearch_on_light.svg" +OpenSearch.type = "default" +OpenSearch.url = "/mocked/base/path/ui/logos/opensearch_on_light.svg" +colorScheme = "light" +`; diff --git a/src/core/common/logos/constants.ts b/src/core/common/logos/constants.ts new file mode 100644 index 000000000000..0972554f1161 --- /dev/null +++ b/src/core/common/logos/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum ImageType { + DEFAULT = 'default', + CUSTOM = 'custom', + ALTERNATIVE = 'alternative', +} + +export enum ColorScheme { + LIGHT = 'light', + DARK = 'dark', +} diff --git a/src/core/common/logos/get_logos.mock.ts b/src/core/common/logos/get_logos.mock.ts new file mode 100644 index 000000000000..d36591c52b6a --- /dev/null +++ b/src/core/common/logos/get_logos.mock.ts @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogos } from './get_logos'; +import { Logos } from './types'; + +export const getLogosMock: { + default: DeeplyMockedKeys; + branded: DeeplyMockedKeys; +} = { + default: getLogos({}, ''), + branded: getLogos( + { + logo: { + defaultUrl: '/custom/branded/logo.svg', + darkModeUrl: '/custom/branded/logo-darkmode.svg', + }, + mark: { + defaultUrl: '/custom/branded/mark.svg', + darkModeUrl: '/custom/branded/mark-darkmode.svg', + }, + }, + '' + ), +}; diff --git a/src/core/common/logos/get_logos.test.ts b/src/core/common/logos/get_logos.test.ts new file mode 100644 index 000000000000..d79564af4318 --- /dev/null +++ b/src/core/common/logos/get_logos.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogos } from './get_logos'; +import { flatObjectSerializer } from '@osd/dev-utils'; + +const serverBasePathMocked = '/mocked/base/path'; + +expect.addSnapshotSerializer(flatObjectSerializer); + +describe('getLogos', () => { + describe('when unbranded', () => { + const branding = {}; + + it('returns the correct logos', () => { + expect(getLogos(branding, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when light color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: false }, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when dark color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: true }, serverBasePathMocked)).toMatchSnapshot(); + }); + }); + + describe('when branding has only light logos', () => { + const branding = { + logo: { defaultUrl: '/custom/branded/logo.svg' }, + mark: { defaultUrl: '/custom/branded/mark.svg' }, + }; + it('returns the correct logos', () => { + expect(getLogos(branding, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when light color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: false }, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when dark color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: true }, serverBasePathMocked)).toMatchSnapshot(); + }); + }); + + describe('when branding has only light logos and spinner', () => { + const branding = { + logo: { defaultUrl: '/custom/branded/logo.svg' }, + mark: { defaultUrl: '/custom/branded/mark.svg' }, + loadingLogo: { defaultUrl: '/custom/branded/spinner.svg' }, + }; + it('returns the correct logos', () => { + expect(getLogos(branding, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when light color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: false }, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when dark color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: true }, serverBasePathMocked)).toMatchSnapshot(); + }); + }); + + describe('when branding has both light and dark logos', () => { + const branding = { + logo: { + defaultUrl: '/custom/branded/logo.svg', + darkModeUrl: '/custom/branded/logo-darkmode.svg', + }, + mark: { + defaultUrl: '/custom/branded/mark.svg', + darkModeUrl: '/custom/branded/mark-darkmode.svg', + }, + }; + it('returns the correct logos', () => { + expect(getLogos(branding, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when light color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: false }, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when dark color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: true }, serverBasePathMocked)).toMatchSnapshot(); + }); + }); + + describe('when branding has both light and dark logos and spinners', () => { + const branding = { + logo: { + defaultUrl: '/custom/branded/logo.svg', + darkModeUrl: '/custom/branded/logo-darkmode.svg', + }, + mark: { + defaultUrl: '/custom/branded/mark.svg', + darkModeUrl: '/custom/branded/mark-darkmode.svg', + }, + loadingLogo: { + defaultUrl: '/custom/branded/spinner.svg', + darkModeUrl: '/custom/branded/spinner-darkmode.svg', + }, + }; + it('returns the correct logos', () => { + expect(getLogos(branding, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when light color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: false }, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when dark color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: true }, serverBasePathMocked)).toMatchSnapshot(); + }); + }); + + describe('when branding has both light and dark logos and only light spinners', () => { + const branding = { + logo: { + defaultUrl: '/custom/branded/logo.svg', + darkModeUrl: '/custom/branded/logo-darkmode.svg', + }, + mark: { + defaultUrl: '/custom/branded/mark.svg', + darkModeUrl: '/custom/branded/mark-darkmode.svg', + }, + loadingLogo: { + defaultUrl: '/custom/branded/spinner.svg', + }, + }; + it('returns the correct logos', () => { + expect(getLogos(branding, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when light color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: false }, serverBasePathMocked)).toMatchSnapshot(); + }); + it('returns the correct logos when dark color scheme is requested', () => { + expect(getLogos({ ...branding, darkMode: true }, serverBasePathMocked)).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/common/logos/get_logos.ts b/src/core/common/logos/get_logos.ts new file mode 100644 index 000000000000..c27a5306e9e1 --- /dev/null +++ b/src/core/common/logos/get_logos.ts @@ -0,0 +1,142 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { deepFreeze } from '@osd/std'; +import type { ImageItem, LogoItem, Logos } from './types'; +import { ColorScheme, ImageType } from './constants'; +import { Branding } from '../../types'; + +// The logos are stored at `src/core/server/core_app/assets/logos` to have a pretty URL +export const OPENSEARCH_DASHBOARDS_THEMED = 'ui/logos/opensearch_dashboards.svg'; +export const OPENSEARCH_DASHBOARDS_ON_LIGHT = 'ui/logos/opensearch_dashboards_on_light.svg'; +export const OPENSEARCH_DASHBOARDS_ON_DARK = 'ui/logos/opensearch_dashboards_on_dark.svg'; +export const OPENSEARCH_THEMED = 'ui/logos/opensearch.svg'; +export const OPENSEARCH_ON_LIGHT = 'ui/logos/opensearch_on_light.svg'; +export const OPENSEARCH_ON_DARK = 'ui/logos/opensearch_on_dark.svg'; +export const MARK_THEMED = 'ui/logos/opensearch_mark.svg'; +export const MARK_ON_LIGHT = 'ui/logos/opensearch_mark_on_light.svg'; +export const MARK_ON_DARK = 'ui/logos/opensearch_mark_on_dark.svg'; +export const CENTER_MARK_THEMED = 'ui/logos/opensearch_center_mark.svg'; +export const CENTER_MARK_ON_LIGHT = 'ui/logos/opensearch_center_mark_on_light.svg'; +export const CENTER_MARK_ON_DARK = 'ui/logos/opensearch_center_mark_on_dark.svg'; +export const ANIMATED_MARK_THEMED = 'ui/logos/opensearch_spinner.svg'; +export const ANIMATED_MARK_ON_LIGHT = 'ui/logos/opensearch_spinner_on_light.svg'; +export const ANIMATED_MARK_ON_DARK = 'ui/logos/opensearch_spinner_on_dark.svg'; + +interface AssetOption { + url?: string; + type: ImageType; + colorScheme: ColorScheme; +} + +/** + * Loops through the assets to find one that has a `url` set. If dark color-scheme asset is needed, + * light assets can be used for fallback but not vice vera. + * Place defaults at the end of assets to use them as final fallbacks. + * `assets` should have dark - light assets of each type, one after the other. + */ +const getFirstUsableAsset = ( + assets: AssetOption[], + requireDarkColorScheme: boolean = false +): ImageItem => { + for (const { url, type, colorScheme } of assets) { + if (url && (requireDarkColorScheme || colorScheme === 'light')) return { url, type }; + } + + // `assets` will contain the default assets so the code will never get here + throw new Error('No default asset found'); +}; + +const getLogo = (assets: AssetOption[], requireDarkColorScheme: boolean = false): LogoItem => { + const lightAsset = getFirstUsableAsset(assets, false); + const darkAsset = getFirstUsableAsset(assets, true); + const colorSchemeAsset = requireDarkColorScheme ? darkAsset : lightAsset; + + return { + light: lightAsset, + dark: darkAsset, + url: colorSchemeAsset.url, + type: colorSchemeAsset.type!, + }; +}; + +/** + * Generates all the combinations of logos based on the color-scheme and branding config + * + * Ideally, the default logos would point to color-scheme-aware (aka themed) imagery while the dark and light + * subtypes reference the dark and light variants. Sadly, Safari doesn't support color-schemes in SVGs yet. + * https://bugs.webkit.org/show_bug.cgi?id=199134 + */ +export const getLogos = (branding: Branding = {}, serverBasePath: string): Logos => { + const { + logo: { defaultUrl: customLogoUrl, darkModeUrl: customDarkLogoUrl } = {}, + mark: { defaultUrl: customMarkUrl, darkModeUrl: customDarkMarkUrl } = {}, + loadingLogo: { defaultUrl: customAnimatedUrl, darkModeUrl: customDarkAnimatedMarkUrl } = {}, + darkMode = false, + } = branding; + + // OSD logos + const defaultLightColorSchemeOpenSearchDashboards = `${serverBasePath}/${OPENSEARCH_DASHBOARDS_ON_LIGHT}`; + const defaultDarkColorSchemeOpenSearchDashboards = `${serverBasePath}/${OPENSEARCH_DASHBOARDS_ON_DARK}`; + // OS logos + const defaultLightColorSchemeOpenSearch = `${serverBasePath}/${OPENSEARCH_ON_LIGHT}`; + const defaultDarkColorSchemeOpenSearch = `${serverBasePath}/${OPENSEARCH_ON_DARK}`; + // OS marks + const defaultLightColorSchemeMark = `${serverBasePath}/${MARK_ON_LIGHT}`; + const defaultDarkColorSchemeMark = `${serverBasePath}/${MARK_ON_DARK}`; + // OS marks variant padded (but not centered) within the container + // ToDo: This naming is misleading; figure out if the distinction could be handled with CSS padding alone + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4714 + const defaultLightColorSchemeCenterMark = `${serverBasePath}/${CENTER_MARK_ON_LIGHT}`; + const defaultDarkColorSchemeCenterMark = `${serverBasePath}/${CENTER_MARK_ON_DARK}`; + // OS animated marks + const defaultLightColorSchemeAnimatedMark = `${serverBasePath}/${ANIMATED_MARK_ON_LIGHT}`; + const defaultDarkColorSchemeAnimatedMark = `${serverBasePath}/${ANIMATED_MARK_ON_DARK}`; + + const colorScheme: ColorScheme = darkMode ? ColorScheme.DARK : ColorScheme.LIGHT; + + // It is easier to read the lines unwrapped, so + // prettier-ignore + return deepFreeze({ + OpenSearch: getLogo([ + { url: customDarkLogoUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.DARK }, + { url: customLogoUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.LIGHT }, + { url: defaultDarkColorSchemeOpenSearch, type: ImageType.DEFAULT, colorScheme: ColorScheme.DARK }, + { url: defaultLightColorSchemeOpenSearch, type: ImageType.DEFAULT, colorScheme: ColorScheme.LIGHT }, + ], darkMode), + + Application: getLogo([ + { url: customDarkLogoUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.DARK }, + { url: customLogoUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.LIGHT }, + { url: defaultDarkColorSchemeOpenSearchDashboards, type: ImageType.DEFAULT, colorScheme: ColorScheme.DARK }, + { url: defaultLightColorSchemeOpenSearchDashboards, type: ImageType.DEFAULT, colorScheme: ColorScheme.LIGHT }, + ], darkMode), + + Mark: getLogo([ + { url: customDarkMarkUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.DARK }, + { url: customMarkUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.LIGHT }, + { url: defaultDarkColorSchemeMark, type: ImageType.DEFAULT, colorScheme: ColorScheme.DARK }, + { url: defaultLightColorSchemeMark, type: ImageType.DEFAULT, colorScheme: ColorScheme.LIGHT }, + ], darkMode), + + CenterMark: getLogo([ + { url: customDarkMarkUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.DARK }, + { url: customMarkUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.LIGHT }, + { url: defaultDarkColorSchemeCenterMark, type: ImageType.DEFAULT, colorScheme: ColorScheme.DARK }, + { url: defaultLightColorSchemeCenterMark, type: ImageType.DEFAULT, colorScheme: ColorScheme.LIGHT }, + ], darkMode), + + AnimatedMark: getLogo([ + { url: customDarkAnimatedMarkUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.DARK }, + { url: customAnimatedUrl, type: ImageType.CUSTOM, colorScheme: ColorScheme.LIGHT }, + { url: customDarkMarkUrl, type: ImageType.ALTERNATIVE, colorScheme: ColorScheme.DARK }, + { url: customMarkUrl, type: ImageType.ALTERNATIVE, colorScheme: ColorScheme.LIGHT }, + { url: defaultDarkColorSchemeAnimatedMark, type: ImageType.DEFAULT, colorScheme: ColorScheme.DARK }, + { url: defaultLightColorSchemeAnimatedMark, type: ImageType.DEFAULT, colorScheme: ColorScheme.LIGHT }, + ], darkMode), + + colorScheme, + }); +}; diff --git a/src/core/common/logos/index.ts b/src/core/common/logos/index.ts new file mode 100644 index 000000000000..7525b1a3a31f --- /dev/null +++ b/src/core/common/logos/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// ToDo: Extend this to handle everything related to white-labelling + +export { getLogos } from './get_logos'; +export { ImageType, ColorScheme } from './constants'; diff --git a/src/core/common/logos/types.ts b/src/core/common/logos/types.ts new file mode 100644 index 000000000000..1885b09017a3 --- /dev/null +++ b/src/core/common/logos/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ColorScheme, ImageType } from './constants'; + +/** + * @public + */ +export interface Logos { + /** + * @deprecated Use {@link Logos.Application} instead. + */ + readonly OpenSearch: LogoItem; + readonly Application: LogoItem; + readonly Mark: LogoItem; + /** + * @deprecated Use {@link Logos.Mark} instead. + */ + readonly CenterMark: LogoItem; + readonly AnimatedMark: LogoItem; + readonly colorScheme: ColorScheme; +} + +export type LogoItem = ImageItem & Record; + +export interface ImageItem { + /** + * The URL of the image + */ + readonly url: string; + readonly type: ImageType; +} diff --git a/src/core/common/mocks.ts b/src/core/common/mocks.ts new file mode 100644 index 000000000000..b1863a96fa38 --- /dev/null +++ b/src/core/common/mocks.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { getLogosMock } from './logos/get_logos.mock'; diff --git a/src/core/common/types.ts b/src/core/common/types.ts new file mode 100644 index 000000000000..1f618a8c396f --- /dev/null +++ b/src/core/common/types.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export type { Logos } from './logos/types'; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 8e5205e6f9bf..14b516ff95bc 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -30,7 +30,8 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@osd/utility-types'; -import { ChromeBadge, ChromeBrand, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './'; +import { ChromeBadge, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './'; +import { getLogosMock } from '../../common/mocks'; const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { @@ -54,6 +55,7 @@ const createStartContractMock = () => { change: jest.fn(), reset: jest.fn(), }, + logos: getLogosMock.default, navControls: { registerLeft: jest.fn(), registerCenter: jest.fn(), @@ -63,8 +65,6 @@ const createStartContractMock = () => { getRight$: jest.fn(), }, setAppTitle: jest.fn(), - setBrand: jest.fn(), - getBrand$: jest.fn(), setIsVisible: jest.fn(), getIsVisible$: jest.fn(), addApplicationClass: jest.fn(), @@ -82,7 +82,6 @@ const createStartContractMock = () => { setCustomNavLink: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); - startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index b8635f5a070f..f11b0f3965e6 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -145,36 +145,6 @@ describe('start', () => { }); }); - describe('brand', () => { - it('updates/emits the brand as it changes', async () => { - const { chrome, service } = await start(); - const promise = chrome.getBrand$().pipe(toArray()).toPromise(); - - chrome.setBrand({ - logo: 'big logo', - smallLogo: 'not so big logo', - }); - chrome.setBrand({ - logo: 'big logo without small logo', - }); - service.stop(); - - await expect(promise).resolves.toMatchInlineSnapshot(` - Array [ - Object {}, - Object { - "logo": "big logo", - "smallLogo": "not so big logo", - }, - Object { - "logo": "big logo without small logo", - "smallLogo": undefined, - }, - ] - `); - }); - }); - describe('visibility', () => { it('emits false when no application is mounted', async () => { const { chrome, service } = await start(); @@ -478,7 +448,6 @@ describe('stop', () => { it('completes applicationClass$, getIsNavDrawerLocked, breadcrumbs$, isVisible$, and brand$ observables', async () => { const { chrome, service } = await start(); const promise = Rx.combineLatest( - chrome.getBrand$(), chrome.getApplicationClasses$(), chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), @@ -496,7 +465,6 @@ describe('stop', () => { await expect( Rx.combineLatest( - chrome.getBrand$(), chrome.getApplicationClasses$(), chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 97746f465abc..d094d86360ef 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -49,6 +49,9 @@ import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_acce import { Header } from './ui'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; import { Branding } from '../'; +import { getLogos } from '../../common'; +import type { Logos } from '../../common/types'; + export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -60,12 +63,6 @@ export interface ChromeBadge { iconType?: IconType; } -/** @public */ -export interface ChromeBrand { - logo?: string; - smallLogo?: string; -} - /** @public */ export type ChromeBreadcrumb = EuiBreadcrumb; @@ -156,7 +153,6 @@ export class ChromeService { this.initVisibility(application); const appTitle$ = new BehaviorSubject('Overview'); - const brand$ = new BehaviorSubject({}); const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); @@ -185,6 +181,8 @@ export class ChromeService { const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + const logos = getLogos(injectedMetadata.getBranding(), http.basePath.serverBasePath); + const isIE = () => { const ua = window.navigator.userAgent; const msie = ua.indexOf('MSIE '); // IE 10 or older @@ -234,6 +232,7 @@ export class ChromeService { navLinks, recentlyAccessed, docTitle, + logos, getHeaderComponent: () => (
), setAppTitle: (appTitle: string) => appTitle$.next(appTitle), - getBrand$: () => brand$.pipe(takeUntil(this.stop$)), - - setBrand: (brand: ChromeBrand) => { - brand$.next( - Object.freeze({ - logo: brand.logo, - smallLogo: brand.smallLogo, - }) - ); - }, - getIsVisible$: () => this.isVisible$, setIsVisible: (isVisible: boolean) => this.isForceHidden$.next(!isVisible), @@ -371,6 +360,8 @@ export interface ChromeStart { recentlyAccessed: ChromeRecentlyAccessed; /** {@inheritdoc ChromeDocTitle} */ docTitle: ChromeDocTitle; + /** {@inheritdoc Logos} */ + readonly logos: Logos; /** * Sets the current app's title @@ -381,31 +372,6 @@ export interface ChromeStart { */ setAppTitle(appTitle: string): void; - /** - * Get an observable of the current brand information. - */ - getBrand$(): Observable; - - /** - * Set the brand configuration. - * - * @remarks - * Normally the `logo` property will be rendered as the - * CSS background for the home link in the chrome navigation, but when the page is - * rendered in a small window the `smallLogo` will be used and rendered at about - * 45px wide. - * - * @example - * ```js - * chrome.setBrand({ - * logo: 'url(/plugins/app/logo.png) center no-repeat' - * smallLogo: 'url(/plugins/app/logo-small.png) center no-repeat' - * }) - * ``` - * - */ - setBrand(brand: ChromeBrand): void; - /** * Get an observable of the current visibility state of the chrome. */ diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index 3b9ad62cfe1a..4cd43362767c 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -34,7 +34,6 @@ export { ChromeService, ChromeStart, InternalChromeStart, - ChromeBrand, ChromeHelpExtension, } from './chrome_service'; export { diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 9bdbec781c5e..7b4e3ba472dc 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -60,15 +60,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "darkMode": false, - "mark": Object { - "darkModeUrl": "/darkModeLogo", - "defaultUrl": "/defaultModeLogo", - }, - } - } + branding={Object {}} closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -131,6 +123,71 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` id="collapsibe-nav" isLocked={false} isNavOpen={true} + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -757,9 +814,9 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiFlexItem eui-yScroll" > @@ -798,7 +855,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/defaultModeLogo" + data-test-opensearch-logo="/test/ui/logos/opensearch_mark_on_light.svg" data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} @@ -809,7 +866,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -858,10 +915,10 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` > @@ -1954,15 +2011,7 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "darkMode": false, - "mark": Object { - "darkModeUrl": "/darkModeLogo", - "defaultUrl": "/defaultModeLogo", - }, - } - } + branding={Object {}} closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -2017,6 +2066,71 @@ exports[`CollapsibleNav renders the default nav 1`] = ` id="collapsibe-nav" isLocked={false} isNavOpen={false} + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2199,15 +2313,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "darkMode": false, - "mark": Object { - "darkModeUrl": "/darkModeLogo", - "defaultUrl": "/defaultModeLogo", - }, - } - } + branding={Object {}} closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -2263,6 +2369,71 @@ exports[`CollapsibleNav renders the default nav 2`] = ` isLocked={false} isNavOpen={false} isOpen={true} + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2445,15 +2616,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "darkMode": false, - "mark": Object { - "darkModeUrl": "/darkModeLogo", - "defaultUrl": "/defaultModeLogo", - }, - } - } + branding={Object {}} closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -2509,6 +2672,71 @@ exports[`CollapsibleNav renders the default nav 3`] = ` isLocked={true} isNavOpen={false} isOpen={true} + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/test/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2922,7 +3150,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` `; -exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = ` +exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1`] = ` @@ -3476,7 +3773,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/darkModeLogo" + data-test-opensearch-logo="/custom/branded/mark-darkmode.svg" data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} @@ -3487,7 +3784,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = >
@@ -3536,10 +3833,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = > @@ -3968,7 +4265,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = `; -exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = ` +exports[`CollapsibleNav with custom branding renders the nav bar in default mode 1`] = ` @@ -4521,7 +4887,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/defaultModeLogo" + data-test-opensearch-logo="/custom/branded/mark.svg" data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} @@ -4532,7 +4898,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = >
@@ -4581,10 +4947,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = > @@ -5013,7 +5379,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = `; -exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = ` +exports[`CollapsibleNav without custom branding renders the nav bar in dark mode 1`] = ` @@ -5564,7 +5994,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="undefined/opensearch_mark_default_mode.svg" + data-test-opensearch-logo="/test/ui/logos/opensearch_mark_on_dark.svg" data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} @@ -5575,7 +6005,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = >
@@ -5624,10 +6054,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = > @@ -6056,7 +6486,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = `; -exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] = ` +exports[`CollapsibleNav without custom branding renders the nav bar in default mode 1`] = ` - - -
-
- -
-
-
- - - -
-
-
-
-
-
- -
- -
-
- -
- - - - - - - -

- OpenSearch Dashboards -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/defaultModeLogo" - data-test-subj="collapsibleNavGroup-opensearchDashboards" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
-
- -
-
- -
-
-
- - - -
-
-
-
-
-
-
-
- - - - - - - -

- Observability -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
-
- -
-
- -
-
-
- - - -
-
-
-
-
-
-
-
- - -
-
- -
    - - - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
  • - -
  • -
    -
-
-
-
-
-
-
-
- - - -
-`; - -exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] = ` - @@ -7653,7 +7097,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="undefined/opensearch_mark_default_mode.svg" + data-test-opensearch-logo="/test/ui/logos/opensearch_mark_on_light.svg" data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} @@ -7664,7 +7108,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] >
@@ -7713,10 +7157,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] > diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 9068e225c8ba..b9da5ac37dbe 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -247,18 +247,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "applicationTitle": "OpenSearch Dashboards", - "darkMode": false, - "logo": Object { - "defaultUrl": "/", - }, - "mark": Object { - "defaultUrl": "/", - }, - } - } + branding={Object {}} breadcrumbs$={ BehaviorSubject { "_isScalar": false, @@ -1437,6 +1426,71 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navControlsCenter$={ BehaviorSubject { "_isScalar": false, @@ -1888,27 +1942,8 @@ exports[`Header handles visibility and lock changes 1`] = ` "borders": "none", "items": Array [ , ], }, @@ -2335,27 +2434,8 @@ exports[`Header handles visibility and lock changes 1`] = ` className="euiHeaderSectionItem" > OpenSearch Dashboards logo @@ -2948,18 +3092,7 @@ exports[`Header handles visibility and lock changes 1`] = ` className="euiHeaderSectionItem euiHeaderSectionItem--borderRight" >
@@ -5625,18 +5877,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "applicationTitle": "OpenSearch Dashboards", - "darkMode": false, - "logo": Object { - "defaultUrl": "/", - }, - "mark": Object { - "defaultUrl": "/", - }, - } - } closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -5696,6 +5936,71 @@ exports[`Header handles visibility and lock changes 1`] = ` id="mockId" isLocked={true} isNavOpen={false} + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navLinks$={ BehaviorSubject { "_isScalar": false, @@ -6634,14 +6939,6 @@ exports[`Header renders condensed header 1`] = ` } branding={ Object { - "applicationTitle": "Foobar Dashboards", - "darkMode": false, - "logo": Object { - "defaultUrl": "/foo", - }, - "mark": Object { - "defaultUrl": "/foo", - }, "useExpandedHeader": false, } } @@ -7777,6 +8074,71 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navControlsCenter$={ BehaviorSubject { "_isScalar": false, @@ -8280,14 +8642,6 @@ exports[`Header renders condensed header 1`] = `
@@ -10872,19 +11348,6 @@ exports[`Header renders condensed header 1`] = ` "serverBasePath": "/test", } } - branding={ - Object { - "applicationTitle": "Foobar Dashboards", - "darkMode": false, - "logo": Object { - "defaultUrl": "/foo", - }, - "mark": Object { - "defaultUrl": "/foo", - }, - "useExpandedHeader": false, - } - } closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -10939,6 +11402,71 @@ exports[`Header renders condensed header 1`] = ` id="mockId" isLocked={false} isNavOpen={false} + logos={ + Object { + "AnimatedMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_spinner_on_light.svg", + }, + "Application": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_dashboards_on_light.svg", + }, + "CenterMark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_center_mark_on_light.svg", + }, + "Mark": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_mark_on_light.svg", + }, + "OpenSearch": Object { + "dark": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_dark.svg", + }, + "light": Object { + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "type": "default", + "url": "/ui/logos/opensearch_on_light.svg", + }, + "colorScheme": "light", + } + } navLinks$={ BehaviorSubject { "_isScalar": false, diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap index 7e44e456f320..ed8ee0c5f18c 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_logo.test.tsx.snap @@ -1,2176 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Header logo when dark-themed uses custom dark-mode logo if branding dark-mode logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when dark-themed uses dashboards' dark logo if branding containing a mark but not a logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when dark-themed uses dashboards' dark logo if branding containing no logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when dark-themed uses dashboards' dark logo if no branding is provided 1`] = ` - - - opensearch dashboards logo - - -`; - -exports[`Header logo when dark-themed uses default-themed custom logo if branding without dark-mode logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when default-themed uses custom default-mode logo if branding logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when default-themed uses dashboards logo if branding containing a mark but not a logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when default-themed uses dashboards logo if branding containing no logo is provided 1`] = ` - - - custom title logo - - -`; - -exports[`Header logo when default-themed uses dashboards logo if no branding is provided 1`] = ` - - - opensearch dashboards logo - - +exports[`Header logo uses branded application title when provided 1`] = ` + + Page Title logo + +`; + +exports[`Header logo uses default application title when not branded 1`] = ` + + opensearch dashboards logo + `; diff --git a/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap index 63e83acd78e3..c1a7c11b69f9 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/home_icon.test.tsx.snap @@ -1,1995 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Home button icon in condensed dark mode uses custom mark dark mode URL 1`] = ` - - - - - -`; - -exports[`Home button icon in condensed dark mode uses custom mark default mode URL if no dark mode mark provided 1`] = ` - - - - - -`; - -exports[`Home button icon in condensed dark mode uses opensearch mark if custom logo provided without mark 1`] = ` - - - - - -`; - -exports[`Home button icon in condensed dark mode uses opensearch mark if no mark provided 1`] = ` - - - - - -`; - -exports[`Home button icon in condensed light mode uses custom mark default mode URL 1`] = ` - - - - - -`; - -exports[`Home button icon in condensed light mode uses opensearch mark if custom logo provided without mark 1`] = ` - - - - - -`; - -exports[`Home button icon in condensed light mode uses opensearch mark if no mark provided 1`] = ` - - - - - -`; - -exports[`Home button icon in dark mode uses custom mark dark mode URL 1`] = ` - - - - - -`; - -exports[`Home button icon in dark mode uses custom mark default mode URL if no dark mode mark provided 1`] = ` - - - - - -`; - -exports[`Home button icon in dark mode uses home icon if custom logo provided without mark 1`] = ` - - - - - -`; - -exports[`Home button icon in dark mode uses home icon if no mark provided 1`] = ` - - - - - -`; - -exports[`Home button icon in light mode uses custom mark default mode URL 1`] = ` - - - - - -`; - -exports[`Home button icon in light mode uses home icon if custom logo provided without mark 1`] = ` - - - - - -`; - -exports[`Home button icon in light mode uses home icon if no branding provided 1`] = ` - - - - - -`; - -exports[`Home button icon in light mode uses home icon if no mark provided 1`] = ` - - - - - +exports[`Home icon, custom branded, uses the custom logo when header is expanded 1`] = ` + +`; + +exports[`Home icon, custom branded, uses the custom logo when header is not expanded 1`] = ` + +`; + +exports[`Home icon, unbranded, uses the home icon when header is expanded 1`] = ` + +`; + +exports[`Home icon, unbranded, uses the mark logo when header is not expanded 1`] = ` + `; diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index dc44fe5053fe..955b7d7ca242 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -37,6 +37,7 @@ import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..'; import { httpServiceMock } from '../../../http/http_service.mock'; import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; import { CollapsibleNav } from './collapsible_nav'; +import { getLogos } from '../../../../common'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -64,10 +65,22 @@ function mockRecentNavLink({ label = 'recent' }: Partial Promise.resolve(), navigateToUrl: () => Promise.resolve(), customNavLink$: new BehaviorSubject(undefined), - branding: { - darkMode: false, - mark: { - defaultUrl: '/defaultModeLogo', - darkModeUrl: '/darkModeLogo', - }, - }, + branding, + logos: getLogos(branding, mockBasePath.serverBasePath), }; } @@ -212,77 +220,79 @@ describe('CollapsibleNav', () => { expectNavIsClosed(component); }); - it('renders the nav bar with custom logo in default mode', () => { - const navLinks = [ - mockLink({ category: opensearchDashboards }), - mockLink({ category: observability }), - ]; - const recentNavLinks = [mockRecentNavLink({})]; - const component = mount( - - ); - // check if nav bar renders default mode custom logo - expect(component).toMatchSnapshot(); + describe('with custom branding', () => { + it('renders the nav bar in default mode', () => { + const navLinks = [ + mockLink({ category: opensearchDashboards }), + mockLink({ category: observability }), + ]; + const recentNavLinks = [mockRecentNavLink({})]; + const component = mount( + + ); - // check if nav bar renders the original default mode opensearch mark - component.setProps({ - branding: { - darkMode: false, - mark: {}, - }, + expect(component).toMatchSnapshot(); }); - expect(component).toMatchSnapshot(); - }); - it('renders the nav bar with custom logo in dark mode', () => { - const navLinks = [ - mockLink({ category: opensearchDashboards }), - mockLink({ category: observability }), - ]; - const recentNavLinks = [mockRecentNavLink({})]; - const component = mount( - - ); - // check if nav bar renders dark mode custom logo - component.setProps({ - branding: { - darkMode: true, - mark: { - defaultUrl: '/defaultModeLogo', - darkModeUrl: '/darkModeLogo', - }, - }, + it('renders the nav bar in dark mode', () => { + const navLinks = [ + mockLink({ category: opensearchDashboards }), + mockLink({ category: observability }), + ]; + const recentNavLinks = [mockRecentNavLink({})]; + const component = mount( + + ); + + expect(component).toMatchSnapshot(); }); - expect(component).toMatchSnapshot(); + }); - // check if nav bar renders default mode custom logo - component.setProps({ - branding: { - darkMode: true, - mark: { - defaultUrl: '/defaultModeLogo', - }, - }, + describe('without custom branding', () => { + it('renders the nav bar in default mode', () => { + const navLinks = [ + mockLink({ category: opensearchDashboards }), + mockLink({ category: observability }), + ]; + const recentNavLinks = [mockRecentNavLink({})]; + const component = mount( + + ); + + expect(component).toMatchSnapshot(); }); - expect(component).toMatchSnapshot(); - // check if nav bar renders the original dark mode opensearch mark - component.setProps({ - branding: { - darkMode: false, - mark: {}, - }, + it('renders the nav bar in dark mode', () => { + const navLinks = [ + mockLink({ category: opensearchDashboards }), + mockLink({ category: observability }), + ]; + const recentNavLinks = [mockRecentNavLink({})]; + const component = mount( + + ); + + expect(component).toMatchSnapshot(); }); - expect(component).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 51d43d96f7fd..8b178200114a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -50,7 +50,7 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; -import { ChromeBranding } from '../../chrome_service'; +import type { Logos } from '../../../../common/types'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -101,7 +101,7 @@ interface Props { navigateToApp: InternalApplicationStart['navigateToApp']; navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; - branding: ChromeBranding; + logos: Logos; } export function CollapsibleNav({ @@ -115,7 +115,7 @@ export function CollapsibleNav({ closeNav, navigateToApp, navigateToUrl, - branding, + logos, ...observables }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); @@ -138,42 +138,6 @@ export function CollapsibleNav({ }); }; - const DEFAULT_OPENSEARCH_MARK = `${branding.assetFolderUrl}/opensearch_mark_default_mode.svg`; - const DARKMODE_OPENSEARCH_MARK = `${branding.assetFolderUrl}/opensearch_mark_dark_mode.svg`; - - const darkMode = branding.darkMode; - const markDefault = branding.mark?.defaultUrl; - const markDarkMode = branding.mark?.darkModeUrl; - - /** - * Use branding configurations to check which URL to use for rendering - * side menu opensearch logo in default mode - * - * @returns a valid custom URL or original default mode opensearch mark if no valid URL is provided - */ - const customSideMenuLogoDefaultMode = () => { - return markDefault ?? DEFAULT_OPENSEARCH_MARK; - }; - - /** - * Use branding configurations to check which URL to use for rendering - * side menu opensearch logo in dark mode - * - * @returns a valid custom URL or original dark mode opensearch mark if no valid URL is provided - */ - const customSideMenuLogoDarkMode = () => { - return markDarkMode ?? markDefault ?? DARKMODE_OPENSEARCH_MARK; - }; - - /** - * Render custom side menu logo for both default mode and dark mode - * - * @returns a valid logo URL - */ - const customSideMenuLogo = () => { - return darkMode ? customSideMenuLogoDarkMode() : customSideMenuLogoDefaultMode(); - }; - return ( { const category = categoryDictionary[categoryName]!; const opensearchLinkLogo = - category.id === 'opensearchDashboards' ? customSideMenuLogo() : category.euiIconType; + category.id === 'opensearchDashboards' ? logos.Mark.url : category.euiIconType; return ( {}, - branding: { - darkMode: false, - logo: { defaultUrl: '/' }, - mark: { defaultUrl: '/' }, - applicationTitle: 'OpenSearch Dashboards', - }, + branding: {}, survey: '/', + logos: chromeServiceMock.createStartContract().logos, }; } @@ -102,17 +98,17 @@ describe('Header', () => { const recentlyAccessed$ = new BehaviorSubject([ { link: '', label: 'dashboard', id: 'dashboard' }, ]); - const component = mountWithIntl( -
- ); + const props = { + ...mockProps(), + isVisible$, + breadcrumbs$, + navLinks$, + recentlyAccessed$, + isLocked$, + customNavLink$, + }; + + const component = mountWithIntl(
); expect(component.find('EuiHeader').exists()).toBeFalsy(); expect(component.find('EuiProgress').exists()).toBeTruthy(); @@ -120,7 +116,6 @@ describe('Header', () => { component.update(); expect(component.find('EuiHeader.primaryHeader').exists()).toBeTruthy(); expect(component.find('EuiHeader.expandedHeader').exists()).toBeTruthy(); - expect(component.find('HeaderLogo').exists()).toBeTruthy(); expect(component.find('HeaderNavControls')).toHaveLength(5); expect(component.find('[data-test-subj="toggleNavButton"]').exists()).toBeTruthy(); expect(component.find('HomeLoader').exists()).toBeTruthy(); @@ -131,6 +126,11 @@ describe('Header', () => { expect(component.find('EuiFlyout[aria-label="Primary"]').exists()).toBeFalsy(); + const headerLogo = component.find('HeaderLogo'); + expect(headerLogo.exists()).toBeTruthy(); + expect(headerLogo.prop('backgroundColorScheme')).toEqual('dark'); + expect(headerLogo.prop('logos')).toEqual(props.logos); + act(() => isLocked$.next(true)); component.update(); expect(component.find('EuiFlyout[aria-label="Primary"]').exists()).toBeTruthy(); @@ -139,16 +139,13 @@ describe('Header', () => { it('renders condensed header', () => { const branding = { - darkMode: false, - logo: { defaultUrl: '/foo' }, - mark: { defaultUrl: '/foo' }, - applicationTitle: 'Foobar Dashboards', useExpandedHeader: false, }; const props = { ...mockProps(), branding, }; + const component = mountWithIntl(
); expect(component.find('EuiHeader.primaryHeader').exists()).toBeTruthy(); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 9496b76b9980..acc7c6869145 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -64,6 +64,7 @@ import { HomeLoader } from './home_loader'; import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; import { HeaderLogo } from './header_logo'; +import type { Logos } from '../../../../common/types'; export interface HeaderProps { opensearchDashboardsVersion: string; @@ -90,6 +91,7 @@ export interface HeaderProps { loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; branding: ChromeBranding; + logos: Logos; survey: string | undefined; } @@ -102,6 +104,7 @@ export function Header({ homeHref, branding, survey, + logos, ...observables }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); @@ -117,7 +120,7 @@ export function Header({ const className = classnames('hide-for-sharing', 'headerGlobalNav'); const { useExpandedHeader = true } = branding; - const headerTheme: EuiHeaderProps['theme'] = 'dark'; + const expandedHeaderColorScheme: EuiHeaderProps['theme'] = 'dark'; return ( <> @@ -126,7 +129,7 @@ export function Header({ {useExpandedHeader && ( , ], borders: 'none', @@ -200,6 +203,7 @@ export function Header({ navLinks$={observables.navLinks$} navigateToApp={application.navigateToApp} branding={branding} + logos={logos} loadingCount$={observables.loadingCount$} /> @@ -259,7 +263,7 @@ export function Header({ } }} customNavLink$={observables.customNavLink$} - branding={branding} + logos={logos} />
diff --git a/src/core/public/chrome/ui/header/header_logo.test.tsx b/src/core/public/chrome/ui/header/header_logo.test.tsx index 6d31e71c1f0a..c43073ec7f48 100644 --- a/src/core/public/chrome/ui/header/header_logo.test.tsx +++ b/src/core/public/chrome/ui/header/header_logo.test.tsx @@ -3,183 +3,102 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiHeaderProps } from '@elastic/eui'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { HeaderLogo, DEFAULT_DARK_LOGO, DEFAULT_LOGO } from './header_logo'; -import { BasePath } from '../../../http/base_path'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { HeaderLogo } from './header_logo'; +import { getLogosMock } from '../../../../common/mocks'; -const basePath = new BasePath('/base'); +const mockTitle = 'Page Title'; const mockProps = () => ({ href: '/', - basePath, navLinks$: new BehaviorSubject([]), forceNavigation$: new BehaviorSubject(false), navigateToApp: jest.fn(), branding: {}, - theme: 'default' as EuiHeaderProps['theme'], + logos: getLogosMock.default, }); describe('Header logo', () => { - describe('when default-themed ', () => { - it('uses dashboards logo if no branding is provided', () => { - const branding = {}; - const props = { - ...mockProps(), - branding, - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(`${basePath.serverBasePath}/${DEFAULT_LOGO}`); - expect(img.prop('alt')).toEqual(`opensearch dashboards logo`); - expect(component).toMatchSnapshot(); - }); + it("uses the light color-scheme's Application logo by default", () => { + const props = { + ...mockProps(), + }; + const component = shallowWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(props.logos.Application.light.url); + }); - it('uses dashboards logo if branding containing no logo is provided', () => { - const branding = { - logo: {}, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const props = { - ...mockProps(), - branding, - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(`${basePath.serverBasePath}/${DEFAULT_LOGO}`); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); - }); + it("uses the light color-scheme's Application logo if the header's theme is not dark", () => { + const props = { + ...mockProps(), + backgroundColorScheme: 'light' as const, + }; + const component = shallowWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(props.logos.Application.light.url); + }); - it('uses dashboards logo if branding containing a mark but not a logo is provided', () => { - const branding = { - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const props = { - ...mockProps(), - branding, - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(`${basePath.serverBasePath}/${DEFAULT_LOGO}`); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); - }); + it("uses the normal color-scheme's Application logo if the header's theme is not dark", () => { + const props = { + ...mockProps(), + backgroundColorScheme: 'normal' as const, + }; + const component = shallowWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(props.logos.Application.light.url); + }); - it('uses custom default-mode logo if branding logo is provided', () => { - const branding = { - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const props = { - ...mockProps(), - branding, - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(branding.logo.defaultUrl); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); - }); + it("uses the dark color-scheme's Application logo if the header's theme is dark", () => { + const props = { + ...mockProps(), + backgroundColorScheme: 'dark' as const, + }; + const component = shallowWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('src')).toEqual(props.logos.Application.dark.url); }); - describe('when dark-themed', () => { - it("uses dashboards' dark logo if no branding is provided", () => { - const branding = {}; - const props = { - ...mockProps(), - branding, - theme: 'dark' as EuiHeaderProps['theme'], - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(`${basePath.serverBasePath}/${DEFAULT_DARK_LOGO}`); - expect(img.prop('alt')).toEqual(`opensearch dashboards logo`); - expect(component).toMatchSnapshot(); - }); + it('uses default application title when not branded', () => { + const props = { + ...mockProps(), + }; + const component = shallowWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('data-test-subj')).toEqual(`defaultLogo`); + expect(img.prop('alt')).toEqual(`opensearch dashboards logo`); + expect(component).toMatchSnapshot(); + }); - it("uses dashboards' dark logo if branding containing no logo is provided", () => { - const branding = { - logo: {}, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const props = { - ...mockProps(), - branding, - theme: 'dark' as EuiHeaderProps['theme'], - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(`${basePath.serverBasePath}/${DEFAULT_DARK_LOGO}`); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); - }); + it('uses branded application title when provided', () => { + const props = { + ...mockProps(), + logos: getLogosMock.branded, + branding: { + applicationTitle: mockTitle, + }, + }; + const component = shallowWithIntl(); + const img = component.find('.logoContainer img'); + expect(img.prop('data-test-subj')).toEqual(`customLogo`); + expect(img.prop('alt')).toEqual(`${mockTitle} logo`); + expect(component).toMatchSnapshot(); + }); - it("uses dashboards' dark logo if branding containing a mark but not a logo is provided", () => { - const branding = { - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; + describe('onClick', () => { + it('uses default application title when not branded', () => { const props = { ...mockProps(), - branding, - theme: 'dark' as EuiHeaderProps['theme'], }; const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(`${basePath.serverBasePath}/${DEFAULT_DARK_LOGO}`); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); - }); + component.find('.logoContainer img').simulate('click'); - it('uses default-themed custom logo if branding without dark-mode logo is provided', () => { - const branding = { - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const props = { - ...mockProps(), - branding, - theme: 'dark' as EuiHeaderProps['theme'], - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(branding.logo.defaultUrl); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); + expect(props.navigateToApp).toHaveBeenCalledTimes(1); + expect(props.navigateToApp).toHaveBeenCalledWith('home'); }); - it('uses custom dark-mode logo if branding dark-mode logo is provided', () => { - const branding = { - logo: { defaultUrl: '/defaultModeLogo', darkModeUrl: '/darkModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const props = { - ...mockProps(), - branding, - theme: 'dark' as EuiHeaderProps['theme'], - }; - const component = mountWithIntl(); - const img = component.find('.logoContainer img'); - expect(img.prop('src')).toEqual(branding.logo.darkModeUrl); - expect(img.prop('alt')).toEqual(`${branding.applicationTitle} logo`); - expect(component).toMatchSnapshot(); - }); + // ToDo: Add tests for onClick + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4692 + it.todo('performs all the complications'); }); }); diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 00de679f5184..b5fddee0e595 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -29,29 +29,13 @@ */ import './header_logo.scss'; -import { EuiHeaderProps } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { ChromeNavLink } from '../..'; import { ChromeBranding } from '../../chrome_service'; -import { HttpStart } from '../../../http'; - -function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { - let current = element; - while (current) { - if (current.tagName === 'A') { - return current as HTMLAnchorElement; - } - - if (!current.parentElement || current.parentElement === document.body) { - return undefined; - } - - current = current.parentElement; - } -} +import type { Logos } from '../../../../common/types'; function onClick( event: React.MouseEvent, @@ -59,7 +43,7 @@ function onClick( navLinks: ChromeNavLink[], navigateToApp: (appId: string) => void ) { - const anchor = findClosestAnchor((event as any).nativeEvent.target); + const anchor = (event.nativeEvent.target as HTMLAnchorElement)?.closest('a'); if (!anchor) { return; } @@ -98,44 +82,36 @@ function onClick( } } -export const DEFAULT_LOGO = 'ui/logos/opensearch_dashboards.svg'; -export const DEFAULT_DARK_LOGO = 'ui/logos/opensearch_dashboards_darkmode.svg'; - interface Props { href: string; navLinks$: Observable; forceNavigation$: Observable; navigateToApp: (appId: string) => void; branding: ChromeBranding; - basePath: HttpStart['basePath']; - // What background is the logo appearing on; this is independent of theme:darkMode - theme?: EuiHeaderProps['theme']; + logos: Logos; + /* indicates the background color-scheme this element will appear over + * `'normal'` and `'light'` are synonyms of being `undefined`, to mean not `'dark'` + */ + backgroundColorScheme?: 'normal' | 'light' | 'dark'; } export function HeaderLogo({ href, navigateToApp, branding, - basePath, - theme = 'default', + logos, + backgroundColorScheme, ...observables }: Props) { const forceNavigation = useObservable(observables.forceNavigation$, false); const navLinks = useObservable(observables.navLinks$, []); - const { - logo: { defaultUrl: customLogoUrl, darkModeUrl: customDarkLogoUrl } = {}, - applicationTitle = 'opensearch dashboards', - } = branding; + const { applicationTitle = 'opensearch dashboards' } = branding; - // Attempt to find a suitable custom branded logo before falling back on OSD's - let logoSrc = theme === 'dark' && customDarkLogoUrl ? customDarkLogoUrl : customLogoUrl; - let testSubj = 'customLogo'; - - // If no custom branded logo was set, use OSD's - if (!logoSrc) { - logoSrc = `${basePath.serverBasePath}/${theme === 'dark' ? DEFAULT_DARK_LOGO : DEFAULT_LOGO}`; - testSubj = 'defaultLogo'; - } + const { + [backgroundColorScheme === 'dark' ? 'dark' : 'light']: { url: logoURL }, + type: logoType, + } = logos.Application; + const testSubj = `${logoType}Logo`; const alt = `${applicationTitle} logo`; @@ -151,8 +127,8 @@ export function HeaderLogo({ > {alt} { - describe('in condensed light mode ', () => { - it('uses opensearch mark if no mark provided', () => { - const branding = { - darkMode: false, - logo: {}, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_MARK}`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); +import { shallow } from 'enzyme'; +import { HomeIcon } from './home_icon'; +import { getLogosMock } from '../../../../common/mocks'; - it('uses opensearch mark if custom logo provided without mark', () => { - const branding = { - darkMode: false, - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_MARK}`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); +const mockTitle = 'Page Title'; - it('uses custom mark default mode URL', () => { - const branding = { - darkMode: false, - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); +describe('Home icon,', () => { + describe('unbranded,', () => { + const mockProps = () => ({ + branding: {}, + logos: getLogosMock.default, }); - }); - describe('in condensed dark mode ', () => { - it('uses opensearch mark if no mark provided', () => { - const branding = { - darkMode: true, - logo: {}, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, - }; - const component = mountWithIntl(); + it('uses the home icon by default', () => { + const props = mockProps(); + const component = shallow(); const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_DARK_MARK}`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); + expect(icon.prop('data-test-subj')).toEqual('homeIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual('m'); + expect(icon.prop('title')).toEqual('opensearch dashboards home'); }); - it('uses opensearch mark if custom logo provided without mark', () => { - const branding = { - darkMode: true, - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, + it('uses the home icon when header is expanded', () => { + const props = { + ...mockProps(), + branding: { + useExpandedHeader: true, + }, }; - const component = mountWithIntl(); + const component = shallow(); const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(`${branding.assetFolderUrl}/${DEFAULT_DARK_MARK}`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); + expect(icon.prop('data-test-subj')).toEqual('homeIcon'); + expect(icon.prop('type')).toEqual('home'); + expect(icon.prop('size')).toEqual('m'); + expect(icon.prop('title')).toEqual('opensearch dashboards home'); - it('uses custom mark default mode URL if no dark mode mark provided', () => { - const branding = { - darkMode: true, - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); - it('uses custom mark dark mode URL', () => { - const branding = { - darkMode: true, - logo: {}, - mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - useExpandedHeader: false, + it('uses the mark logo when header is not expanded', () => { + const props = { + ...mockProps(), + branding: { + useExpandedHeader: false, + }, }; - const component = mountWithIntl(); + const component = shallow(); const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(branding.mark.darkModeUrl); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); + expect(icon.prop('data-test-subj')).toEqual('defaultMark'); + expect(icon.prop('type')).toEqual(props.logos.Mark.url); + expect(icon.prop('size')).toEqual('l'); + expect(icon.prop('title')).toEqual('opensearch dashboards home'); + expect(component).toMatchSnapshot(); }); }); - describe('in light mode ', () => { - it('uses home icon if no branding provided', () => { - const branding = {}; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual('home'); - expect(icon.prop('size')).toEqual(`m`); - expect(icon.prop('title')).toEqual(`opensearch dashboards home`); - expect(component).toMatchSnapshot(); + describe('custom branded,', () => { + const mockProps = () => ({ + branding: { + applicationTitle: mockTitle, + }, + logos: getLogosMock.branded, }); - it('uses home icon if no mark provided', () => { - const branding = { - darkMode: false, - logo: {}, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); + it('uses the custom logo by default', () => { + const props = mockProps(); + const component = shallow(); const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual('home'); - expect(icon.prop('size')).toEqual(`m`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); + expect(icon.prop('data-test-subj')).toEqual('customMark'); + expect(icon.prop('type')).toEqual(props.logos.Mark.url); + expect(icon.prop('size')).toEqual('l'); + expect(icon.prop('title')).toEqual(`${mockTitle} home`); }); - it('uses home icon if custom logo provided without mark', () => { - const branding = { - darkMode: false, - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual('home'); - expect(icon.prop('size')).toEqual(`m`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); + it('uses the custom logo when header is expanded', () => { + const props = mockProps(); + // @ts-expect-error + props.branding.useExpandedHeader = true; - it('uses custom mark default mode URL', () => { - const branding = { - darkMode: false, - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); + const component = shallow(); const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); - expect(icon.prop('size')).toEqual(`l`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); - }); + expect(icon.prop('data-test-subj')).toEqual('customMark'); + expect(icon.prop('type')).toEqual(props.logos.Mark.url); + expect(icon.prop('size')).toEqual('l'); + expect(icon.prop('title')).toEqual(`${mockTitle} home`); - describe('in dark mode ', () => { - it('uses home icon if no mark provided', () => { - const branding = { - darkMode: true, - logo: {}, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual('home'); - expect(icon.prop('size')).toEqual(`m`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); - it('uses home icon if custom logo provided without mark', () => { - const branding = { - darkMode: true, - logo: { defaultUrl: '/defaultModeLogo' }, - mark: {}, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual('home'); - expect(icon.prop('size')).toEqual(`m`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); + it('uses the custom logo when header is not expanded', () => { + const props = mockProps(); + // @ts-expect-error + props.branding.useExpandedHeader = false; - it('uses custom mark default mode URL if no dark mode mark provided', () => { - const branding = { - darkMode: true, - logo: {}, - mark: { defaultUrl: '/defaultModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); + const component = shallow(); const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(branding.mark.defaultUrl); - expect(icon.prop('size')).toEqual(`l`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); - expect(component).toMatchSnapshot(); - }); + expect(icon.prop('data-test-subj')).toEqual('customMark'); + expect(icon.prop('type')).toEqual(props.logos.Mark.url); + expect(icon.prop('size')).toEqual('l'); + expect(icon.prop('title')).toEqual(`${mockTitle} home`); - it('uses custom mark dark mode URL', () => { - const branding = { - darkMode: true, - logo: {}, - mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, - applicationTitle: 'custom title', - assetFolderUrl: 'base/ui/default_branding', - }; - const component = mountWithIntl(); - const icon = component.find('EuiIcon'); - expect(icon.prop('type')).toEqual(branding.mark.darkModeUrl); - expect(icon.prop('size')).toEqual(`l`); - expect(icon.prop('title')).toEqual(`${branding.applicationTitle} home`); expect(component).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/header/home_icon.tsx b/src/core/public/chrome/ui/header/home_icon.tsx index 9260fc19ccae..2c0beb5a26cb 100644 --- a/src/core/public/chrome/ui/header/home_icon.tsx +++ b/src/core/public/chrome/ui/header/home_icon.tsx @@ -4,51 +4,46 @@ */ import React from 'react'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, IconSize } from '@elastic/eui'; import { ChromeBranding } from '../../chrome_service'; +import type { Logos } from '../../../../common/types'; -export const DEFAULT_MARK = 'opensearch_mark_default_mode.svg'; -export const DEFAULT_DARK_MARK = 'opensearch_mark_dark_mode.svg'; +interface Props { + branding: ChromeBranding; + logos: Logos; +} /** * Use branding configurations to render the header mark on the nav bar. - * - * @param {ChromeBranding} - branding object consist of mark, darkmode selection, asset path and title - * @returns Mark component which is going to be rendered on the main page header bar. */ -export const HomeIcon = ({ - darkMode, - assetFolderUrl = '', - mark, - applicationTitle = 'opensearch dashboards', - useExpandedHeader = true, -}: ChromeBranding) => { - const { defaultUrl: markUrl, darkModeUrl: darkMarkUrl } = mark ?? {}; - - const customMark = darkMode ? darkMarkUrl ?? markUrl : markUrl; - const defaultMark = darkMode ? DEFAULT_DARK_MARK : DEFAULT_MARK; - - const getIconProps = () => { - const iconType = customMark - ? customMark - : useExpandedHeader - ? 'home' - : `${assetFolderUrl}/${defaultMark}`; - const testSubj = customMark ? 'customMark' : useExpandedHeader ? 'homeIcon' : 'defaultMark'; - const title = `${applicationTitle} home`; - // marks look better at the large size, but the home icon should be medium to fit in with other icons - const size = iconType === 'home' ? ('m' as const) : ('l' as const); - - return { - 'data-test-subj': testSubj, - 'data-test-image-url': iconType, - type: iconType, - title, - size, - }; - }; - - const props = getIconProps(); - - return ; +export const HomeIcon = ({ branding, logos }: Props) => { + const { applicationTitle = 'opensearch dashboards', useExpandedHeader = true } = branding; + + const { url: markURL, type: markType } = logos.Mark; + + let markIcon = markURL; + let testSubj = `${markType}Mark`; + // Marks look better at the large size + let markIconSize: IconSize = 'l'; + + // If no custom branded mark was set, use `home` icon only for expanded headers + if (markType !== 'custom' && useExpandedHeader) { + markIcon = 'home'; + testSubj = 'homeIcon'; + // Home icon should be medium to fit in with other icons + markIconSize = 'm'; + } + + const alt = `${applicationTitle} home`; + + return ( + + ); }; diff --git a/src/core/public/chrome/ui/header/home_loader.tsx b/src/core/public/chrome/ui/header/home_loader.tsx index 0083df43ff8b..9463b41060f7 100644 --- a/src/core/public/chrome/ui/header/home_loader.tsx +++ b/src/core/public/chrome/ui/header/home_loader.tsx @@ -39,6 +39,7 @@ import { ChromeNavLink } from '../..'; import { ChromeBranding } from '../../chrome_service'; import { LoadingIndicator } from '../loading_indicator'; import { HomeIcon } from './home_icon'; +import type { Logos } from '../../../../common/types'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -107,9 +108,10 @@ interface Props { loadingCount$: Observable; navigateToApp: (appId: string) => void; branding: ChromeBranding; + logos: Logos; } -export function HomeLoader({ href, navigateToApp, branding, ...observables }: Props) { +export function HomeLoader({ href, navigateToApp, branding, logos, ...observables }: Props) { const forceNavigation = useObservable(observables.forceNavigation$, false); const navLinks = useObservable(observables.navLinks$, []); const loadingCount = useObservable(observables.loadingCount$, 0); @@ -130,7 +132,7 @@ export function HomeLoader({ href, navigateToApp, branding, ...observables }: Pr > {!(loadingCount > 0) && (
- +
)}
diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index efc8d4aa31bb..20f070dbba80 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -825,4 +825,39 @@ describe('Fetch', () => { expect(usedSpy).toHaveBeenCalledTimes(2); }); }); + + describe('long numerals', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + + it('should use alternate parser on JSON responses when asked to', async () => { + fetchMock.get('*', { + body: `{"long-max": ${longPositive}, "long-min": ${longNegative}}`, + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + + await expect(fetchInstance.fetch('/my/path', { withLongNumerals: true })).resolves.toEqual({ + 'long-max': longPositive, + 'long-min': longNegative, + }); + }); + + it('should use alternate parser on non-JSON responses when asked to', async () => { + fetchMock.get('*', { + body: `{"long-max": ${longPositive}, "long-min": ${longNegative}}`, + status: 200, + headers: { + 'Content-Type': 'text', + }, + }); + + await expect(fetchInstance.fetch('/my/path', { withLongNumerals: true })).resolves.toEqual({ + 'long-max': longPositive, + 'long-min': longNegative, + }); + }); + }); }); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 694372c46d99..767d58643003 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -31,7 +31,7 @@ import { omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; -import { isRelativeUrl } from '@osd/std'; +import { isRelativeUrl, parse } from '@osd/std'; import { IBasePath, @@ -190,12 +190,12 @@ export class Fetch { if (NDJSON_CONTENT.test(contentType)) { body = await response.blob(); } else if (JSON_CONTENT.test(contentType)) { - body = await response.json(); + body = fetchOptions.withLongNumerals ? parse(await response.text()) : await response.json(); } else { const text = await response.text(); try { - body = JSON.parse(text); + body = fetchOptions.withLongNumerals ? parse(text) : JSON.parse(text); } catch (err) { body = text; } diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index ab046e6d2d5a..3b7dff71c811 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -257,6 +257,12 @@ export interface HttpFetchOptions extends HttpRequestInit { * response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. */ asResponse?: boolean; + + /** + * When `true`, if the response has a JSON mime type, the {@link HttpResponse} will use an alternate JSON parser + * that converts long numerals to BigInts. Defaults to `false`. + */ + withLongNumerals?: boolean; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 9a38771f513e..03ef6b6392f9 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -50,7 +50,6 @@ import './index.scss'; import { ChromeBadge, - ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeHelpExtensionMenuLink, @@ -89,6 +88,7 @@ import { } from './context'; import { Branding } from '../types'; +export type { Logos } from '../common'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ export { CoreContext, CoreSystem } from './core_system'; @@ -193,7 +193,7 @@ export { ErrorToastOptions, } from './notifications'; -export { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; +export { MountPoint, UnmountCallback, PublicUiSettingsParams, ChromeBrand } from './types'; export { URL_MAX_LENGTH } from './core_app'; @@ -298,7 +298,6 @@ export interface CoreStart { export { Capabilities, ChromeBadge, - ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeHelpExtensionMenuLink, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 5fa3bf888b5c..b2cf4e8880cf 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -84,7 +84,7 @@ function createManifest( version: 'some-version', configPath: ['path'], requiredPlugins: required, - requiredOpenSearchPlugins: optional, + requiredEnginePlugins: optional, optionalPlugins: optional, requiredBundles: [], }; diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 4ef3313cc772..24e3bfb8a590 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -13,9 +13,6 @@ @import "@elastic/charts/dist/theme"; @import "@elastic/eui/src/themes/charts/theme"; -// Grab some nav-specific EUI vars -@import "@elastic/eui/src/components/collapsible_nav/variables"; - // Application Layout $euiCollapsibleNavWidth: $euiSize * 20; diff --git a/src/core/public/types.ts b/src/core/public/types.ts index 0c1d6b70c728..ebbdd53f3cf5 100644 --- a/src/core/public/types.ts +++ b/src/core/public/types.ts @@ -37,6 +37,8 @@ export { StringValidationRegex, } from '../../core/types'; +export type { Logos } from '../common/types'; + /** * A function that should mount DOM content inside the provided container element * and return a handler to unmount it. @@ -55,3 +57,12 @@ export type MountPoint = (element: T) => Un * @public */ export type UnmountCallback = () => void; + +/** + * @deprecated: Use Branding instead + * @public + */ +export interface ChromeBrand { + logo?: string; + smallLogo?: string; +} diff --git a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png index 3fb291f8830c..a256e84b28b8 100644 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-192x192.png and b/src/core/server/core_app/assets/favicons/android-chrome-192x192.png differ diff --git a/src/core/server/core_app/assets/favicons/android-chrome-512x512.png b/src/core/server/core_app/assets/favicons/android-chrome-512x512.png index 47bc1a2bd52b..404fbfad398f 100644 Binary files a/src/core/server/core_app/assets/favicons/android-chrome-512x512.png and b/src/core/server/core_app/assets/favicons/android-chrome-512x512.png differ diff --git a/src/core/server/core_app/assets/favicons/apple-touch-icon.png b/src/core/server/core_app/assets/favicons/apple-touch-icon.png index 3fdcfd3f09ed..00b8c190b08c 100644 Binary files a/src/core/server/core_app/assets/favicons/apple-touch-icon.png and b/src/core/server/core_app/assets/favicons/apple-touch-icon.png differ diff --git a/src/core/server/core_app/assets/favicons/favicon-32x32.png b/src/core/server/core_app/assets/favicons/favicon-32x32.png index ce2d8fe187fe..3046f5df76f1 100644 Binary files a/src/core/server/core_app/assets/favicons/favicon-32x32.png and b/src/core/server/core_app/assets/favicons/favicon-32x32.png differ diff --git a/src/core/server/core_app/assets/favicons/manifest.json b/src/core/server/core_app/assets/favicons/manifest.json index 9153b046420b..1318668560ac 100644 --- a/src/core/server/core_app/assets/favicons/manifest.json +++ b/src/core/server/core_app/assets/favicons/manifest.json @@ -3,12 +3,12 @@ "short_name": "", "icons": [ { - "src": "/android-chrome-192x192.png", + "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/android-chrome-512x512.png", + "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } diff --git a/src/core/server/core_app/assets/favicons/mstile-144x144.png b/src/core/server/core_app/assets/favicons/mstile-144x144.png index 577ecfed1c02..5a379489e086 100644 Binary files a/src/core/server/core_app/assets/favicons/mstile-144x144.png and b/src/core/server/core_app/assets/favicons/mstile-144x144.png differ diff --git a/src/core/server/core_app/assets/favicons/mstile-150x150.png b/src/core/server/core_app/assets/favicons/mstile-150x150.png index 938310ed5537..b064558a1eda 100644 Binary files a/src/core/server/core_app/assets/favicons/mstile-150x150.png and b/src/core/server/core_app/assets/favicons/mstile-150x150.png differ diff --git a/src/core/server/core_app/assets/favicons/mstile-310x150.png b/src/core/server/core_app/assets/favicons/mstile-310x150.png index 6be5bf986934..d938be9ee090 100644 Binary files a/src/core/server/core_app/assets/favicons/mstile-310x150.png and b/src/core/server/core_app/assets/favicons/mstile-310x150.png differ diff --git a/src/core/server/core_app/assets/favicons/mstile-310x310.png b/src/core/server/core_app/assets/favicons/mstile-310x310.png index b45858ef4bad..0790cc1a7f5f 100644 Binary files a/src/core/server/core_app/assets/favicons/mstile-310x310.png and b/src/core/server/core_app/assets/favicons/mstile-310x310.png differ diff --git a/src/core/server/core_app/assets/favicons/mstile-70x70.png b/src/core/server/core_app/assets/favicons/mstile-70x70.png index 399a42ff6ae5..5155640f0976 100644 Binary files a/src/core/server/core_app/assets/favicons/mstile-70x70.png and b/src/core/server/core_app/assets/favicons/mstile-70x70.png differ diff --git a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg b/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg index 6ea5f91851f0..5f8b7cfd8576 100644 --- a/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg +++ b/src/core/server/core_app/assets/favicons/safari-pinned-tab.svg @@ -1,45 +1 @@ - - - - -Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - - - + diff --git a/src/core/server/core_app/assets/logos/opensearch.svg b/src/core/server/core_app/assets/logos/opensearch.svg new file mode 100644 index 000000000000..9795f6c332ab --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_center_mark.svg b/src/core/server/core_app/assets/logos/opensearch_center_mark.svg new file mode 100644 index 000000000000..42a29b55050c --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_center_mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_center_mark_on_dark.svg b/src/core/server/core_app/assets/logos/opensearch_center_mark_on_dark.svg new file mode 100644 index 000000000000..43091f7d039a --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_center_mark_on_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_center_mark_on_light.svg b/src/core/server/core_app/assets/logos/opensearch_center_mark_on_light.svg new file mode 100644 index 000000000000..5a0d83c568b7 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_center_mark_on_light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_dashboards.svg b/src/core/server/core_app/assets/logos/opensearch_dashboards.svg index bb85dcdd10ed..35f56544a26c 100644 --- a/src/core/server/core_app/assets/logos/opensearch_dashboards.svg +++ b/src/core/server/core_app/assets/logos/opensearch_dashboards.svg @@ -1,5 +1,5 @@ - + diff --git a/src/core/server/core_app/assets/logos/opensearch_dashboards_darkmode.svg b/src/core/server/core_app/assets/logos/opensearch_dashboards_on_dark.svg similarity index 100% rename from src/core/server/core_app/assets/logos/opensearch_dashboards_darkmode.svg rename to src/core/server/core_app/assets/logos/opensearch_dashboards_on_dark.svg diff --git a/src/core/server/core_app/assets/logos/opensearch_dashboards_on_light.svg b/src/core/server/core_app/assets/logos/opensearch_dashboards_on_light.svg new file mode 100644 index 000000000000..bb85dcdd10ed --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_dashboards_on_light.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_mark.svg b/src/core/server/core_app/assets/logos/opensearch_mark.svg new file mode 100644 index 000000000000..b1986db87913 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_mark_on_dark.svg b/src/core/server/core_app/assets/logos/opensearch_mark_on_dark.svg new file mode 100644 index 000000000000..d202064dea30 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_mark_on_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_mark_on_light.svg b/src/core/server/core_app/assets/logos/opensearch_mark_on_light.svg new file mode 100644 index 000000000000..2c6bc1ee17e0 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_mark_on_light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_on_dark.svg b/src/core/server/core_app/assets/logos/opensearch_on_dark.svg new file mode 100644 index 000000000000..1830ff7f6683 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_on_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_on_light.svg b/src/core/server/core_app/assets/logos/opensearch_on_light.svg new file mode 100644 index 000000000000..f716c67e58f9 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_on_light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_spinner.svg b/src/core/server/core_app/assets/logos/opensearch_spinner.svg new file mode 100644 index 000000000000..98c6f2af6189 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_spinner.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_spinner_on_dark.svg b/src/core/server/core_app/assets/logos/opensearch_spinner_on_dark.svg new file mode 100644 index 000000000000..8d2b16595121 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_spinner_on_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/core/server/core_app/assets/logos/opensearch_spinner_on_light.svg b/src/core/server/core_app/assets/logos/opensearch_spinner_on_light.svg new file mode 100644 index 000000000000..41ab3c960b94 --- /dev/null +++ b/src/core/server/core_app/assets/logos/opensearch_spinner_on_light.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/core/server/opensearch/client/cluster_client.test.ts b/src/core/server/opensearch/client/cluster_client.test.ts index 1510d2b148fe..81f55b987805 100644 --- a/src/core/server/opensearch/client/cluster_client.test.ts +++ b/src/core/server/opensearch/client/cluster_client.test.ts @@ -99,8 +99,16 @@ describe('ClusterClient', () => { const scopedClusterClient = clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ headers: expect.any(Object) }); + const expected = { headers: expect.any(Object) }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); expect(scopedClusterClient.asInternalUser).toBe(clusterClient.asInternalUser); expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value); @@ -113,7 +121,7 @@ describe('ClusterClient', () => { const scopedClusterClient1 = clusterClient.asScoped(request); const scopedClusterClient2 = clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenCalledTimes(2 * 2); expect(scopedClusterClient1).not.toBe(scopedClusterClient2); expect(scopedClusterClient1.asInternalUser).toBe(scopedClusterClient2.asInternalUser); @@ -135,10 +143,18 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, foo: 'bar', 'x-opaque-id': expect.any(String) }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('creates a scoped facade with filtered auth headers', () => { @@ -155,10 +171,18 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('respects auth headers precedence', () => { @@ -179,10 +203,18 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('includes the `customHeaders` from the config without filtering them', () => { @@ -200,15 +232,23 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, foo: 'bar', hello: 'dolly', 'x-opaque-id': expect.any(String), }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('adds the x-opaque-id header based on the request id', () => { @@ -225,13 +265,21 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, 'x-opaque-id': 'my-fake-id', }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('respect the precedence of auth headers over config headers', () => { @@ -251,15 +299,23 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, foo: 'auth', hello: 'dolly', 'x-opaque-id': expect.any(String), }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('respect the precedence of request headers over config headers', () => { @@ -279,15 +335,23 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, foo: 'request', hello: 'dolly', 'x-opaque-id': expect.any(String), }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('respect the precedence of config headers over default headers', () => { @@ -304,13 +368,21 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { [headerKey]: 'foo', 'x-opaque-id': expect.any(String), }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('respect the precedence of request headers over default headers', () => { @@ -327,13 +399,21 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { [headerKey]: 'foo', 'x-opaque-id': expect.any(String), }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('respect the precedence of x-opaque-id header over config headers', () => { @@ -355,13 +435,21 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, 'x-opaque-id': 'from request', }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('filter headers when called with a `FakeRequest`', () => { @@ -380,10 +468,18 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, authorization: 'auth' }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); it('does not add auth headers when called with a `FakeRequest`', () => { @@ -404,10 +500,18 @@ describe('ClusterClient', () => { clusterClient.asScoped(request); - expect(scopedClient.child).toHaveBeenCalledTimes(1); - expect(scopedClient.child).toHaveBeenCalledWith({ + const expected = { headers: { ...DEFAULT_HEADERS, foo: 'bar' }, - }); + }; + expect(scopedClient.child).toHaveBeenCalledTimes(2); + expect(scopedClient.child).toHaveBeenNthCalledWith(1, expect.objectContaining(expected)); + expect(scopedClient.child).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + ...expected, + enableLongNumeralSupport: true, + }) + ); }); }); diff --git a/src/core/server/opensearch/client/cluster_client.ts b/src/core/server/opensearch/client/cluster_client.ts index b29eea89d4db..8ea87bb910f7 100644 --- a/src/core/server/opensearch/client/cluster_client.ts +++ b/src/core/server/opensearch/client/cluster_client.ts @@ -90,10 +90,27 @@ export class ClusterClient implements ICustomClusterClient { asScoped(request: ScopeableRequest) { const scopedHeaders = this.getScopedHeaders(request); + const scopedClient = this.rootScopedClient.child({ headers: scopedHeaders, }); - return new ScopedClusterClient(this.asInternalUser, scopedClient); + + const asInternalUserWithLongNumeralsSupport = this.asInternalUser.child({ + // @ts-expect-error - Remove ignoring after https://github.com/opensearch-project/opensearch-js/pull/598 is included in a release + enableLongNumeralSupport: true, + }); + + const scopedClientWithLongNumeralsSupport = this.rootScopedClient.child({ + headers: scopedHeaders, + // @ts-expect-error - Remove ignoring after https://github.com/opensearch-project/opensearch-js/pull/598 is included in a release + enableLongNumeralSupport: true, + }); + return new ScopedClusterClient( + this.asInternalUser, + scopedClient, + asInternalUserWithLongNumeralsSupport, + scopedClientWithLongNumeralsSupport + ); } public async close() { @@ -101,7 +118,7 @@ export class ClusterClient implements ICustomClusterClient { return; } this.isClosed = true; - await Promise.all([this.asInternalUser.close(), this.rootScopedClient.close()]); + await Promise.all([this.asInternalUser.close(noop), this.rootScopedClient.close(noop)]); } private getScopedHeaders(request: ScopeableRequest): Headers { diff --git a/src/core/server/opensearch/client/mocks.ts b/src/core/server/opensearch/client/mocks.ts index 7b055d5b03cf..40b731f0f3bf 100644 --- a/src/core/server/opensearch/client/mocks.ts +++ b/src/core/server/opensearch/client/mocks.ts @@ -112,12 +112,16 @@ const createClientMock = (): OpenSearchClientMock => export interface ScopedClusterClientMock { asInternalUser: OpenSearchClientMock; asCurrentUser: OpenSearchClientMock; + asInternalUserWithLongNumeralsSupport: OpenSearchClientMock; + asCurrentUserWithLongNumeralsSupport: OpenSearchClientMock; } const createScopedClusterClientMock = () => { const mock: ScopedClusterClientMock = { asInternalUser: createClientMock(), asCurrentUser: createClientMock(), + asInternalUserWithLongNumeralsSupport: createClientMock(), + asCurrentUserWithLongNumeralsSupport: createClientMock(), }; return mock; @@ -126,12 +130,16 @@ const createScopedClusterClientMock = () => { export interface ClusterClientMock { asInternalUser: OpenSearchClientMock; asScoped: jest.MockedFunction<() => ScopedClusterClientMock>; + asInternalUserWithLongNumeralsSupport: OpenSearchClientMock; + asCurrentUserWithLongNumeralsSupport: jest.MockedFunction<() => ScopedClusterClientMock>; } const createClusterClientMock = () => { const mock: ClusterClientMock = { asInternalUser: createClientMock(), asScoped: jest.fn(), + asInternalUserWithLongNumeralsSupport: createClientMock(), + asCurrentUserWithLongNumeralsSupport: jest.fn(), }; mock.asScoped.mockReturnValue(createScopedClusterClientMock()); @@ -145,6 +153,8 @@ const createCustomClusterClientMock = () => { const mock: CustomClusterClientMock = { asInternalUser: createClientMock(), asScoped: jest.fn(), + asInternalUserWithLongNumeralsSupport: createClientMock(), + asCurrentUserWithLongNumeralsSupport: jest.fn(), close: jest.fn(), }; diff --git a/src/core/server/opensearch/client/scoped_cluster_client.test.ts b/src/core/server/opensearch/client/scoped_cluster_client.test.ts index 396075d35316..5e3d222c3be2 100644 --- a/src/core/server/opensearch/client/scoped_cluster_client.test.ts +++ b/src/core/server/opensearch/client/scoped_cluster_client.test.ts @@ -36,7 +36,12 @@ describe('ScopedClusterClient', () => { const internalClient = opensearchClientMock.createOpenSearchClient(); const scopedClient = opensearchClientMock.createOpenSearchClient(); - const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); + const scopedClusterClient = new ScopedClusterClient( + internalClient, + scopedClient, + internalClient, + scopedClient + ); expect(scopedClusterClient.asInternalUser).toBe(internalClient); }); @@ -45,7 +50,12 @@ describe('ScopedClusterClient', () => { const internalClient = opensearchClientMock.createOpenSearchClient(); const scopedClient = opensearchClientMock.createOpenSearchClient(); - const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); + const scopedClusterClient = new ScopedClusterClient( + internalClient, + scopedClient, + internalClient, + scopedClient + ); expect(scopedClusterClient.asCurrentUser).toBe(scopedClient); }); diff --git a/src/core/server/opensearch/client/scoped_cluster_client.ts b/src/core/server/opensearch/client/scoped_cluster_client.ts index d4db3ce3606c..4453243bd3ba 100644 --- a/src/core/server/opensearch/client/scoped_cluster_client.ts +++ b/src/core/server/opensearch/client/scoped_cluster_client.ts @@ -49,12 +49,25 @@ export interface IScopedClusterClient { * on behalf of the user that initiated the request to the OpenSearch Dashboards server. */ readonly asCurrentUser: OpenSearchClient; + /** + * A {@link OpenSearchClient | client}, with support for long numerals, to be used to + * query the opensearch cluster on behalf of the internal OpenSearch Dashboards user. + */ + readonly asInternalUserWithLongNumeralsSupport: OpenSearchClient; + /** + * A {@link OpenSearchClient | client}, with support for long numerals, to be used to + * query the opensearch cluster on behalf of the user that initiated the request to + * the OpenSearch Dashboards server. + */ + readonly asCurrentUserWithLongNumeralsSupport: OpenSearchClient; } /** @internal **/ export class ScopedClusterClient implements IScopedClusterClient { constructor( public readonly asInternalUser: OpenSearchClient, - public readonly asCurrentUser: OpenSearchClient + public readonly asCurrentUser: OpenSearchClient, + public readonly asInternalUserWithLongNumeralsSupport: OpenSearchClient, + public readonly asCurrentUserWithLongNumeralsSupport: OpenSearchClient ) {} } diff --git a/src/core/server/opensearch/opensearch_config.ts b/src/core/server/opensearch/opensearch_config.ts index fee26c354fbe..7ef3a7665a97 100644 --- a/src/core/server/opensearch/opensearch_config.ts +++ b/src/core/server/opensearch/opensearch_config.ts @@ -53,7 +53,7 @@ export const configSchema = schema.object({ defaultValue: false, }), sniffOnConnectionFault: schema.boolean({ defaultValue: false }), - hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema, { minSize: 1 })], { + hosts: schema.oneOf([hostURISchema, schema.arrayOf(hostURISchema)], { defaultValue: 'http://localhost:9200', }), username: schema.maybe( diff --git a/src/core/server/opensearch/opensearch_service.test.ts b/src/core/server/opensearch/opensearch_service.test.ts index d5d354f9df86..1280479314a6 100644 --- a/src/core/server/opensearch/opensearch_service.test.ts +++ b/src/core/server/opensearch/opensearch_service.test.ts @@ -266,6 +266,30 @@ describe('#setup', () => { expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1); }); }); + + it('opensearchNodeVersionCompatibility$ avoid polling when opensearch hosts is empty', async () => { + const mockedClient = mockClusterClientInstance.asInternalUser; + configService.atPath.mockReturnValueOnce( + new BehaviorSubject({ + hosts: [], + healthCheck: { + delay: duration(2000), + }, + ssl: { + verificationMode: 'none', + }, + } as any) + ); + opensearchService = new OpenSearchService(coreContext); + const setupContract = await opensearchService.setup(setupDeps); + + // reset all mocks called during setup phase + MockLegacyClusterClient.mockClear(); + + setupContract.opensearchNodesCompatibility$.subscribe(async () => { + expect(mockedClient.nodes.info).toHaveBeenCalledTimes(0); + }); + }); }); describe('#start', () => { diff --git a/src/core/server/opensearch/opensearch_service.ts b/src/core/server/opensearch/opensearch_service.ts index 87a2da974ba1..bab3e7ede9f3 100644 --- a/src/core/server/opensearch/opensearch_service.ts +++ b/src/core/server/opensearch/opensearch_service.ts @@ -28,7 +28,7 @@ * under the License. */ -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject, of } from 'rxjs'; import { first, map, shareReplay, takeUntil } from 'rxjs/operators'; import { merge } from '@osd/std'; @@ -91,14 +91,26 @@ export class OpenSearchService this.legacyClient = this.createLegacyClusterClient('data', config); this.client = this.createClusterClient('data', config); - const opensearchNodesCompatibility$ = pollOpenSearchNodesVersion({ - internalClient: this.client.asInternalUser, - optimizedHealthcheck: config.optimizedHealthcheck, - log: this.log, - ignoreVersionMismatch: config.ignoreVersionMismatch, - opensearchVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), - opensearchDashboardsVersion: this.opensearchDashboardsVersion, - }).pipe(takeUntil(this.stop$), shareReplay({ refCount: true, bufferSize: 1 })); + let opensearchNodesCompatibility$; + if (config.hosts.length > 0) { + opensearchNodesCompatibility$ = pollOpenSearchNodesVersion({ + internalClient: this.client.asInternalUser, + optimizedHealthcheck: config.optimizedHealthcheck, + log: this.log, + ignoreVersionMismatch: config.ignoreVersionMismatch, + opensearchVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), + opensearchDashboardsVersion: this.opensearchDashboardsVersion, + }).pipe(takeUntil(this.stop$), shareReplay({ refCount: true, bufferSize: 1 })); + } else { + this.log.debug(`Opensearch is not configured.`); + opensearchNodesCompatibility$ = of({ + isCompatible: true, + message: 'Opensearch is not configured', + incompatibleNodes: [], + warningNodes: [], + opensearchDashboardsVersion: this.opensearchDashboardsVersion, + }); + } this.createLegacyCustomClient = (type, clientConfig = {}) => { const finalConfig = merge({}, config, clientConfig); diff --git a/src/core/server/opensearch/status.ts b/src/core/server/opensearch/status.ts index 6b5d2741b405..87eda3a8893f 100644 --- a/src/core/server/opensearch/status.ts +++ b/src/core/server/opensearch/status.ts @@ -77,7 +77,7 @@ export const calculateStatus$ = ( return { level: ServiceStatusLevels.available, - summary: `OpenSearch is available`, + summary: (message ?? `OpenSearch is available`) || `Unknown`, meta: { warningNodes: [], incompatibleNodes: [], diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts index 66fd1336a554..5ef01f88f3f7 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts @@ -247,72 +247,110 @@ test('return error when manifest contains unrecognized properties', async () => }); }); -describe('requiredOpenSearchPlugins', () => { - test('return error when plugin `requiredOpenSearchPlugins` is a string and not an array of string', async () => { +describe('requiredEnginePlugins', () => { + test('return error when plugin `requiredEnginePlugins` is a string and not an object', async () => { mockReadFilePromise.mockResolvedValue( Buffer.from( JSON.stringify({ - id: 'id1', + id: 'invalid-manifest-plugin', version: '7.0.0', server: true, - requiredOpenSearchPlugins: 'abc', + requiredEnginePlugins: 'abc', }) ) ); await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ - message: `The "requiredOpenSearchPlugins" in plugin manifest for "id1" should be an array of strings. (invalid-manifest, ${pluginManifestPath})`, + message: `The "requiredEnginePlugins" in plugin manifest for "invalid-manifest-plugin" should be an object that maps a plugin name to a version range. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); - test('return error when `requiredOpenSearchPlugins` is not a string', async () => { + test('return error when plugin `requiredEnginePlugins` is an array and not an object', async () => { mockReadFilePromise.mockResolvedValue( - Buffer.from(JSON.stringify({ id: 'id2', version: '7.0.0', requiredOpenSearchPlugins: 2 })) + Buffer.from( + JSON.stringify({ + id: 'invalid-manifest-plugin', + version: '7.0.0', + server: true, + requiredEnginePlugins: [{ 'plugin-1': '1.0.0.0', 'plugin-2': '^2.0.1' }], + }) + ) ); await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ - message: `The "requiredOpenSearchPlugins" in plugin manifest for "id2" should be an array of strings. (invalid-manifest, ${pluginManifestPath})`, + message: `The "requiredEnginePlugins" in plugin manifest for "invalid-manifest-plugin" should be an object that maps a plugin name to a version range. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); - test('return error when plugin requiredOpenSearchPlugins is an array that contains non-string values', async () => { + test('return error when plugin requiredEnginePlugins is an object but contains non-string version value', async () => { mockReadFilePromise.mockResolvedValue( Buffer.from( - JSON.stringify({ id: 'id3', version: '7.0.0', requiredOpenSearchPlugins: ['plugin1', 2] }) + JSON.stringify({ + id: 'test-invalid-version-type-plugin', + version: '7.0.0', + requiredEnginePlugins: { 'invalid-plugin-version-range': 2 }, + }) + ) + ); + + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `The "requiredEnginePlugins" in plugin manifest for "test-invalid-version-type-plugin" should be an object that maps a plugin name to a version range. (invalid-manifest, ${pluginManifestPath})`, + type: PluginDiscoveryErrorType.InvalidManifest, + path: pluginManifestPath, + }); + }); + + test('return error when plugin requiredEnginePlugins contains invalid version range', async () => { + mockReadFilePromise.mockResolvedValue( + Buffer.from( + JSON.stringify({ + id: 'test-invalid-version-range-plugin', + version: '7.0.0', + requiredEnginePlugins: { + 'invalid-version-range-plugin': '?1.0.0', + 'valid-version-range-plugin': '^1.0.0', + }, + }) ) ); await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ - message: `The "requiredOpenSearchPlugins" in plugin manifest for "id3" should be an array of strings. (invalid-manifest, ${pluginManifestPath})`, + message: `The "requiredEnginePlugins" in the plugin manifest for "test-invalid-version-range-plugin" contains invalid version ranges: ?1.0.0 for invalid-version-range-plugin (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); - test('Happy path when plugin `requiredOpenSearchPlugins` is an array of string', async () => { + test('Happy path when plugin `requiredEnginePlugins` is a map of plugin name to its compatible version ranges', async () => { mockReadFilePromise.mockResolvedValue( Buffer.from( JSON.stringify({ - id: 'id1', + id: 'test-plugin-version-range-sanity', version: '7.0.0', server: true, - requiredOpenSearchPlugins: ['plugin1', 'plugin2'], + requiredEnginePlugins: { + 'valid-plugin-1': '^1.0.0', + 'valid-plugin-2': '1.0.0', + }, }) ) ); await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ - id: 'id1', - configPath: 'id_1', + id: 'test-plugin-version-range-sanity', + configPath: 'test_plugin_version_range_sanity', version: '7.0.0', opensearchDashboardsVersion: '7.0.0', optionalPlugins: [], requiredPlugins: [], - requiredOpenSearchPlugins: ['plugin1', 'plugin2'], + requiredEnginePlugins: { + 'valid-plugin-1': '^1.0.0', + 'valid-plugin-2': '1.0.0', + }, requiredBundles: [], server: true, ui: false, @@ -374,7 +412,7 @@ test('set defaults for all missing optional fields', async () => { opensearchDashboardsVersion: '7.0.0', optionalPlugins: [], requiredPlugins: [], - requiredOpenSearchPlugins: [], + requiredEnginePlugins: {}, requiredBundles: [], server: true, ui: false, @@ -391,7 +429,10 @@ test('return all set optional fields as they are in manifest', async () => { opensearchDashboardsVersion: '7.0.0', requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], optionalPlugins: ['some-optional-plugin'], - requiredOpenSearchPlugins: ['test-opensearch-plugin-1', 'test-opensearch-plugin-2'], + requiredEnginePlugins: { + 'test-opensearch-plugin-1': '^1.0.0', + 'test-opensearch-plugin-2': '>=1.0.0', + }, ui: true, }) ) @@ -405,7 +446,10 @@ test('return all set optional fields as they are in manifest', async () => { optionalPlugins: ['some-optional-plugin'], requiredBundles: [], requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'], - requiredOpenSearchPlugins: ['test-opensearch-plugin-1', 'test-opensearch-plugin-2'], + requiredEnginePlugins: { + 'test-opensearch-plugin-1': '^1.0.0', + 'test-opensearch-plugin-2': '>=1.0.0', + }, server: false, ui: true, }); @@ -420,7 +464,7 @@ test('return manifest when plugin expected OpenSearch Dashboards version matches version: 'some-version', opensearchDashboardsVersion: '7.0.0-alpha2', requiredPlugins: ['some-required-plugin'], - requiredOpenSearchPlugins: [], + requiredEnginePlugins: {}, server: true, }) ) @@ -433,7 +477,7 @@ test('return manifest when plugin expected OpenSearch Dashboards version matches opensearchDashboardsVersion: '7.0.0-alpha2', optionalPlugins: [], requiredPlugins: ['some-required-plugin'], - requiredOpenSearchPlugins: [], + requiredEnginePlugins: {}, requiredBundles: [], server: true, ui: false, @@ -461,7 +505,7 @@ test('return manifest when plugin expected OpenSearch Dashboards version is `ope opensearchDashboardsVersion: 'opensearchDashboards', optionalPlugins: [], requiredPlugins: ['some-required-plugin'], - requiredOpenSearchPlugins: [], + requiredEnginePlugins: {}, requiredBundles: [], server: true, ui: true, diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index ee435d2e310a..cfc23f0dc197 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -29,6 +29,7 @@ */ import { readFile, stat } from 'fs/promises'; +import semver from 'semver'; import { resolve } from 'path'; import { coerce } from 'semver'; import { snakeCase } from 'lodash'; @@ -61,7 +62,7 @@ const KNOWN_MANIFEST_FIELDS = (() => { version: true, configPath: true, requiredPlugins: true, - requiredOpenSearchPlugins: true, + requiredEnginePlugins: true, optionalPlugins: true, ui: true, server: true, @@ -157,26 +158,42 @@ export async function parseManifest( ); } - if ( - manifest.requiredOpenSearchPlugins !== undefined && - (!Array.isArray(manifest.requiredOpenSearchPlugins) || - !manifest.requiredOpenSearchPlugins.every((plugin) => typeof plugin === 'string')) - ) { - throw PluginDiscoveryError.invalidManifest( - manifestPath, - new Error( - `The "requiredOpenSearchPlugins" in plugin manifest for "${manifest.id}" should be an array of strings.` + if ('requiredEnginePlugins' in manifest) { + if ( + typeof manifest.requiredEnginePlugins !== 'object' || + !Object.entries(manifest.requiredEnginePlugins).every( + ([pluginId, pluginVersion]) => + typeof pluginId === 'string' && typeof pluginVersion === 'string' ) - ); - } + ) { + throw PluginDiscoveryError.invalidManifest( + manifestPath, + new Error( + `The "requiredEnginePlugins" in plugin manifest for "${manifest.id}" should be an object that maps a plugin name to a version range.` + ) + ); + } + const invalidPluginVersions: string[] = []; + for (const [pluginName, versionRange] of Object.entries(manifest.requiredEnginePlugins)) { + log.info( + `Plugin ${manifest.id} has a dependency on engine plugin: [${pluginName}@${versionRange}]` + ); - if ( - Array.isArray(manifest.requiredOpenSearchPlugins) && - manifest.requiredOpenSearchPlugins.length > 0 - ) { - log.info( - `Plugin ${manifest.id} has a dependency on following OpenSearch plugin(s): "${manifest.requiredOpenSearchPlugins}".` - ); + if (!isOpenSearchPluginVersionRangeValid(versionRange)) { + invalidPluginVersions.push(`${versionRange} for ${pluginName}`); + } + } + + if (invalidPluginVersions.length > 0) { + throw PluginDiscoveryError.invalidManifest( + manifestPath, + new Error( + `The "requiredEnginePlugins" in the plugin manifest for "${ + manifest.id + }" contains invalid version ranges: ${invalidPluginVersions.join(', ')}` + ) + ); + } } const expectedOpenSearchDashboardsVersion = @@ -221,9 +238,8 @@ export async function parseManifest( opensearchDashboardsVersion: expectedOpenSearchDashboardsVersion, configPath: manifest.configPath || snakeCase(manifest.id), requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [], - requiredOpenSearchPlugins: Array.isArray(manifest.requiredOpenSearchPlugins) - ? manifest.requiredOpenSearchPlugins - : [], + requiredEnginePlugins: + manifest.requiredEnginePlugins !== undefined ? manifest.requiredEnginePlugins : {}, optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [], requiredBundles: Array.isArray(manifest.requiredBundles) ? manifest.requiredBundles : [], ui: includesUiPlugin, @@ -276,3 +292,14 @@ function isVersionCompatible( 0 ); } +/** + * Checks whether specified version range is valid. + * @param versionRange Version range to be checked. + */ +function isOpenSearchPluginVersionRangeValid(versionRange: string) { + try { + return semver.validRange(versionRange); + } catch (err) { + return false; + } +} diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index 4d1660f65c75..ea400ddcd913 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -42,7 +42,7 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { Plugin } from '../types'; +import { Plugin, CompatibleEnginePluginVersions } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { @@ -57,7 +57,7 @@ describe('PluginsService', () => { disabled = false, version = 'some-version', requiredPlugins = [], - requiredOpenSearchPlugins = [], + requiredEnginePlugins = {}, requiredBundles = [], optionalPlugins = [], opensearchDashboardsVersion = '7.0.0', @@ -69,7 +69,7 @@ describe('PluginsService', () => { disabled?: boolean; version?: string; requiredPlugins?: string[]; - requiredOpenSearchPlugins?: string[]; + requiredEnginePlugins?: CompatibleEnginePluginVersions; requiredBundles?: string[]; optionalPlugins?: string[]; opensearchDashboardsVersion?: string; @@ -86,7 +86,7 @@ describe('PluginsService', () => { configPath: `${configPath}${disabled ? '-disabled' : ''}`, opensearchDashboardsVersion, requiredPlugins, - requiredOpenSearchPlugins, + requiredEnginePlugins, requiredBundles, optionalPlugins, server, diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 04df84ab8d12..69b781031c7a 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -69,7 +69,10 @@ function createPluginManifest(manifestProps: Partial = {}): Plug configPath: 'path', opensearchDashboardsVersion: '7.0.0', requiredPlugins: ['some-required-dep'], - requiredOpenSearchPlugins: ['some-os-plugins'], + requiredEnginePlugins: { + 'test-os-plugin1': '^2.2.1', + 'test-os-plugin2': '2.2.1 || 2.2.2', + }, optionalPlugins: ['some-optional-dep'], requiredBundles: [], server: true, diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index cc53e1b443e9..0aa13e53d650 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -66,7 +66,7 @@ export class PluginWrapper< public readonly configPath: PluginManifest['configPath']; public readonly requiredPlugins: PluginManifest['requiredPlugins']; public readonly optionalPlugins: PluginManifest['optionalPlugins']; - public readonly requiredOpenSearchPlugins: PluginManifest['requiredOpenSearchPlugins']; + public readonly requiredEnginePlugins: PluginManifest['requiredEnginePlugins']; public readonly requiredBundles: PluginManifest['requiredBundles']; public readonly includesServerPlugin: PluginManifest['server']; public readonly includesUiPlugin: PluginManifest['ui']; @@ -96,7 +96,7 @@ export class PluginWrapper< this.configPath = params.manifest.configPath; this.requiredPlugins = params.manifest.requiredPlugins; this.optionalPlugins = params.manifest.optionalPlugins; - this.requiredOpenSearchPlugins = params.manifest.requiredOpenSearchPlugins; + this.requiredEnginePlugins = params.manifest.requiredEnginePlugins; this.requiredBundles = params.manifest.requiredBundles; this.includesServerPlugin = params.manifest.server; this.includesUiPlugin = params.manifest.ui; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index ca46a97a237e..48c9eb6d6823 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -56,7 +56,9 @@ function createPluginManifest(manifestProps: Partial = {}): Plug configPath: 'path', opensearchDashboardsVersion: '7.0.0', requiredPlugins: ['some-required-dep'], - requiredOpenSearchPlugins: ['some-backend-plugin'], + requiredEnginePlugins: { + 'test-os-plugin1': '^2.2.1', + }, requiredBundles: [], optionalPlugins: ['some-optional-dep'], server: true, diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index c3ee05d60ed8..36c594908845 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -47,7 +47,7 @@ import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; import { take } from 'rxjs/operators'; -import { DiscoveredPlugin } from './types'; +import { DiscoveredPlugin, CompatibleEnginePluginVersions } from './types'; const { join } = posix; const MockPluginsSystem: jest.Mock = PluginsSystem as any; @@ -78,7 +78,7 @@ const createPlugin = ( disabled = false, version = 'some-version', requiredPlugins = [], - requiredOpenSearchPlugins = [], + requiredEnginePlugins = {}, requiredBundles = [], optionalPlugins = [], opensearchDashboardsVersion = '7.0.0', @@ -90,7 +90,7 @@ const createPlugin = ( disabled?: boolean; version?: string; requiredPlugins?: string[]; - requiredOpenSearchPlugins?: string[]; + requiredEnginePlugins?: CompatibleEnginePluginVersions; requiredBundles?: string[]; optionalPlugins?: string[]; opensearchDashboardsVersion?: string; @@ -107,7 +107,7 @@ const createPlugin = ( configPath: `${configPath}${disabled ? '-disabled' : ''}`, opensearchDashboardsVersion, requiredPlugins, - requiredOpenSearchPlugins, + requiredEnginePlugins, requiredBundles, optionalPlugins, server, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 286bb0d97846..dba7388f83b3 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -42,7 +42,7 @@ import { CoreContext } from '../core_context'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { PluginWrapper } from './plugin'; -import { PluginName } from './types'; +import { PluginName, CompatibleEnginePluginVersions } from './types'; import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; @@ -53,13 +53,13 @@ function createPlugin( { required = [], optional = [], - requiredOSPlugin = [], + requiredOSPlugin = {}, server = true, ui = true, }: { required?: string[]; optional?: string[]; - requiredOSPlugin?: string[]; + requiredOSPlugin?: CompatibleEnginePluginVersions; server?: boolean; ui?: boolean; } = {} @@ -72,7 +72,7 @@ function createPlugin( configPath: 'path', opensearchDashboardsVersion: '7.0.0', requiredPlugins: required, - requiredOpenSearchPlugins: requiredOSPlugin, + requiredEnginePlugins: requiredOSPlugin, optionalPlugins: optional, requiredBundles: [], server, @@ -195,7 +195,12 @@ test('correctly orders plugins and returns exposed values for "setup" and "start } const plugins = new Map([ [ - createPlugin('order-4', { required: ['order-2'], requiredOSPlugin: ['test-plugin'] }), + createPlugin('order-4', { + required: ['order-2'], + requiredOSPlugin: { + 'test-plugin-1': '^1.1.1', + }, + }), { setup: { 'order-2': 'added-as-2' }, start: { 'order-2': 'started-as-2' }, @@ -253,12 +258,12 @@ test('correctly orders plugins and returns exposed values for "setup" and "start ); const opensearch = startDeps.opensearch; - opensearch.client.asInternalUser.cat.plugins.mockResolvedValue({ + opensearch.client.asInternalUser.cat.plugins.mockResolvedValueOnce({ body: [ { name: 'node-1', - component: 'test-plugin', - version: 'v1', + component: 'test-plugin-1', + version: '1.9.9', }, ], } as any); @@ -509,7 +514,7 @@ describe('start', () => { { name: 'node-1', component: 'test-plugin', - version: 'v1', + version: '2.1.0', }, ], } as any); @@ -549,8 +554,12 @@ describe('start', () => { it('validates opensearch plugin installation when dependency is fulfilled', async () => { [ - createPlugin('order-1', { requiredOSPlugin: ['test-plugin'] }), - createPlugin('order-2'), + createPlugin('dependency-fulfilled-plugin', { + requiredOSPlugin: { + 'test-plugin': '^2.0.0', + }, + }), + createPlugin('no-dependency-plugin'), ].forEach((plugin, index) => { jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`); jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`); @@ -561,12 +570,18 @@ describe('start', () => { const pluginsStart = await pluginsSystem.startPlugins(startDeps); expect(pluginsStart).toBeInstanceOf(Map); expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1); + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).toHaveBeenCalledTimes(0); }); it('validates opensearch plugin installation and does not error out when plugin is not installed', async () => { [ - createPlugin('id-1', { requiredOSPlugin: ['missing-opensearch-dep'] }), - createPlugin('id-2'), + createPlugin('dependency-missing-plugin', { + requiredOSPlugin: { + 'missing-opensearch-dep': '^2.0.0', + }, + }), + createPlugin('no-dependency-plugin'), ].forEach((plugin, index) => { jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`); jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`); @@ -577,17 +592,51 @@ describe('start', () => { const pluginsStart = await pluginsSystem.startPlugins(startDeps); expect(pluginsStart).toBeInstanceOf(Map); expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1); + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn).toHaveBeenCalledWith( + `OpenSearch plugin "missing-opensearch-dep" is not installed on the engine for the OpenSearch Dashboards plugin to function as expected.` + ); }); - it('validates opensearch plugin installation and does not error out when there is no dependency', async () => { - [createPlugin('id-1'), createPlugin('id-2')].forEach((plugin, index) => { + it('validates opensearch plugin installation and log warning when plugin exist but version is incompatible', async () => { + [ + createPlugin('version-mismatch-plugin', { + requiredOSPlugin: { + 'test-plugin-version-mismatch': '^1.0.0', + }, + }), + createPlugin('no-dependency-plugin'), + ].forEach((plugin, index) => { jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`); jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`); pluginsSystem.addPlugin(plugin); }); + + await pluginsSystem.setupPlugins(setupDeps); + const pluginsStart = await pluginsSystem.startPlugins(startDeps); + expect(pluginsStart).toBeInstanceOf(Map); + expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1); + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn).toHaveBeenCalledWith( + `OpenSearch plugin "test-plugin-version-mismatch" is not installed on the engine for the OpenSearch Dashboards plugin to function as expected.` + ); + }); + + it('validates opensearch plugin installation and does not warn when there is no dependency', async () => { + [createPlugin('no-dependency-plugin-1'), createPlugin('no-dependency-plugin-2')].forEach( + (plugin, index) => { + jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`); + jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`); + pluginsSystem.addPlugin(plugin); + } + ); await pluginsSystem.setupPlugins(setupDeps); const pluginsStart = await pluginsSystem.startPlugins(startDeps); expect(pluginsStart).toBeInstanceOf(Map); expect(opensearch.client.asInternalUser.cat.plugins).toHaveBeenCalledTimes(1); + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).toHaveBeenCalledTimes(0); }); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 6445f38f8e70..07aa88e9db31 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -29,6 +29,8 @@ */ import { withTimeout } from '@osd/std'; +import semver from 'semver'; +import { CatPluginsResponse } from '@opensearch-project/opensearch/api/types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; @@ -167,21 +169,40 @@ export class PluginsSystem { private async healthCheckOpenSearchPlugins(deps: PluginsServiceStartDeps) { // make _cat/plugins?format=json call to the OpenSearch instance - const opensearchPlugins = await this.getOpenSearchPlugins(deps); + const opensearchInstalledPlugins = await this.getOpenSearchPlugins(deps); for (const pluginName of this.satupPlugins) { this.log.debug(`For plugin "${pluginName}"...`); const plugin = this.plugins.get(pluginName)!; - const pluginBackendDeps = new Set([...plugin.requiredOpenSearchPlugins]); - pluginBackendDeps.forEach((opensearchPlugin) => { - if (!opensearchPlugins.find((obj) => obj.component === opensearchPlugin)) { + const pluginOpenSearchDeps = Object.entries(plugin.requiredEnginePlugins); + for (const [enginePluginName, versionRange] of pluginOpenSearchDeps) { + // add check to see if the installing Dashboards plugin version is compatible with installed OpenSearch plugin + if ( + !this.isVersionCompatibleOSPluginInstalled( + opensearchInstalledPlugins, + enginePluginName, + versionRange + ) + ) { this.log.warn( - `OpenSearch plugin "${opensearchPlugin}" is not installed on the cluster for the OpenSearch Dashboards plugin to function as expected.` + `OpenSearch plugin "${enginePluginName}" is not installed on the engine for the OpenSearch Dashboards plugin to function as expected.` ); } - }); + } } } + private isVersionCompatibleOSPluginInstalled( + opensearchInstalledPlugins: CatPluginsResponse, + depPlugin: string, + versionRange: string + ) { + return opensearchInstalledPlugins.find( + (obj) => + obj.component === depPlugin && + semver.satisfies(semver.coerce(obj.version)!.version, versionRange) + ); + } + private async getOpenSearchPlugins(deps: PluginsServiceStartDeps) { // Makes cat.plugin api call to fetch list of OpenSearch plugins installed on the cluster try { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 12b28f48f237..66c8115efc4a 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -105,6 +105,9 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; +/** @public */ +export type CompatibleEnginePluginVersions = Record; + /** @internal */ export interface PluginDependencies { asNames: ReadonlyMap; @@ -155,10 +158,10 @@ export interface PluginManifest { readonly requiredPlugins: readonly PluginName[]; /** - * An optional list of component names of the backend OpenSearch plugins that **must be** installed on the cluster + * An optional list of the OpenSearch plugins that **must be** installed on the cluster * for this plugin to function properly. */ - readonly requiredOpenSearchPlugins: readonly PluginName[]; + readonly requiredEnginePlugins: CompatibleEnginePluginVersions; /** * List of plugin ids that this plugin's UI code imports modules from that are diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index c7c03c1eb72c..d0a62555d4b2 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -198,7 +198,7 @@ export class RenderingService { /** * Assign values for branding related configurations based on branding validation - * by calling checkBrandingValid(). For dark mode URLs, add additonal validation + * by calling checkBrandingValid(). For dark mode URLs, add additional validation * to see if there is a valid default mode URL exist first. If URL is valid, pass in * the actual URL; if not, pass in undefined. * diff --git a/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap b/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap index 87a00f601a44..36d073992ec8 100644 --- a/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap +++ b/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap @@ -13,7 +13,9 @@ Array [ content="width=device-width" name="viewport" />, - , + + OpenSearch Dashboards + , , , @@ -80,9 +82,12 @@ Array [ class="loadingLogoContainer" >
@@ -234,26 +231,13 @@ Array [ id="osd_legacy_browser_error" style="display:none" > - - - - - +

@@ -289,7 +273,9 @@ Array [ content="width=device-width" name="viewport" />, - , + <title> + OpenSearch Dashboards + , , , @@ -356,9 +342,12 @@ Array [ class="loadingLogoContainer" >

@@ -516,26 +497,13 @@ Array [ id="osd_legacy_browser_error" style="display:none" > - - - - - +

@@ -571,7 +539,9 @@ Array [ content="width=device-width" name="viewport" />, - , + <title> + OpenSearch Dashboards + , , , @@ -634,36 +604,18 @@ Array [ class="osdLoaderWrap" data-test-subj="loadingLogo" > - - - - - - - - +

@@ -815,26 +757,13 @@ Array [ id="osd_legacy_browser_error" style="display:none" > - - - - - +

@@ -895,7 +824,7 @@ Array [ rel="manifest" />, , @@ -941,6 +870,9 @@ Array [

@@ -958,26 +890,13 @@ Array [ id="osd_legacy_browser_error" style="display:none" > - - - - - +

@@ -1038,7 +957,7 @@ Array [ rel="manifest" />, , @@ -1078,36 +997,18 @@ Array [ class="osdLoaderWrap" data-test-subj="loadingLogo" > - - - - - - - - +