From d25248f4beb05eca6388046658c1f81fc2ce2a6f Mon Sep 17 00:00:00 2001 From: Miki Date: Fri, 4 Aug 2023 23:09:34 -0700 Subject: [PATCH] Refactor logo usage Also: * Move logos to a central location * Make the loading spinner color-scheme-aware * Recreate `OverviewPageHeader`, `HomeIcon`, `HeaderLogo`, `SolutionTitle`, `Welcome`, `Overview` tests * Enhance `ExitFullScreenButton`, `Header` tests * Tinified favicon assets Signed-off-by: Miki --- CHANGELOG.md | 1 + .../src/serializers/flat_object_serializer.ts | 93 + .../osd-dev-utils/src/serializers/index.ts | 1 + src/core/common/index.ts | 7 + .../__snapshots__/get_logos.test.ts.snap | 613 +++++ src/core/common/logos/constants.ts | 15 + src/core/common/logos/get_logos.mock.ts | 27 + src/core/common/logos/get_logos.test.ts | 133 + src/core/common/logos/get_logos.ts | 142 ++ src/core/common/logos/index.ts | 9 + src/core/common/logos/types.ts | 34 + src/core/common/mocks.ts | 6 + src/core/common/types.ts | 6 + src/core/public/chrome/chrome_service.mock.ts | 7 +- src/core/public/chrome/chrome_service.test.ts | 32 - src/core/public/chrome/chrome_service.tsx | 52 +- src/core/public/chrome/index.ts | 1 - .../collapsible_nav.test.tsx.snap | 1700 +++++-------- .../header/__snapshots__/header.test.tsx.snap | 838 +++++-- .../__snapshots__/header_logo.test.tsx.snap | 2209 +---------------- .../__snapshots__/home_icon.test.tsx.snap | 2034 +-------------- .../chrome/ui/header/collapsible_nav.test.tsx | 156 +- .../chrome/ui/header/collapsible_nav.tsx | 44 +- .../public/chrome/ui/header/header.test.tsx | 43 +- src/core/public/chrome/ui/header/header.tsx | 16 +- .../chrome/ui/header/header_logo.test.tsx | 223 +- .../public/chrome/ui/header/header_logo.tsx | 58 +- .../chrome/ui/header/home_icon.test.tsx | 282 +-- .../public/chrome/ui/header/home_icon.tsx | 77 +- .../public/chrome/ui/header/home_loader.tsx | 6 +- src/core/public/index.ts | 5 +- src/core/public/types.ts | 11 + .../favicons/android-chrome-192x192.png | Bin 5646 -> 2007 bytes .../favicons/android-chrome-512x512.png | Bin 15485 -> 5248 bytes .../assets/favicons/apple-touch-icon.png | Bin 5216 -> 1868 bytes .../assets/favicons/favicon-32x32.png | Bin 1231 -> 545 bytes .../core_app/assets/favicons/manifest.json | 4 +- .../assets/favicons/mstile-144x144.png | Bin 3293 -> 1836 bytes .../assets/favicons/mstile-150x150.png | Bin 3286 -> 1853 bytes .../assets/favicons/mstile-310x150.png | Bin 3548 -> 2004 bytes .../assets/favicons/mstile-310x310.png | Bin 7576 -> 3787 bytes .../core_app/assets/favicons/mstile-70x70.png | Bin 2268 -> 1331 bytes .../assets/favicons/safari-pinned-tab.svg | 46 +- .../core_app/assets/logos/opensearch.svg | 12 + .../assets/logos/opensearch_center_mark.svg | 6 + .../logos/opensearch_center_mark_on_dark.svg | 6 + .../logos/opensearch_center_mark_on_light.svg | 6 + .../assets/logos/opensearch_dashboards.svg | 2 +- ....svg => opensearch_dashboards_on_dark.svg} | 0 .../logos/opensearch_dashboards_on_light.svg | 13 + .../core_app/assets/logos/opensearch_mark.svg | 6 + .../assets/logos/opensearch_mark_on_dark.svg | 6 + .../assets/logos/opensearch_mark_on_light.svg | 6 + .../assets/logos/opensearch_on_dark.svg | 12 + .../assets/logos/opensearch_on_light.svg | 12 + .../assets/logos/opensearch_spinner.svg | 9 + .../logos/opensearch_spinner_on_dark.svg | 9 + .../logos/opensearch_spinner_on_light.svg | 9 + .../server/rendering/rendering_service.tsx | 2 +- .../__snapshots__/template.test.tsx.snap | 500 ++-- src/core/server/rendering/views/styles.tsx | 8 +- src/core/server/rendering/views/template.tsx | 156 +- src/core/tsconfig.json | 1 + .../actions/add_to_library_action.test.tsx | 1 + .../actions/clone_panel_action.test.tsx | 1 + .../actions/expand_panel_action.test.tsx | 1 + .../library_notification_action.test.tsx | 1 + .../actions/replace_panel_action.test.tsx | 1 + .../unlink_from_library_action.test.tsx | 1 + .../dashboard_listing.test.tsx.snap | 325 ++- .../dashboard_top_nav.test.tsx.snap | 390 ++- .../embeddable/dashboard_container.test.tsx | 2 + .../embeddable/dashboard_container.tsx | 6 +- .../dashboard_container_factory.tsx | 1 + .../embeddable/grid/dashboard_grid.test.tsx | 1 + .../viewport/dashboard_viewport.test.tsx | 2 + .../viewport/dashboard_viewport.tsx | 4 + src/plugins/dashboard/public/plugin.tsx | 1 + src/plugins/dev_tools/public/plugin.ts | 2 +- .../__snapshots__/welcome.test.tsx.snap | 278 +-- .../public/application/components/home.js | 2 + .../solution_title.test.tsx.snap | 200 +- .../solutions_section/solution_panel.tsx | 5 +- .../solutions_section/solution_title.test.tsx | 147 +- .../solutions_section/solution_title.tsx | 110 +- .../solutions_section/solutions_section.tsx | 12 +- .../application/components/welcome.test.tsx | 140 +- .../public/application/components/welcome.tsx | 70 +- .../assets/logos/opensearch_mark_centered.svg | 14 - .../assets/logos/opensearch_mark_default.svg | 5 - .../server/tutorials/opensearch_logs/index.ts | 2 +- .../tutorials/opensearch_metrics/index.ts | 2 +- src/plugins/management/public/plugin.ts | 2 +- .../management_overview/public/plugin.ts | 2 +- .../public/application.tsx | 3 +- .../public/components/app.tsx | 9 +- .../__snapshots__/overview.test.tsx.snap | 216 +- .../components/overview/overview.test.tsx | 75 +- .../public/components/overview/overview.tsx | 12 +- .../exit_full_screen_button.test.tsx.snap | 254 +- .../exit_full_screen_button.test.tsx | 76 +- .../exit_full_screen_button.tsx | 7 +- .../overview_page_header.test.tsx.snap | 274 +- .../overview_page_header.test.tsx | 320 ++- .../overview_page_header.tsx | 70 +- 105 files changed, 4730 insertions(+), 8029 deletions(-) create mode 100644 packages/osd-dev-utils/src/serializers/flat_object_serializer.ts create mode 100644 src/core/common/index.ts create mode 100644 src/core/common/logos/__snapshots__/get_logos.test.ts.snap create mode 100644 src/core/common/logos/constants.ts create mode 100644 src/core/common/logos/get_logos.mock.ts create mode 100644 src/core/common/logos/get_logos.test.ts create mode 100644 src/core/common/logos/get_logos.ts create mode 100644 src/core/common/logos/index.ts create mode 100644 src/core/common/logos/types.ts create mode 100644 src/core/common/mocks.ts create mode 100644 src/core/common/types.ts create mode 100644 src/core/server/core_app/assets/logos/opensearch.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_center_mark.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_center_mark_on_dark.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_center_mark_on_light.svg rename src/core/server/core_app/assets/logos/{opensearch_dashboards_darkmode.svg => opensearch_dashboards_on_dark.svg} (100%) create mode 100644 src/core/server/core_app/assets/logos/opensearch_dashboards_on_light.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_mark.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_mark_on_dark.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_mark_on_light.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_on_dark.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_on_light.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_spinner.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_spinner_on_dark.svg create mode 100644 src/core/server/core_app/assets/logos/opensearch_spinner_on_light.svg delete mode 100644 src/plugins/home/public/assets/logos/opensearch_mark_centered.svg delete mode 100644 src/plugins/home/public/assets/logos/opensearch_mark_default.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 545ee8110c8c..ba0570bd4ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,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)) - [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 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/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/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/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 3fb291f8830c4e35d43d1bb9d17ee2394689e7a6..a256e84b28b815d3ccc422d64e9820927575c7a8 100644 GIT binary patch delta 2004 zcmV;_2P^oFEY}Z^8Gi!+003nIoHYOd0R>P@R7C(aQvjpjx!LOhH&y^PQ~;jY0HE9e zm(c)CasZ;<0Cc1PJ6ZsMvjA+G0GrkTj>Q03f&f8d06AF(J6Hgt-vD8Z07Pd1K47NE z+yH#907q*8JY4`#bpVvj0Fuf8jKTnhy8v>b0B4g*caWUF(|-VozW`Ny08@DYN^Z8+ z=m4VM0A7cz&fr9GivU-DBvo-$ftf5^cN$D+vC`$C#Mh3q$b_oD0E4$?kEZ}zgb_qx z3_o1}nbVfK&Wf(1g=q!2~w6aLU)7^DZi~ zup)G@$$!F1HE3X)#rbtiUz;U(8jJ{SmT`=!>$8;Kn6@L9;$C9Pc39341ZdI&w`!QK zLzdLDF;%w9lFFE_3zp~H%#seL42k8Y#|+M`l! z>2L@>(v1RybjoKw$99DT;2%TS|!#!qWw)cnHAY$~0Ttg*mK4vHMO@dD%*=|*Y^q891C)hrabJmM#8 zVZ^WChaoCO4-?Eom0~G6K>h^{eTAS|dVgp!2UTirWPmvetU=5?jVdfbl|he3oIRSQ z0(%hinjXn~hAQ1wL=SwXz;}qr(PQ5hY$)Pvg}?TGCEY=JddljOvIJLrUlk91r@*?J zGZ2{BwV_7G4Vx)ma zZ*|>YfvbP}|^76T}8G=YY`98Q8JszBr! zDnx}E30er`J{tg99|c7woqw#F8F><8@b}_Any?{3A!@iG;i82kUsW;*X04>C=gj1>~2OBk|vp#M|nORiBRN;uP zcPehld_k<(N9}eoo$Lp=t%!LMtOeLCbvjuuadShLSXdjdF*Q2#a1UdKVAcYT&(9bD z;?zYJ)01ATz-Fw`*?(k&yGd+~F4B(?*eo?V>m|6SDEQf0{iRIzDEF zn^;fT-|?wlOlRvI{*Q3`;dpT8S?0QSz8Ha7l+r)qK02mQm^`3kwmtfnhi*~qnEnyx z=$1lBweHydcMA2CzC))3MxpH)qf1sfwD`8C3T}w@Q_MWmsfrPxu{%U7IX*G4(iK|DqwBO;J2%fC6S~IlJ7_VTo@C5gyLbLF z=rY3|x4&q!sZE-ey3W)utZ=+wpMFoo`)| mBuSDaNs=TZ82K!M^?ptwVEheGko z`}6zp{k!+%By;j)COOIE%p^~=hMGJ+4iydn0KivNkkNXUUH?@q;B&i-ywi9VXf{%+ zQUJi$1l)Tw^yfa)*iK_>%IF>yX|Z2$nIwTd!Q+P(`%d43u+vw8iUJ%)JK zfpkDLEC@CPGJyy$p32}DPT%r9nmZ$-%UQNOa`fB$($G}nXvG#i{Pv)=-p#4G-douF zy;8F!8jzmeO##adHfGlPg0cq1+SJa|Wda^mP;gHGQSfi%&ywWY*EiqJH4A5=BsGgK;=3;H1VH2H|J6#&aB`=Vo6xdLjc z)J&h^!~;=}^o`_O)D|V{>P#WD?G6wF=RKomO~ON#e=FGjOV@Qitrw|xMc!vU-P zetW>3qF^6`t{hNSK3v@Uva6>08&6hju-)t1UmqR3`u?QRE*MX;!v^b#6pk4rTU*u! zEk69VPg{#{3YV-mf2j}$ZW>vC*1ExByk4KWiG@c%$qifDHp5pwq zbbz=$+Y=88qJDAK9vsLZ%&m6#k#A+?fcM=I=bSxHPP_l@i%7__XY{Eh!o2uL@WsOL zdQo@s?~1dlv0>wPb;34|@9TX`qb|`CfeB;-tFzaV!H+2wry6@z1gF<*bGa$`xw@;9 z0yaD;NYKVc7I4tAem3ad@#m0bD0i=%?D0I(%5r34N)UeQ9~EVT<9RZ6Uvhl}p+*1x z`}5H^YS5;64L@IiJz*&(&elQ6O-r!9R5j=^W?2|yQF7rSI4#hmrbeyU4;6dKJW(S5 zazO7uKMv>gV!4UE#b8#D>kAWYOqXvq+&=C6H$}dC-r~4mMe0zVfk4*C1^bavsRc1e zoy2x0eaXncLs9;#5Korl=E84xPCugy{K>Cak)K8}7n0f$o><=h9@hg3{CYk7pTw2G zX%bOmy=TmbWRlI3{F%*cG2fG8rUY6jZ3MtK9gba`;D_j_62foZm(^%adSk1qSpG{t zd^M>l6y6A|UJ38g(5)`k`7xJi^7N=~)Fz>r!^l@t_iB)so>ZX*ek`t=Slr<2Z@rx+ z7&Lo0HZ?n2TB~A|1Q)@%Q}{>F=uAC1V_^;Yo54`w6lAu(RK{u&$0%+UkwZ{FmZidv z&t6(m7pCn4jsOX_0|1i`9PH%7qEFZB!!GwaJNbSD-dlfKk+)jQ2%Q~>I5l8|aoQ-g zJ_2SD$;XOYwfcKd8)P#x%jsQI|GlX^?z2&h+qo^EDz)iB3Ji_ch{mPAt zCe`XhhyagarR!h%rXne+0{%KF)k4xFLV^}_Q`Knvpf%-b5#`x)< zr<;}P^4dU!ei;rSws#U|p=M7wTPH^J6DZMp*8Hywg%P$@!~8;1n|Jkb2`6qCDyo2$ zmGh8(gPBug0pVcZ%NLc3rJQ)wP~6fP@NtcphtxV&+|SH|m2{L?b+x<>4Yw=lh$ZFx zr0mp`ChMuTkN@(5Zs{kf!HVATNPgmM!HxBrtN)b?qz{0TdzxBSceB>=TMc_sZi-wGt4riW|S`FM4&DjtZh#oHG zLfPs0_cm)XhOt4If!nx@)V=y{tqysvFTTgf5uXWUsF4OatV^wP;q`E-+(NM;2OVo% z>_p%AD^_Zk)18n zD_jeovwcJ-AUIIweCP4VT}I^>@b!+enXW6F{ge=aq{9WeFw&R9=S7Jqt!snOK~ukU zaAQ6lCG$Duc4)kvnkh5NNZKwr+kP`$7}U6>Mb=WF>2NxstS}ndLTG8M;Rj)L(g!Os5UrVz$mhv7s%>lvE+eiJu6i0P4-ji>kHM?QW6DL1r!A<;X|&Y-I1=$=^SF<9jx z5L*Oo&mV}AIGET%N7xZn==${sY$$i|{?=s``K!|F^nTDz3LWHWRp$#lL#oR$ttS;g z*;ZjLvfDQiCdixku=&`_5{o6JYwHK{ET5Pla1K!=LnAdAd9oh_NIZY)5wP)_^lHx|N8~q@aNWXOMI&iRP4MoxC&x>lnW24JurwmwniN?B4)YW zZQWxk=&%24c_Z||TpoY(Il`WCudYZkmY3HGy04l^h;Ndyb6NPlYJO>7bWl)&E zA<7A@zP4BhsbTcwFX;@Mt;;KHbnt4y^(@-|oOaOl$q+H{k0C)V&cb+&{u6XZf##67 z<^88KLa7;V8b1Q{j`%!6C^SDf>HZsx-|b8`{-O{?ruVCt{q8p(migCPq8cWmT?$#c zDZEsYUlt?17P$n%)7fmlK^@g4nVeTozLa%Lovq)xM5|egiVSqX1EStzX6JV$#s_n2 z5m80lqxx>}lhQ<*fRl)R{_H&Re<-dP$%2A(>S<|-B;1<+*##{g$ka;1Q&}yY`ZsHf z*Xk}w8arQP(a{z#*KQ!K-xWz8PTu|2Bk%PMkLJ{n&ok{gPcG7cI*7i!v$1qcu3xz6 zWI(w+wJhq3wli^KapZ)D>2nKY5t)Dm{XZ@vS~#!ryB_5k3|#0lh5@m$RAZ@Wq8Ar~ zS&s63ifqh^<+s)pMzcpmd(^I5@=NK=!Z9uS_-kP)Ho1fH+YFt&6n|(hT~D;!OZBm9 zaP@23uJ^A8&}eRmQ;V5g|Ni~8dy9!XoV%b*&k^Ai6ypZM(7XzGPW!{a{pI&eL#Mij z5)gleqV8bdliFNSLYL(1QSZho%~BMpN&$n)PQ2kz8Lwy=eez_U8;Tz3fQ;kM9xnpl z+LzWlYs>~2m1dTKY{#NzsG|2l2}hW+wA9WA-p!j5>^VoO!PYpa`bp;Lu07xOp)KR< zAkxU*x20javA=WXGdoTJ@cfSk<8DQh)6-A_9|sZp-`3_rw#Xb^MYK6iu?} zJ^6;#oZ7B!NWKS;923}IPylcJ;JWvKo($KlAzlcf6nBs6$i#)midz;Pak*)@7<&HZ zJ|>BvO7^rbFL(Yu;k{lvXu)1K`@5T9DLT;gAygFKEf$}6g^7~~MeoLuNtfJJuWd!2 zf(Q$Kqo}QX_LB(^D06+3%vQH!HsfPcSdKP@A&vpS+r-s2%ig~A z*gyGU`#GCXPHyzHa7`+U#Ghyx$uJ&dsYHtR6A|0&G{%_x5P}4Ha2-MlKS)*lkJ_Awe^j*(LC4#6){C^~kal7*I$7#|R9YI9m(cKZ7XC%<*f z&CnPO5McAH-mUnMKqp=NFQXsjXXHl006k^2J|awgaaF1L_Eq*bm)%!1$KVOoZR-i+ z4@iP=8O7928?%=YCnc&&EJZQT_qb<#UVXpNNs0x*S-a>)hj8W*kKeY(J19JpHLZyH zHqLnYZzEy%06g(C(~`!2<}p2iq~satoxN*YXGFn6b;50A$HI}H5wLW0cMljXtJBx? zd6{N2c<;RNZUaFlZFjQ5D;V?Uq)nQ~>xBR<11+j#Dl2_WTUzBxj)@v~6}nSX1xKFfZFMrR}HWLcta9iv;k|Hz~$~sv~nf|%yqz{RO<<5yz@`r zJwB>lBfBB8ak2q+1>>;g{&{bkw!F^`6)X_ivpfw17UUiH3B~e{=nP5s+Qz$99VL-n z*+O6vm=z^J4NN~-l=1?@s-SHl<$$^ZK3S|~aUni1)WTbPL;Z!E^r5e8CHo`1&8!sy_UHhGHtMClu~bGo`T-6AI8!M z=|_o>_}vBBcnw7O>oU20e@o}DDZE}RZB%Z@u+YV2VN9|pK_1TDS{Zu*t%lsCS(}yc z7?a|piEK!a1|nCsNZJr5nK!9E-6?~l-BY0GkYuFQriPuBNN~$trWYoHja?aKwY)q^ zT+XEVC@iwC*z)n2R?^Y750`91<_j7V4@eRjBSBgDH%r3#_t+#baob zE2WF7P^A~ZmH6Y4QFXR^_KousnjfkdDAS+^WxcN?5$;H)JaJrWzPClqhC``lpoV)i z^HnM>X1Uu5-RNeACUTs^OW+T#UMf|VLZp42er6j?lK?3=4xH}4^t*HNZl-+-< zV9XOH%Y(B;dqX?LGn>iaDXZ&gY36AqV&QJ}EC4)QJlyPD{Omm3+T21SJOU!T!faez zB3xWEsDd8S+N0 z_~ve&<6*h!Ff$LS505DR(E^~ta5*e+Nnj+(DWs?*V3`oU9!peMgEn}0ZhD}1q?e`_ gb2vnyP>BPuPeJT?F3q3zyaYf|R!ycx+BEck0Jf56?*IS* 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 47bc1a2bd52b4bebce63c75edd6398d5e0533b6e..404fbfad398faad3f0bea9949e3a8eebc5888fdc 100644 GIT binary patch literal 5248 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 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 3fdcfd3f09ed02c1478f690884ecafe844cb32ae..00b8c190b08cdbc7451477b7b1ef878295bfc0ed 100644 GIT binary patch delta 1864 zcmV-O2ewSNFWVE~1?RDYNNXOjR} zfB;l`9Zzhu)#m`C--WBe0EfH)TZ8~segIE&B2#Y-L0+cF+o8nRn!eI^p0xmctpIwg zV~wRUVto=tVtu2!bDOeonXhb>tY?s@T7{heVT%A?h&X3~E?#*7JY0;i#sGu20ClEH zcaT19hA3Ec7=KD;m%7iAx659Mpha_xf~dX#YnUuA1PlNG1_en(K~#9!?AeJ@8bJ^S z;JZ$%yz9`1KB!^V2+fnTd&@r70go=uj^zS>TP}V5gPeR zTc@sfK@@=x>9Sz#ODK07BD#ey=}SkG`iKzG5r2I8J#5<=v}!9%L_6@WpISKgH5A*X zM~7=zM1ELz0-Y~=^yCx^inc0u{)^gzQdW$f+(JNYSWmJqX(+Wxf5oB@60bIkJYN?| zt=&fOyKVON0F_qh+0rp2<-EuI9aQ>3 zFMl>5EnVm73W~qOSNo9H8}Kv-n6UOK9NLLQMK`=xA5J09>Nq|^udUOQ&r#9l@oH%k z3Z$n@N(XKU8i_!}I=ND3p+LMbH*}0fb|4~5F3YD-p_5^^G-H0eF6`?fQs0`5*&|3LyeoL`bWup^93mphWbpYTo}3>HfpX7b|bHo#rHp7NTJQEZ)jaJgR_$zg1n%#$U z;xR2SIEz4fC%(Ul_n!8ham(VWV1b+G9qFVwcb5>(n`3NDAW&^Rwtf z0FTb7(Uwbx5o=)SFgmY6hkvpHON%7cau%A$1IHuD@tvFw&SZe$hNOrHPZE>#hNehVp$n156kUT8>ZhNL*Zt0ApxlF~$c*+-gnO-dWq5@bb6r-r+n*~qeb z>Q*|@HX<7@trcBn;g5}(gV!W;Qc3+G^n-ZZ^IA;@I}AoD6^;5u1Gy~YffQ12BGDwRs5qPpMQqKJ_puXOJK0000wnV literal 5216 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 literal 1231 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081EXDlPlzi}0Yl3UhPE9H ztuO@Swrpo;+s4qk9V*zd8_d|w&<+!6hA|+z8dfp1?gZ-sithv|{{J7uZGxK#7M!$= zp?we3D4-gUEkIp6VYb7}?A*xEu$iF^%x&HUQs23a0d6D2sX($DY$nJ7Kto_&024vU8(%Yp9K?r6E~bbaf?@^=A-Hw8TET>UwYSZUyDle?_XHK@pJWJogGXW7S<{U zPhC;?m1g*1<+FVWCV`4yZWiplVgDrE(|-Ogpq-3K-tI1$d$Nyg0dhDCJR*x37}yg* zm~rai9ov9{>?NMQuIx`)MTMl4H70%e4b)TX>Eak-ak_RAuQijS!0~W>m8t6%y?AtL zsfE+~z$HR~Dy=7e->&)ZpRoShx3c1y&u`A%`S*WWURk=}rQA~~+ai{y=VUaUJ#^wt zF`x5Y&MPsW-5R3V9X?1f#IZ5lKNNr99Mgsu|G$ghU$#BLR=4w_{c~M3 z1MEZfKk!K9OlLfFX{TGvgNq^yTT>?Pe)vF@aovXx3|j=bKKSlR<67y+e*T{{--g#! z+u66T+V8#jz+S#D4;R|;*f25M1jRB+O!#neDf9VR|B~2(J$@I2#COIm&8=mg__pC~ z?53zA{akndb2_p3njPh7GKzfn?c?k@JNEw!ea$$d$SrDLgkS2_)&J#MdcN>fZk#KB z)1q>dLPw|D>a)84m=fmoSEnXPyaC3IYKdz^NlIc#s#S7PDv)9@GB7gKH89mRG7K@a zure~UGB(vVFt9Q(5L_I~j-nwqKP5A*61Rqge@}M2D?NY%?PN}v7CMhd7XyecH3Bq*_5p`a)~Ei)%op`@}PRUxyWB$Jc1ICAEQ%n|m}4IT@; a^cY@=3zmFxGMx&vg2B_(&t;ucLK6UJ!tXHv 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 577ecfed1c02b8b1f2a4796867145aa052e88ebd..5a379489e086f1be5854b0c191a96dd8a69473a5 100644 GIT binary patch literal 1836 zcmdUw`9IT-1IPLF8KSv9@?lcm5?bTy$}NrA4D(rX%#|ZY&5_(g#j1^LL|N`*W_-Rm zvr_x!vyU?tAGuBCaC0&0qQQ$qN&t(9#^c z_N~`8&tkNzG5hyK_%O53Pp63n6%qmo&iuNtn>#kA-;+wdAM|dd_2!G_ zklq-9ss)5#J{=QhAC>@3N6X}3W5c|77$bDNYd6!!=CA}uC8 z$xR?vmXBnK;kb~UAOY)g%=^ukNWc7tUc#0;A^`?;9vFmq99&#`5xqNX1!K}ik4K14 z>FI|3TqhY;HHA1XLCMm<$PA`HR?SKXwOMEX%C$40x{!GyJ9o}jY?M(Ne%Qq}j+%13 zz+P3e8F@gv;(o81Xr@rC`76UxY~NhwPlb`$)^5BX1sclY?)nFF>U%}q$ktV)q%)pi zzBoGsfMlx)s=B2EpX9{4Eb`y3uT3gIJ0@pfp3F4K%-|jiXT4eiz2J`0Lfi909_F*W z%Q8zLR$rc4_Cr6 z>*kBRKM%B0gd1ZFb(iDUZ%ws_u;a6IGMf@pAddQ#P2riekz7!ASBiM-U;6|Pv+?*q z;2uMbd2_QPIDgsDL+wAZS9dE+9C7A2e=E3AW0kNXspO^7^7@~|iYpV*`tX>>Is{kS zS~&Tl{qX~%sJi!Qt-cD$pOcd{Rb0&Jvwk)#5B69%XOGiZv?QerwvwEz?*kdzT3P+L zJ6)Pop1{}YEay$ao@K#a87l*+K2wyMM_!B7r;J3&qr~d^B>m2ni7@4pj2nd&&|%2U zVLO;;vi$jU?J(z=JuU;fy}Vp$9=ZYYd-lVEHaFd?bxteP(^hRfg^b#)ktq6JW0NpC zAg>LF5Pyj>bGIKjIZu3o`cmdEx9nj_OOEr}=s7>5oH!ZDbf^y2O^bVT z+WYInELO9JC|wJ|0MPy>#;WDs@5$1Omm?jeW48u%5pP;7O~)dq{|f=_T#%LDv6E83 zz`Bgp)ztB`NP2PX1c`yWF>#F#a0A(ueiOpOc?A3x`6D*FkL?WP&mi$h<5oF?p42L@ zm0sf5x=k7g{E;8kdhfwh+e71(h>`;14}tUlTQ?*~@r+X!(uV`_*2I#Drsy-h-10qU zcsV>N#{pM*Wi#dA`3m}MxbLm({q7TmZNuA8)J!SdXQG(PwT=>;D^CoiE{s}-w<`YD zpL>v2a;P3=P|;=DZjyL|w5Z-Laoym0KDDZyi#qmqygW9~^~CJk=%Q$-llzlGevIqw znE;F+*cs6sy=C&waS&?wXTKc z&SGD7*Ml1uTtK9{FomXar^z{1!-rHw3$g5V`wWpc_qC~=a0A{Mn)TL~sY(Gz=YeaS TfQUalx)LF@jU%Gg%9r>L7XHs2 literal 3293 zcmV<33?lQ1P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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 literal 3286 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 literal 7576 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 RN&VLueH~-%@_P@T{}0BfAvgd4 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 399a42ff6ae5de490bfd345c911369b6557a31cb..5155640f09769667081fe8f0232108fad5a22da5 100644 GIT binary patch literal 1331 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&Ee-GqaRt)<2R+#F_i-rDp=KpP ze!&cXez3k1;l5e$`jN8qmy6YD7W^Cw+dede*xubS(Npi_@~OtpX7GFB2-}1Ce4G;)Uhe(wbN}i=Pxx}?#U>6nINGs~QhHQquA z8b4aU@XM>L(&A_LFjZu97r7p@;XHFru*kQKE>?f{PICH?(XF#v?&BmA5tC)&DVwGj z-ORj^@}+3^Zas50??sl2y_U;%-$-F~)exH~(~x>~zd(1p(y}kPCq%W2b%N!DU2|UC z7OVO4d6VNzF^xkD@>lWYweNF$`Ap+MZ{9wIL)C>EM}LX8*M~L>-1p&Z4Ozky<@$K} zA5Xo5Zgcd@e*b7%vEXZjh_A~s<)G+<*_Y>*v~4)7TFhuXbF;zXo@wj?VtWGaJp8DW z*l@Dz=>@(=w}PtpdJeB=ne1aYnJM#+)bF+jhFkg`Ful0avSFo5xslTG4Z99rSu8(C z-_?ezBwgj)7hZ#vUk=O_yW!{bzLUL}b(O-SZOj}W-|McF&JIqDS(mUYh--De_{--T zt?VxNvirz-sm}F@F7Z~?4U;ap%oIF-lgIM&k`^=j8UOeE6K)GXb@|4e(s#2<#3xp_ z&#_GuQi(rnZqU2PP3E<8&;e09v+dT&%O`br$cr89*xhkcFWydh>aCLp%H~;dy(;~? zMbXT1?lhdA zy5q^StLJ^M2uR2HdL9wT{4#}e@zRFNb6EBi_g`0Add9``+Q9{uC7YSHw?^kOUb-#48FbRl$c7US8UxWxAeL5ncKfCg)b_$?h@Eq z)Y0E7(8Pj+6144 zs$E=aQ`TR1*{0jdBYTvW_tapw{U3R|Yq_v+u|7a^`Ur8_Tuo%mKqovrSu z>2@vE-eo7YP1bz!s)DWd`$Lt9iDFSykgX6w(8mTkYDK!7biv8fBU?t zv#Zj(UFjIl)T(Sr5oO)d5cm7#`a(xzZ9<$a_tfn>qx#D;cX7$)lNEJ2D_&04=3P4b z)Rco+KUW;+xFqW<8~Q=B*EDjW*2xFbvo|-r+M?kmRa$1Il_vFMMfUewnY?L?SzE!` d`CoB8qu;Hy)pvh$9RU_944$rjF6*2UngA?S#FPL4 literal 2268 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 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/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" > - - - - - - - - +

-
- - - - - - -
- - -`; - -exports[`Welcome page should render welcome logo in default mode using the original OpenSearch Dashboards logo 1`] = ` - -
-
-
- - - - -

diff --git a/src/plugins/home/public/application/components/home.js b/src/plugins/home/public/application/components/home.js index 2852ed4cda19..7024d73080e8 100644 --- a/src/plugins/home/public/application/components/home.js +++ b/src/plugins/home/public/application/components/home.js @@ -163,6 +163,7 @@ export class Home extends Component { solutions={solutions} directories={directories} branding={getServices().injectedMetadata.getBranding()} + logos={getServices().chrome.logos} /> ) : null} @@ -202,6 +203,7 @@ export class Home extends Component { urlBasePath={this.props.urlBasePath} telemetry={this.props.telemetry} branding={getServices().injectedMetadata.getBranding()} + logos={getServices().chrome.logos} /> ); } diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_title.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_title.test.tsx.snap index 2fb95d11d5c8..c88537eac658 100644 --- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_title.test.tsx.snap +++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_title.test.tsx.snap @@ -1,186 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SolutionTitle in dark mode renders the home dashboard logo using mark dark mode URL 1`] = ` - - -
- custom title logo -
- -

- custom title -

-
- -

- Visualize & analyze - - -

-
-
-
-`; - -exports[`SolutionTitle in dark mode renders the home dashboard logo using mark default mode URL 1`] = ` - - -
- custom title logo -
- -

- custom title -

-
- -

- Visualize & analyze - - -

-
-
-
-`; - -exports[`SolutionTitle in dark mode renders the home dashboard logo using original in and out door logo 1`] = ` - - - - -

- custom title -

-
- -

- Visualize & analyze - - -

-
-
-
-`; - -exports[`SolutionTitle in default mode renders the home dashboard logo using mark default mode URL 1`] = ` - - -
- custom title logo -
- -

- custom title -

-
- -

- Visualize & analyze - - -

-
-
-
-`; - -exports[`SolutionTitle in default mode renders the home dashboard logo using original in and out door logo 1`] = ` +exports[`SolutionTitle renders correctly by default 1`] = `

- custom title + Page Title

`; -exports[`SolutionTitle in default mode renders the title section of the solution panel 1`] = ` +exports[`SolutionTitle renders correctly when branded 1`] = ` custom title logo

- custom title + Page Title

= ({ addBasePath, solution, apps = [], branding }) => ( +export const SolutionPanel: FC = ({ addBasePath, solution, apps = [], branding, logos }) => ( = ({ addBasePath, solution, apps = [], bra title={solution.title} subtitle={solution.subtitle} branding={branding} + logos={logos} /> diff --git a/src/plugins/home/public/application/components/solutions_section/solution_title.test.tsx b/src/plugins/home/public/application/components/solutions_section/solution_title.test.tsx index 5d4a6ebf079c..9d8f7662f8fb 100644 --- a/src/plugins/home/public/application/components/solutions_section/solution_title.test.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solution_title.test.tsx @@ -31,6 +31,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SolutionTitle } from './solution_title'; +import { getLogosMock } from '../../../../../../core/common/mocks'; const solutionEntry = { id: 'opensearchDashboards', @@ -42,121 +43,51 @@ const solutionEntry = { order: 1, }; +const mockTitle = 'Page Title'; +const makeProps = () => ({ + title: solutionEntry.title, + subtitle: solutionEntry.subtitle, + iconType: solutionEntry.icon, + branding: { + applicationTitle: mockTitle, + }, + logos: getLogosMock.default, +}); + describe('SolutionTitle ', () => { - describe('in default mode', () => { - test('renders the title section of the solution panel', () => { - const branding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeUrl', - darkModeUrl: '/darkModeUrl', - }, - applicationTitle: 'custom title', - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); + it('renders correctly by default', () => { + const props = { + ...makeProps(), + }; + const component = shallow(); + const elements = component.find('EuiToken'); + expect(elements.length).toEqual(1); - test('renders the home dashboard logo using mark default mode URL', () => { - const branding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeUrl', - darkModeUrl: '/darkModeUrl', - }, - applicationTitle: 'custom title', - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); + const img = elements.first(); + expect(img.prop('iconType')).toEqual(props.logos.Mark.url); - test('renders the home dashboard logo using original in and out door logo', () => { - const branding = { - darkMode: false, - mark: {}, - applicationTitle: 'custom title', - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); + const titles = component.find('EuiTitle > h3'); + expect(titles.length).toEqual(1); + expect(titles.first().text()).toEqual(mockTitle); + + expect(component).toMatchSnapshot(); }); - describe('in dark mode', () => { - test('renders the home dashboard logo using mark dark mode URL', () => { - const branding = { - darkMode: true, - mark: { - defaultUrl: '/defaultModeUrl', - darkModeUrl: '/darkModeUrl', - }, - applicationTitle: 'custom title', - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); + it('renders correctly when branded', () => { + const props = { + ...makeProps(), + logos: getLogosMock.branded, + }; + const component = shallow(); + const elements = component.find({ + 'data-test-subj': 'dashboardCustomLogo', }); + expect(elements.length).toEqual(1); - test('renders the home dashboard logo using mark default mode URL', () => { - const branding = { - darkMode: true, - mark: { - defaultUrl: '/defaultModeUrl', - }, - applicationTitle: 'custom title', - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); + const img = elements.first(); + expect(img.prop('src')).toEqual(props.logos.Mark.url); + expect(img.prop('alt')).toEqual(`${mockTitle} logo`); - test('renders the home dashboard logo using original in and out door logo', () => { - const branding = { - darkMode: true, - mark: {}, - applicationTitle: 'custom title', - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); }); }); diff --git a/src/plugins/home/public/application/components/solutions_section/solution_title.tsx b/src/plugins/home/public/application/components/solutions_section/solution_title.tsx index 8a0db3ac1afb..f2a7af96d715 100644 --- a/src/plugins/home/public/application/components/solutions_section/solution_title.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solution_title.tsx @@ -29,6 +29,7 @@ */ import React, { FC } from 'react'; +import type { Logos } from 'opensearch-dashboards/public'; import { EuiFlexGroup, EuiFlexItem, @@ -53,97 +54,36 @@ interface Props { */ iconType: IconType; branding: HomePluginBranding; + logos: Logos; } /** - * Use branding configurations to check which URL to use for rendering - * home card logo in default mode. In default mode, home card logo will - * proritize default mode mark URL. If it is invalid, default opensearch logo - * will be rendered. - * - * @param {HomePluginBranding} - pass in custom branding configurations - * @returns a valid custom URL or undefined if no valid URL is provided - */ -const customHomeLogoDefaultMode = (branding: HomePluginBranding) => { - return branding.mark?.defaultUrl ?? undefined; -}; - -/** - * Use branding configurations to check which URL to use for rendering - * home logo in dark mode. In dark mode, home logo will render - * dark mode mark URL if valid. Otherwise, it will render the default - * mode mark URL if valid. If both dark mode mark URL and default mode mark - * URL are invalid, the default opensearch logo will be rendered. - * - * @param {HomePluginBranding} - pass in custom branding configurations - * @returns {string|undefined} a valid custom URL or undefined if no valid URL is provided + * The component that renders the blue dashboard card on home page. + * `title` and `iconType` are deprecated because SolutionTitle component will only be rendered once + * as the home dashboard card. */ -const customHomeLogoDarkMode = (branding: HomePluginBranding) => { - return branding.mark?.darkModeUrl ?? branding.mark?.defaultUrl ?? undefined; -}; - -/** - * Render custom home logo for both default mode and dark mode - * - * @param {HomePluginBranding} - pass in custom branding configurations - * @returns {string|undefined} a valid custom loading logo URL, or undefined - */ -const customHomeLogo = (branding: HomePluginBranding) => { - return branding.darkMode ? customHomeLogoDarkMode(branding) : customHomeLogoDefaultMode(branding); -}; - -/** - * Check if we render a custom home logo or the default opensearch spinner. - * If customWelcomeLogo() returns undefined(no valid custom URL is found), we - * render the default opensearch logo - * - * @param {HomePluginBranding} - pass in custom branding configurations - * @returns a image component with custom logo URL, or the default opensearch logo - */ -const renderBrandingEnabledOrDisabledLogo = (branding: HomePluginBranding) => { - const customLogo = customHomeLogo(branding); - if (customLogo) { - return ( -
- {branding.applicationTitle -
- ); - } - const DEFAULT_OPENSEARCH_MARK = `${branding.assetFolderUrl}/opensearch_mark_default_mode.svg`; - const DARKMODE_OPENSEARCH_MARK = `${branding.assetFolderUrl}/opensearch_mark_dark_mode.svg`; - - return ( - - ); -}; - -/** - * - * @param {string} title - * @param {string} subtitle - * @param {IconType} iconType - will always be inputOutput icon type here - * @param {HomePluginBranding} branding - custom branding configurations - * - * @returns - a EUI component that renders the blue dashboard card on home page, - * title and iconType are deprecated here because SolutionTitle component will only be rendered once - * as the home dashboard card, and we are now in favor of using custom branding configurations. - */ -export const SolutionTitle: FC = ({ title, subtitle, iconType, branding }) => ( +export const SolutionTitle: FC = ({ subtitle, branding, logos }) => ( - {renderBrandingEnabledOrDisabledLogo(branding)} + {logos.Mark.type === 'custom' ? ( +
+ {branding.applicationTitle +
+ ) : ( + + )} = ({ addBasePath, solutions, directories, branding }) => { +export const SolutionsSection: FC = ({ + addBasePath, + solutions, + directories, + branding, + logos, +}) => { // Separate OpenSearch Dashboards from other solutions const opensearchDashboards = solutions.find(({ id }) => id === 'opensearchDashboards'); const opensearchDashboardsApps = directories @@ -78,6 +86,7 @@ export const SolutionsSection: FC = ({ addBasePath, solutions, directorie solution={solution} addBasePath={addBasePath} branding={branding} + logos={logos} /> ))}
@@ -89,6 +98,7 @@ export const SolutionsSection: FC = ({ addBasePath, solutions, directorie addBasePath={addBasePath} apps={opensearchDashboardsApps.length ? opensearchDashboardsApps : undefined} branding={branding} + logos={logos} /> ) : null}
diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx index 6a08c977e157..c03bdff6aae0 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -32,6 +32,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Welcome } from './welcome'; import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; +import { getLogosMock } from '../../../../../core/common/mocks'; jest.mock('../opensearch_dashboards_services', () => ({ getServices: () => ({ @@ -48,120 +49,95 @@ test('should render a Welcome screen with the telemetry disclaimer', () => { }); */ -const branding = { - darkMode: false, - mark: { - defaultUrl: '/', +const mockTitle = 'Page Title'; +const makeProps = () => ({ + urlBasePath: '/', + onSkip: jest.fn(), + branding: { + applicationTitle: mockTitle, }, - applicationTitle: 'OpenSearch Dashboards', -}; + logos: getLogosMock.default, +}); -describe('Welcome page ', () => { - describe('should render a Welcome screen ', () => { - test('with the telemetry disclaimer when optIn is true', () => { +describe('Welcome page', () => { + describe('renders the Welcome screen', () => { + it('with the telemetry disclaimer when optIn is true', () => { const telemetry = telemetryPluginMock.createStartContract(); telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); - const component = shallow( - {}} telemetry={telemetry} branding={branding} /> - ); + const props = { + ...makeProps(), + telemetry, + }; + const component = shallow(); expect(component).toMatchSnapshot(); }); - test('with the telemetry disclaimer when optIn is false', () => { + it('with the telemetry disclaimer when optIn is false', () => { const telemetry = telemetryPluginMock.createStartContract(); telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); - const component = shallow( - {}} telemetry={telemetry} branding={branding} /> - ); + const props = { + ...makeProps(), + telemetry, + }; + const component = shallow(); expect(component).toMatchSnapshot(); }); - test('with no telemetry disclaimer', () => { - const component = shallow( {}} branding={branding} />); - + it('with no telemetry disclaimer', () => { + const props = { + ...makeProps(), + }; + const component = shallow(); expect(component).toMatchSnapshot(); }); - test('fires opt-in seen when mounted', () => { + it('and fires opt-in seen when mounted', () => { const telemetry = telemetryPluginMock.createStartContract(); const mockSetOptedInNoticeSeen = jest.fn(); telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; - shallow( - {}} telemetry={telemetry} branding={branding} /> - ); + + const props = { + ...makeProps(), + telemetry, + }; + shallow(); expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); }); }); - describe('should render welcome logo in default mode ', () => { - test('using mark default mode URL', () => { - const customBranding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeMark', - }, - applicationTitle: 'custom title', + describe('renders the welcome logo', () => { + it('with OpenSearch center mark when not branded', () => { + const props = { + ...makeProps(), }; - const component = shallow( - {}} branding={customBranding} /> - ); - expect(component).toMatchSnapshot(); - }); + const component = shallow(); - test('using the original OpenSearch Dashboards logo', () => { - const defaultBranding = { - darkMode: false, - mark: {}, - applicationTitle: 'OpenSearch Dashboards', - }; - const component = shallow( - {}} branding={defaultBranding} /> - ); - expect(component).toMatchSnapshot(); - }); - }); - describe('should render welcome logo in dark mode ', () => { - test('using mark dark mode URL', () => { - const customBranding = { - darkMode: true, - mark: { - defaultUrl: '/defaultModeMark', - darkModeUrl: '/darkModeMark', - }, - title: 'custom title', - }; - const component = shallow( - {}} branding={customBranding} /> - ); - expect(component).toMatchSnapshot(); - }); + const elements = component.find('EuiIcon'); + expect(elements.length).toEqual(1); + expect(elements.first().prop('type')).toEqual(props.logos.CenterMark.url); - test('using mark default mode URL', () => { - const customBranding = { - darkMode: true, - mark: { - defaultUrl: '/defaultModeMark', - }, - title: 'custom title', - }; - const component = shallow( - {}} branding={customBranding} /> - ); expect(component).toMatchSnapshot(); }); - test('using the original opensearch logo', () => { - const customBranding = { - darkMode: true, - mark: {}, - title: 'custom title', + it('with custom branded logo when branded', () => { + const props = { + ...makeProps(), + logos: getLogosMock.branded, }; - const component = shallow( - {}} branding={customBranding} /> - ); + const component = shallow(); + + const elements = component.find({ + 'data-test-subj': 'welcomeCustomLogo', + }); + expect(elements.length).toEqual(1); + + const img = elements.first(); + expect(img.prop('src')).toEqual(props.logos.CenterMark.url); + expect(img.prop('alt')).toEqual(`${mockTitle} logo`); + expect(component).toMatchSnapshot(); }); }); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 1a364ce732bc..108180cacd99 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -47,23 +47,19 @@ import { } from '@elastic/eui'; import { METRIC_TYPE } from '@osd/analytics'; import { FormattedMessage } from '@osd/i18n/react'; +import { Logos } from 'opensearch-dashboards/public'; import { getServices } from '../opensearch_dashboards_services'; import { TelemetryPluginStart } from '../../../../telemetry/public'; - import { SampleDataCard } from './sample_data'; -import OpenSearchMarkCentered from '../../assets/logos/opensearch_mark_centered.svg'; + interface Props { urlBasePath: string; onSkip: () => void; telemetry?: TelemetryPluginStart; branding: { - darkMode: boolean; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; applicationTitle?: string; }; + logos: Logos; } /** @@ -146,72 +142,32 @@ export class Welcome extends React.Component { } }; - private darkMode = this.props.branding.darkMode; - private markDefault = this.props.branding.mark.defaultUrl; - private markDarkMode = this.props.branding.mark.darkModeUrl; private applicationTitle = this.props.branding.applicationTitle; /** - * Use branding configurations to check which URL to use for rendering - * welcome logo in default mode. In default mode, welcome logo will - * proritize default mode mark URL. If it is invalid, default opensearch logo - * will be rendered. - * - * @returns a valid custom URL or undefined if no valid URL is provided - */ - private customWelcomeLogoDefaultMode = () => { - return this.markDefault ?? undefined; - }; - - /** - * Use branding configurations to check which URL to use for rendering - * welcome logo in dark mode. In dark mode, welcome logo will render - * dark mode mark URL if valid. Otherwise, it will render the default - * mode mark URL if valid. If both dark mode mark URL and default mode mark - * URL are invalid, the default opensearch logo will be rendered. - * - * @returns a valid custom URL or undefined if no valid URL is provided + * Returns Welcome logo, wrapped in a container, using the custom branded logos or, + * the centered opensearch mark if no branding is provided. */ - private customWelcomeLogoDarkMode = () => { - return this.markDarkMode ?? this.markDefault ?? undefined; - }; + private renderWelcomeLogo = () => { + const { url: centerMarkURL, type: centerMarkType } = this.props.logos.CenterMark; - /** - * Render custom welcome logo for both default mode and dark mode - * - * @returns a valid custom loading logo URL, or undefined - */ - private customWelcomeLogo = () => { - if (this.darkMode) { - return this.customWelcomeLogoDarkMode(); - } - return this.customWelcomeLogoDefaultMode(); - }; - - /** - * Check if we render a custom welcome logo or the default opensearch spinner. - * If customWelcomeLogo() returns undefined(no valid custom URL is found), we - * render the default opensearch logo - * - * @returns a image component with custom logo URL, or the default opensearch logo - */ - private renderBrandingEnabledOrDisabledLogo = () => { - if (this.customWelcomeLogo()) { + if (centerMarkType === 'custom') { return (
{this.applicationTitle
); } + return ( - + ); }; @@ -224,7 +180,7 @@ export class Welcome extends React.Component {
- {this.renderBrandingEnabledOrDisabledLogo()} + {this.renderWelcomeLogo()} - - - - \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/opensearch_mark_default.svg b/src/plugins/home/public/assets/logos/opensearch_mark_default.svg deleted file mode 100644 index e185d0fc8c3b..000000000000 --- a/src/plugins/home/public/assets/logos/opensearch_mark_default.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/plugins/home/server/tutorials/opensearch_logs/index.ts b/src/plugins/home/server/tutorials/opensearch_logs/index.ts index 59589e596822..86f8caebd0ac 100644 --- a/src/plugins/home/server/tutorials/opensearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/opensearch_logs/index.ts @@ -58,7 +58,7 @@ export function opensearchLogsSpecProvider(context: TutorialContext): TutorialSc learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-opensearch.html', }, }), - euiIconType: '/plugins/home/assets/logos/opensearch_mark_default.svg', + euiIconType: '/ui/logos/opensearch_mark.svg', artifacts: { application: { label: i18n.translate('home.tutorials.opensearchLogs.artifacts.application.label', { diff --git a/src/plugins/home/server/tutorials/opensearch_metrics/index.ts b/src/plugins/home/server/tutorials/opensearch_metrics/index.ts index e7a2559d2df8..38694ca9d588 100644 --- a/src/plugins/home/server/tutorials/opensearch_metrics/index.ts +++ b/src/plugins/home/server/tutorials/opensearch_metrics/index.ts @@ -57,7 +57,7 @@ export function opensearchMetricsSpecProvider(context: TutorialContext): Tutoria learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-opensearch.html', }, }), - euiIconType: '/plugins/home/assets/logos/opensearch_mark_default.svg', + euiIconType: '/ui/logos/opensearch_mark.svg', artifacts: { application: { label: i18n.translate('home.tutorials.opensearchMetrics.artifacts.application.label', { diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 4cc1c4aa7f8f..81a970a0fc48 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -76,7 +76,7 @@ export class ManagementPlugin implements Plugin { diff --git a/src/plugins/opensearch_dashboards_overview/public/application.tsx b/src/plugins/opensearch_dashboards_overview/public/application.tsx index f6d0e172e394..26bf7294263c 100644 --- a/src/plugins/opensearch_dashboards_overview/public/application.tsx +++ b/src/plugins/opensearch_dashboards_overview/public/application.tsx @@ -51,7 +51,6 @@ export const renderApp = ( .filter(({ id }) => id !== 'opensearchDashboards') .filter(({ id }) => navLinks.find(({ category, hidden }) => !hidden && category?.id === id)); const features = home.featureCatalogue.get(); - const branding = core.injectedMetadata.getBranding(); ReactDOM.render( @@ -64,7 +63,7 @@ export const renderApp = ( newsfeed$={newsfeed$} solutions={solutions} features={features} - branding={branding} + logos={core.chrome.logos} /> , diff --git a/src/plugins/opensearch_dashboards_overview/public/components/app.tsx b/src/plugins/opensearch_dashboards_overview/public/components/app.tsx index dfd3b717981f..def278cdd200 100644 --- a/src/plugins/opensearch_dashboards_overview/public/components/app.tsx +++ b/src/plugins/opensearch_dashboards_overview/public/components/app.tsx @@ -32,12 +32,11 @@ import React, { useEffect, useState } from 'react'; import { Observable } from 'rxjs'; import { I18nProvider } from '@osd/i18n/react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; -import { CoreStart } from 'src/core/public'; +import { CoreStart, Logos } from 'src/core/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { FetchResult } from 'src/plugins/newsfeed/public'; import { FeatureCatalogueEntry, FeatureCatalogueSolution } from 'src/plugins/home/public'; import { Overview } from './overview'; -import { OverviewPluginBranding } from '../plugin'; interface OpenSearchDashboardsOverviewAppDeps { basename: string; @@ -47,7 +46,7 @@ interface OpenSearchDashboardsOverviewAppDeps { newsfeed$?: Observable; solutions: FeatureCatalogueSolution[]; features: FeatureCatalogueEntry[]; - branding: OverviewPluginBranding; + logos: Logos; } export const OpenSearchDashboardsOverviewApp = ({ @@ -55,7 +54,7 @@ export const OpenSearchDashboardsOverviewApp = ({ newsfeed$, solutions, features, - branding, + logos, }: OpenSearchDashboardsOverviewAppDeps) => { const [newsFetchResult, setNewsFetchResult] = useState(null); @@ -78,7 +77,7 @@ export const OpenSearchDashboardsOverviewApp = ({ newsFetchResult={newsFetchResult} solutions={solutions} features={features} - branding={branding} + logos={logos} /> diff --git a/src/plugins/opensearch_dashboards_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/opensearch_dashboards_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index e352dd58236c..6d92c8759243 100644 --- a/src/plugins/opensearch_dashboards_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/opensearch_dashboards_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Overview render 1`] = ` +exports[`Overview renders with solutions and features 1`] = `
`; -exports[`Overview without features 1`] = ` +exports[`Overview renders with solutions and without features 1`] = `
`; -exports[`Overview without solutions 1`] = ` +exports[`Overview renders without solutions and with features 1`] = `
({ useOpenSearchDashboards: jest.fn().mockReturnValue({ @@ -163,46 +164,42 @@ const mockFeatures = [ }, ]; -const mockBranding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeUrl', - darkModeUrl: '/darkModeUrl', - }, -}; +const makeProps = () => ({ + newsFetchResult: mockNewsFetchResult, + solutions: mockSolutions, + features: mockFeatures, + logos: getLogosMock.default, +}); describe('Overview', () => { - test('render', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); - test('without solutions', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); - test('without features', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); + describe('renders', () => { + it('with solutions and features', () => { + const props = { + ...makeProps(), + }; + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('without solutions and with features', () => { + const props = { + ...makeProps(), + solutions: [], + }; + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + it('with solutions and without features', () => { + const props = { + ...makeProps(), + features: [], + }; + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); }); + + // ToDo: Add tests for all the complications of Overview + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4693 + it.todo('renders each of the complications of Overview'); }); diff --git a/src/plugins/opensearch_dashboards_overview/public/components/overview/overview.tsx b/src/plugins/opensearch_dashboards_overview/public/components/overview/overview.tsx index 3df491b1b422..30df4dbe8146 100644 --- a/src/plugins/opensearch_dashboards_overview/public/components/overview/overview.tsx +++ b/src/plugins/opensearch_dashboards_overview/public/components/overview/overview.tsx @@ -41,7 +41,7 @@ import { EuiToken, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; -import { CoreStart } from 'opensearch-dashboards/public'; +import { CoreStart, Logos } from 'opensearch-dashboards/public'; import { RedirectAppLinks, useOpenSearchDashboards, @@ -60,18 +60,18 @@ import { AddData } from '../add_data'; import { GettingStarted } from '../getting_started'; import { ManageData } from '../manage_data'; import { NewsFeed } from '../news_feed'; -import { OverviewPluginBranding } from '../../plugin'; const sortByOrder = (featureA: FeatureCatalogueEntry, featureB: FeatureCatalogueEntry) => (featureA.order || Infinity) - (featureB.order || Infinity); + interface Props { newsFetchResult: FetchResult | null | void; solutions: FeatureCatalogueSolution[]; features: FeatureCatalogueEntry[]; - branding: OverviewPluginBranding; + logos: Logos; } -export const Overview: FC = ({ newsFetchResult, solutions, features, branding }) => { +export const Overview: FC = ({ newsFetchResult, solutions, features, logos }) => { const [isNewOpenSearchDashboardsInstance, setNewOpenSearchDashboardsInstance] = useState(false); const { services: { http, data, uiSettings, application }, @@ -147,14 +147,14 @@ export const Overview: FC = ({ newsFetchResult, solutions, features, bran } - branding={branding} + logos={logos} />
diff --git a/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap b/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap index 028a23d69e76..1a88da2e58f4 100644 --- a/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap +++ b/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap @@ -1,96 +1,178 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`is rendered 1`] = ` - +exports[`ExitFullScreenButton is rendered using the dark theme's mark by default 1`] = ` +
+ +

+ In full screen mode, press ESC to exit. +

+
- -

+ - In full screen mode, press ESC to exit. -

-
-
-
+ + + + + + +
+
+`; + +exports[`ExitFullScreenButton is rendered using the dark theme's mark when light color-scheme is applied 1`] = ` +
+ +

+ In full screen mode, press ESC to exit. +

+
+
+ +
+
+`; + +exports[`ExitFullScreenButton is rendered using the light theme's mark when dark color-scheme is applied 1`] = ` +
+ +

+ In full screen mode, press ESC to exit. +

+
+
+ -
+

+ Exit full screen +

+ +
+ + + + + +
- +
`; diff --git a/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx b/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx index cae20349cf3e..fd5a11011bdc 100644 --- a/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx +++ b/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx @@ -29,36 +29,80 @@ */ import React from 'react'; -import sinon from 'sinon'; import { ExitFullScreenButton } from './exit_full_screen_button'; import { keys } from '@elastic/eui'; -import { mount } from 'enzyme'; +import { shallow } from 'enzyme'; +import { getLogosMock } from '../../../../core/common/mocks'; +import { ColorScheme } from '../../../../core/common'; -test('is rendered', () => { - const component = mount( {}} />); - - expect(component).toMatchSnapshot(); +const mockProps = () => ({ + onExitFullScreenMode: jest.fn(), + logos: getLogosMock.default, }); -describe('onExitFullScreenMode', () => { - test('is called when the button is pressed', () => { - const onExitHandler = sinon.stub(); +describe('ExitFullScreenButton', () => { + it("is rendered using the dark theme's mark by default", () => { + const props = { + ...mockProps(), + }; + const component = shallow(); + // In light color-scheme, the button has a dark background + const icons = component.find(`EuiIcon[type="${props.logos.Mark.dark.url}"]`); - const component = mount(); + expect(icons.length).toEqual(1); - component.find('button').simulate('click'); + expect(component).toMatchSnapshot(); + }); - sinon.assert.calledOnce(onExitHandler); + it("is rendered using the dark theme's mark when light color-scheme is applied", () => { + const props = { + ...mockProps(), + logos: { ...getLogosMock.default, colorScheme: ColorScheme.LIGHT }, + }; + const component = shallow(); + // In light color-scheme, the button has a dark background + const icons = component.find(`EuiIcon[type="${props.logos.Mark.dark.url}"]`); + + expect(icons.length).toEqual(1); + + expect(component).toMatchSnapshot(); }); - test('is called when the ESC key is pressed', () => { - const onExitHandler = sinon.stub(); + it("is rendered using the light theme's mark when dark color-scheme is applied", () => { + const props = { + ...mockProps(), + logos: { ...getLogosMock.default, colorScheme: ColorScheme.DARK }, + }; + const component = shallow(); + // In light color-scheme, the button has a dark background + const icons = component.find(`EuiIcon[type="${props.logos.Mark.light.url}"]`); + + expect(icons.length).toEqual(1); + + expect(component).toMatchSnapshot(); + }); +}); + +describe('onExitFullScreenMode', () => { + it('is called when the button is pressed', () => { + const props = { + ...mockProps(), + }; + const component = shallow(); + component.find('button').simulate('click'); + + expect(props.onExitFullScreenMode).toHaveBeenCalledTimes(1); + }); - mount(); + it('is called when the ESC key is pressed', () => { + const props = { + ...mockProps(), + }; + shallow(); const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any); document.dispatchEvent(escapeKeyEvent); - sinon.assert.calledOnce(onExitHandler); + expect(props.onExitFullScreenMode).toHaveBeenCalledTimes(1); }); }); diff --git a/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.tsx index 523f8345088f..cf13d0f224c4 100644 --- a/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.tsx +++ b/src/plugins/opensearch_dashboards_react/public/exit_full_screen_button/exit_full_screen_button.tsx @@ -32,14 +32,14 @@ import { i18n } from '@osd/i18n'; import React, { PureComponent } from 'react'; import { EuiScreenReaderOnly, keys } from '@elastic/eui'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { Logos } from 'opensearch-dashboards/public'; export interface ExitFullScreenButtonProps { onExitFullScreenMode: () => void; + logos: Logos; } import './index.scss'; -// eslint-disable-next-line @osd/eslint/no-restricted-paths -import OpenSearchMarkDarkMode from '../../../home/public/assets/logos/opensearch_mark_darkmode.svg'; class ExitFullScreenButtonUi extends PureComponent { public onKeyDown = (e: KeyboardEvent) => { @@ -57,6 +57,7 @@ class ExitFullScreenButtonUi extends PureComponent { } public render() { + const colorScheme = this.props.logos.colorScheme !== 'dark' ? 'dark' : 'light'; return (
@@ -83,7 +84,7 @@ class ExitFullScreenButtonUi extends PureComponent { > - +
diff --git a/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/__snapshots__/overview_page_header.test.tsx.snap b/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/__snapshots__/overview_page_header.test.tsx.snap index 72a25d779ded..326a5b51718e 100644 --- a/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/__snapshots__/overview_page_header.test.tsx.snap +++ b/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/__snapshots__/overview_page_header.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`OverviewPageHeader in dark mode render logo as custom dark mode logo 1`] = ` +exports[`OverviewPageHeader renders with the toolbar by default 1`] = `
@@ -38,7 +38,7 @@ exports[`OverviewPageHeader in dark mode render logo as custom dark mode logo className="osdOverviewPageHeader__actionItem" grow={false} > - Add data - - - - - -
-
-`; - -exports[`OverviewPageHeader in dark mode render logo as custom default mode logo 1`] = ` -
-
- - - - - -

- Page Title -

-
-
-
-
- - - - - - Add data - - - - - -
-
-
-`; - -exports[`OverviewPageHeader in dark mode render logo as original dark mode opensearch mark 1`] = ` -
-
- - - - - -

- Page Title -

-
-
-
-
- - - - - - Add data - - - - - -
-
-
-`; - -exports[`OverviewPageHeader in default mode render logo as custom default mode logo 1`] = ` -
-
- - - - - -

- Page Title -

-
-
-
-
- - - - - - Add data - - - - - -
-
-
-`; - -exports[`OverviewPageHeader in default mode render logo as original default mode opensearch mark 1`] = ` -
-
- - - - - -

- Page Title -

-
-
-
-
- - - - - - Add data - - + diff --git a/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.test.tsx b/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.test.tsx index d8290f0dbcb3..2e27ebd0cb6b 100644 --- a/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.test.tsx +++ b/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.test.tsx @@ -31,95 +31,265 @@ import React from 'react'; import { OverviewPageHeader } from './overview_page_header'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { useOpenSearchDashboards } from '../../context'; +import { getLogosMock } from '../../../../../core/common/mocks'; -jest.mock('../../app_links', () => ({ - RedirectAppLinks: jest.fn((element: JSX.Element) => element), -})); - -jest.mock('../../context', () => ({ - useOpenSearchDashboards: jest.fn().mockReturnValue({ - services: { - application: { capabilities: { navLinks: { management: true, dev_tools: true } } }, - notifications: { toast: { addSuccess: jest.fn() } }, - }, - }), -})); - -afterAll(() => jest.clearAllMocks()); +jest.mock('../../context', () => ({ useOpenSearchDashboards: jest.fn() })); const mockTitle = 'Page Title'; const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); +const mockProps = () => ({ + addBasePath: addBasePathMock, + title: mockTitle, + branding: {}, + logos: getLogosMock.default, +}); + describe('OverviewPageHeader ', () => { - describe('in default mode ', () => { - test('render logo as custom default mode logo', () => { - const branding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeLogo', - darkModeUrl: '/darkModeLogo', - }, - }; - - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); + beforeAll(() => { + // @ts-ignore + useOpenSearchDashboards.mockImplementation(() => ({ + services: { + application: { capabilities: { navLinks: { management: true, dev_tools: true } } }, + notifications: { toast: { addSuccess: jest.fn() } }, + }, + })); + }); - test('render logo as original default mode opensearch mark', () => { - const branding = { - darkMode: false, - mark: {}, - }; + afterAll(() => { + jest.clearAllMocks(); + }); - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); + it('renders without overlap by default', () => { + const props = { + ...mockProps(), + }; + + const component = shallowWithIntl(); + + const header = component.find('header'); + expect(header.hasClass('osdOverviewPageHeader--hasOverlap')).toBeFalsy(); + expect(header.hasClass('osdOverviewPageHeader--noOverlap')).toBeTruthy(); }); - describe('in dark mode ', () => { - test('render logo as custom dark mode logo', () => { - const branding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeLogo', - darkModeUrl: '/darkModeLogo', - }, - }; - - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); + it('renders with overlap', () => { + const props = { + ...mockProps(), + overlap: true, + }; + + const component = shallowWithIntl(); + + const header = component.find('header'); + expect(header.hasClass('osdOverviewPageHeader--hasOverlap')).toBeTruthy(); + expect(header.hasClass('osdOverviewPageHeader--noOverlap')).toBeFalsy(); + }); + + it('renders without an icon by default', () => { + const props = { + ...mockProps(), + }; + + const component = shallowWithIntl(); + + const icons = component.find({ 'data-test-subj': 'osdOverviewPageHeaderLogo' }); + expect(icons.length).toBe(0); + }); + + it('renders the mark icon when asked to', () => { + const props = { + ...mockProps(), + showIcon: true, + }; + + const component = shallowWithIntl(); + + const icons = component.find({ 'data-test-subj': 'osdOverviewPageHeaderLogo' }); + expect(icons.length).toBe(1); + expect(icons.first().prop('type')).toEqual(props.logos.Mark.url); + }); + + it('uses the provided title in the header', () => { + const props = { + ...mockProps(), + }; + + const component = shallowWithIntl(); + + const head = component.find('h1'); + expect(head.length).toBe(1); + expect(head.first().text()).toEqual(mockTitle); + }); + + it('renders with the toolbar by default', () => { + const props = { + ...mockProps(), + }; + + const component = shallowWithIntl(); + + const items = component.find('header > div > EuiFlexGroup > EuiFlexItem'); + expect(items.length).toBe(2); + + const buttons = component.find({ className: 'osdOverviewPageHeader__actionButton' }); + // This also validates the order of the items + const btnAddData = buttons.at(0); + expect(btnAddData.prop('iconType')).toEqual('indexOpen'); + expect(btnAddData.prop('href')).toEqual('/app/home#/tutorial_directory'); + + // Would contain only the "Add Data" button + expect(component).toMatchSnapshot(); + }); + + it('renders with the toolbar when it is explicitly asked not to be hidden', () => { + const props = { + ...mockProps(), + hideToolbar: false, + }; + + const component = shallowWithIntl(); + + const items = component.find('header > div > EuiFlexGroup > EuiFlexItem'); + expect(items.length).toBe(2); + + const buttons = component.find({ className: 'osdOverviewPageHeader__actionButton' }); + // This also validates the order of the items + const btnAddData = buttons.at(0); + expect(btnAddData.prop('iconType')).toEqual('indexOpen'); + expect(btnAddData.prop('href')).toEqual('/app/home#/tutorial_directory'); + }); + + it('renders without the toolbar when it is explicitly asked to be hidden', () => { + const props = { + ...mockProps(), + hideToolbar: true, + }; - test('render logo as custom default mode logo', () => { - const branding = { - darkMode: false, - mark: { - defaultUrl: '/defaultModeLogo', - }, - }; - - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); + const component = shallowWithIntl(); + + const items = component.find('header > div > EuiFlexGroup > EuiFlexItem'); + expect(items.length).toBe(1); + + const buttons = component.find({ className: 'osdOverviewPageHeader__actionButton' }); + expect(buttons.length).toBe(0); + }); +}); + +describe('OverviewPageHeader toolbar items - Management', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + const setupAndGetButton = (management: boolean, showManagementLink?: boolean) => { + // @ts-ignore + useOpenSearchDashboards.mockImplementation(() => ({ + services: { + application: { capabilities: { navLinks: { management, dev_tools: true } } }, + notifications: { toast: { addSuccess: jest.fn() } }, + }, + })); + + const props = mockProps(); + if (showManagementLink !== undefined) { + // @ts-ignore + props.showManagementLink = showManagementLink; + } + + const component = shallowWithIntl(); + + return component.find({ + className: 'osdOverviewPageHeader__actionButton', + href: '/app/management', }); + }; + + it('renders without management when the management plugin is disabled', () => { + const btnManagement = setupAndGetButton(false); + expect(btnManagement.length).toEqual(0); + }); + + it('renders without management when the management plugin is disabled and asked not to show', () => { + const btnManagement = setupAndGetButton(false, false); + expect(btnManagement.length).toEqual(0); + }); + + it('renders without management when the management plugin is disabled even if asked to show', () => { + const btnManagement = setupAndGetButton(false, true); + expect(btnManagement.length).toEqual(0); + }); + + it('renders without management when the management plugin is enabled', () => { + const btnManagement = setupAndGetButton(true); + expect(btnManagement.length).toEqual(0); + }); + + it('renders without management when the management plugin is enabled but asked not to show', () => { + const btnManagement = setupAndGetButton(true, false); + expect(btnManagement.length).toEqual(0); + }); + + it('renders with management when the management plugin is enabled and asked to show', () => { + const btnManagement = setupAndGetButton(true, true); + expect(btnManagement.length).toEqual(1); + }); +}); + +describe('OverviewPageHeader toolbar items - DevTools', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + const setupAndGetButton = (devTools: boolean, showDevToolsLink?: boolean) => { + // @ts-ignore + useOpenSearchDashboards.mockImplementation(() => ({ + services: { + application: { capabilities: { navLinks: { management: true, dev_tools: devTools } } }, + notifications: { toast: { addSuccess: jest.fn() } }, + }, + })); + + const props = mockProps(); + if (showDevToolsLink !== undefined) { + // @ts-ignore + props.showDevToolsLink = showDevToolsLink; + } - test('render logo as original dark mode opensearch mark', () => { - const branding = { - darkMode: false, - mark: {}, - }; + const component = shallowWithIntl(); - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); + return component.find({ + className: 'osdOverviewPageHeader__actionButton', + href: '/app/dev_tools#/console', }); + }; + + it('renders without dev_tools when the dev_tools plugin is disabled', () => { + const btnDevTools = setupAndGetButton(false); + expect(btnDevTools.length).toEqual(0); + }); + + it('renders without dev_tools when the dev_tools plugin is disabled and asked not to show', () => { + const btnDevTools = setupAndGetButton(false, false); + expect(btnDevTools.length).toEqual(0); + }); + + it('renders without dev_tools when the dev_tools plugin is disabled even if asked to show', () => { + const btnDevTools = setupAndGetButton(false, true); + expect(btnDevTools.length).toEqual(0); + }); + + it('renders without dev_tools when the dev_tools plugin is enabled', () => { + const btnDevTools = setupAndGetButton(true); + expect(btnDevTools.length).toEqual(0); + }); + + it('renders without dev_tools when the dev_tools plugin is enabled but asked not to show', () => { + const btnDevTools = setupAndGetButton(true, false); + expect(btnDevTools.length).toEqual(0); + }); + + it('renders with dev_tools when the dev_tools plugin is enabled and asked to show', () => { + const btnDevTools = setupAndGetButton(true, true); + expect(btnDevTools.length).toEqual(1); }); }); diff --git a/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.tsx b/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.tsx index 00273fcf993b..a636f7ecdb7d 100644 --- a/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.tsx +++ b/src/plugins/opensearch_dashboards_react/public/overview_page/overview_page_header/overview_page_header.tsx @@ -38,7 +38,7 @@ import { IconType, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { CoreStart } from 'opensearch-dashboards/public'; +import { CoreStart, Logos } from 'opensearch-dashboards/public'; import { RedirectAppLinks } from '../../app_links'; import { useOpenSearchDashboards } from '../../context'; import { ReactPluginBranding } from '../..'; @@ -47,24 +47,28 @@ import './index.scss'; interface Props { hideToolbar?: boolean; + /** @deprecated use showIcon */ iconType?: IconType; + showIcon?: boolean; overlap?: boolean; showDevToolsLink?: boolean; showManagementLink?: boolean; title: JSX.Element | string; addBasePath: (path: string) => string; - branding: ReactPluginBranding; + logos: Logos; + /** @deprecated use logos */ + branding?: ReactPluginBranding; } export const OverviewPageHeader: FC = ({ hideToolbar, - iconType, + showIcon = false, overlap, showDevToolsLink, showManagementLink, title, addBasePath, - branding, + logos, }) => { const { services: { application }, @@ -75,58 +79,6 @@ export const OverviewPageHeader: FC = ({ dev_tools: isDevToolsEnabled, } = application.capabilities.navLinks; - 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 - * overview logo in default mode. In default mode, overview logo will - * proritize default mode mark URL. If it is invalid, default opensearch logo - * will be rendered. - * - * @returns a valid custom URL or undefined if no valid URL is provided - */ - const customOverviewLogoDefaultMode = () => { - return markDefault ?? DEFAULT_OPENSEARCH_MARK; - }; - - /** - * Use branding configurations to check which URL to use for rendering - * overview logo in dark mode. In dark mode, overview logo will render - * dark mode mark URL if valid. Otherwise, it will render the default - * mode mark URL if valid. If both dark mode mark URL and default mode mark - * URL are invalid, the default opensearch logo will be rendered. - * - * @returns a valid custom URL or undefined if no valid URL is provided - */ - const customOverviewLogoDarkMode = () => { - return markDarkMode ?? markDefault ?? DARKMODE_OPENSEARCH_MARK; - }; - - /** - * Render custom overview logo for both default mode and dark mode - * - * @returns a valid custom loading logo URL, or undefined - */ - const customOverviewLogo = () => { - return darkMode ? customOverviewLogoDarkMode() : customOverviewLogoDefaultMode(); - }; - - /** - * Check if we render a custom overview logo or the default opensearch spinner. - * If customOverviewLogo() returns undefined(no valid custom URL is found), we - * render the default opensearch logo - * - * @returns a image component with custom logo URL, or the default opensearch logo - */ - const renderBrandingEnabledOrDisabledLogo = (iconTypeInput?: IconType) => { - return customOverviewLogo() ?? iconTypeInput ?? ''; - }; - return (
= ({ - {iconType && ( + {showIcon && ( )}