From f014d09d7ce080743c70406f9eac321fb53590cf Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Mon, 1 Nov 2021 11:41:12 -0700 Subject: [PATCH] [1.x] Custom Branding (#826) * Make top left logo on the main screen configurable Add a new config opensearchDashboards.branding.logoUrl in yaml file for making top left corner logo on the main screen configurable. If URL is invalid, the default OpenSearch logo will be shown. Signed-off-by: Abby Hu * Welcome page title and logo configurable (#738) Add two new configs branding.smallLogoUrl and branding.title in the yaml file for making the welcome page logo and title configurable. If URL is invalid, the default branding will be shown. Signed-off-by: Qingyang(Abby) Hu * Make loading page logo and title configurable (#746) Add one new config branding.loadingLogoUrl for making loading page logo configurable. URL can be in svg and gif format. If no loading logo is found, the static logo with a horizontal bar loading bar will be shown. If logo is also not found, the default OpenSearch loading logo and spinner will be shown. Signed-off-by: Qingyang(Abby) Hu * Branding configs rename and improvement (#771) Change config smallLogoUrl to logoUrl, config logoUrl to fullLogoUrl to emphasize that thumbnail version of the logo will be used mostly in the application. Full version of the logo will only be used on the main page nav bar. If full logo is not provided, thumbnail logo will be used on the nav bar. Some config improvement includes fixing the validation error when inputting empty string, and add title validation function. Signed-off-by: Qingyang(Abby) Hu * Branding config structure change and renaming (#793) Change the branding related config to a map structure in the yml file. Also rename the configs according to the official branding guidelines. The full logo on the main page header will be called logo; the small logo icon will be called mark. Signed-off-by: Qingyang(Abby) Hu * Darkmode configurations for header logo, welcome logo and loading logo (#797) Add dark mode configs in the yml file that allows user to configure a dark mode version of the logo. When user toggles dark mode under the Advanced Setting, the logo will be rendered accordingly. Signed-off-by: Qingyang(Abby) Hu * Add favicon configuration (#801) Added a configuration on favicon inside opensearchDashboards.branding in the yml file. If user inputs a valid URL, we gurantee basic browser favicon customization, while remaining places show the default browser/device favicon icon. If user does not provide a valid URL for favicon, the opensearch favicon icon will be used. Signed-off-by: Qingyang(Abby) Hu * Make home page primary dashboard card logo and title configurable (#809) Home page dashboard card logo and title can be customized by config mark.defaultUrl and mark.darkModeUrl. Unit test and functional test are also written. Signed-off-by: Qingyang(Abby) Hu * Side menu logo configuration Make logo for opensearch dashboard side menu be configurable. Use config mark.defaultUrl and mark.darkModeUrl. Signed-off-by: Abby Hu * Overview Header Logo Configuration Make logo for opensearch dashboard overview header logo be configurable. Use config mark.defaultUrl and mark.darkModeUrl. Signed-off-by: Abby Hu * Redirect URL not allowed Add an addtional parameter to the checkUrlValid function so that max redirect count is 0. We do not allow URLs that can be redirected because of potential security issues. Signed-off-by: Abby Hu * Store default opensearch branding asset folder Store the original opensearch branding logos in an asset folder, instead of making API calls. Signed-off-by: Abby Hu * [Branding] handle comments from PR Handling the helper function rename and grammar issues. To avoid risk, we will not remove the duplicate code for 1.2 and everything related to those comments (ie function renames). That will be handled in 1.3. Here is the issue tracking it: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/895 Signed-off-by: Kawika Avilla Co-authored-by: Kawika Avilla Backport PR: https://github.com/opensearch-project/OpenSearch-Dashboards/pull/897 --- config/opensearch_dashboards.yml | 13 + src/core/public/chrome/chrome_service.tsx | 5 + .../collapsible_nav.test.tsx.snap | 6059 ++++++++++++++++- .../header/__snapshots__/header.test.tsx.snap | 97 +- ...earch_dashboards_custom_logo.test.tsx.snap | 1013 +++ ...opensearch_dashboards_custom_logo.test.tsx | 105 + .../opensearch_dashboards_custom_logo.tsx | 98 + .../chrome/ui/header/collapsible_nav.test.tsx | 81 + .../chrome/ui/header/collapsible_nav.tsx | 44 +- .../public/chrome/ui/header/header.test.tsx | 6 + src/core/public/chrome/ui/header/header.tsx | 6 +- .../public/chrome/ui/header/header_logo.scss | 9 + .../public/chrome/ui/header/header_logo.tsx | 16 +- src/core/public/core_system.ts | 2 +- src/core/public/index.ts | 4 + .../injected_metadata_service.mock.ts | 1 + .../injected_metadata_service.test.ts | 13 + .../injected_metadata_service.ts | 8 +- src/core/public/mocks.ts | 1 + src/core/public/plugins/plugin_context.ts | 2 + .../public/plugins/plugins_service.test.ts | 4 +- .../default_branding/opensearch_logo.svg} | 68 +- .../opensearch_mark_dark_mode.svg | 34 + .../opensearch_mark_default_mode.svg | 34 + .../server/opensearch_dashboards_config.ts | 32 + .../rendering/__mocks__/rendering_service.ts | 4 + .../rendering_service.test.ts.snap | 40 + .../rendering/rendering_service.test.ts | 58 +- .../server/rendering/rendering_service.tsx | 244 +- src/core/server/rendering/types.ts | 35 + .../__snapshots__/template.test.tsx.snap | 1480 ++++ src/core/server/rendering/views/styles.tsx | 10 + .../server/rendering/views/template.test.tsx | 222 + src/core/server/rendering/views/template.tsx | 126 +- src/core/server/types.ts | 1 + src/core/types/custom_branding.ts | 62 + src/core/types/index.ts | 1 + src/legacy/server/config/schema.js | 16 + .../__snapshots__/home.test.js.snap | 17 + .../__snapshots__/welcome.test.tsx.snap | 387 +- .../components/_solutions_section.scss | 10 + .../application/components/_welcome.scss | 10 + .../public/application/components/home.js | 4 +- .../application/components/home.test.js | 3 + .../solution_panel.test.tsx.snap | 10 + .../solution_title.test.tsx.snap | 234 +- .../solutions_section.test.tsx.snap | 80 + .../solutions_section/solution_panel.test.tsx | 11 +- .../solutions_section/solution_panel.tsx | 5 +- .../solutions_section/solution_title.test.tsx | 126 +- .../solutions_section/solution_title.tsx | 114 +- .../solutions_section.test.tsx | 13 + .../solutions_section/solutions_section.tsx | 12 +- .../application/components/welcome.test.tsx | 133 +- .../public/application/components/welcome.tsx | 96 +- .../opensearch_dashboards_services.ts | 5 + src/plugins/home/public/index.ts | 1 + src/plugins/home/public/plugin.ts | 5 + .../public/application.tsx | 2 + .../public/components/app.tsx | 10 +- .../__snapshots__/overview.test.tsx.snap | 27 + .../components/overview/overview.test.tsx | 23 +- .../public/components/overview/overview.tsx | 6 +- .../public/plugin.ts | 4 + .../public/index.ts | 4 + .../overview_page_header.test.tsx.snap | 270 +- .../overview_page_header.test.tsx | 77 +- .../overview_page_header.tsx | 62 +- test/common/config.js | 8 +- .../apps/visualize/_custom_branding.js | 228 + test/functional/apps/visualize/index.ts | 1 + test/functional/page_objects/common_page.ts | 3 +- test/functional/services/global_nav.ts | 9 + 73 files changed, 11871 insertions(+), 193 deletions(-) create mode 100644 src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap create mode 100644 src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx create mode 100644 src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx create mode 100644 src/core/public/chrome/ui/header/header_logo.scss rename src/core/{public/chrome/ui/header/branding/opensearch_dashboards_logo_darkmode.tsx => server/core_app/assets/default_branding/opensearch_logo.svg} (92%) create mode 100644 src/core/server/core_app/assets/default_branding/opensearch_mark_dark_mode.svg create mode 100644 src/core/server/core_app/assets/default_branding/opensearch_mark_default_mode.svg create mode 100644 src/core/server/rendering/views/__snapshots__/template.test.tsx.snap create mode 100644 src/core/server/rendering/views/template.test.tsx create mode 100644 src/core/types/custom_branding.ts create mode 100644 test/functional/apps/visualize/_custom_branding.js diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 0736e064043e..143dac3c175e 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -148,3 +148,16 @@ # 'ff00::/8', # ] #vis_type_timeline.graphiteBlockedIPs: [] + +# opensearchDashboards.branding: + # logo: + # defaultUrl: "" + # darkModeUrl: "" + # mark: + # defaultUrl: "" + # darkModeUrl: "" + # loadingLogo: + # defaultUrl: "" + # darkModeUrl: "" + # faviconUrl: "" + # applicationTitle: "" \ No newline at end of file diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 93747ee2501e..cf004a47c5cf 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -51,6 +51,7 @@ import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; +import { Branding } from '../'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -71,6 +72,9 @@ export interface ChromeBrand { /** @public */ export type ChromeBreadcrumb = EuiBreadcrumb; +/** @public */ +export type ChromeBranding = Branding; + /** @public */ export interface ChromeHelpExtension { /** @@ -253,6 +257,7 @@ export class ChromeService { navControlsRight$={navControls.getRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} + branding={injectedMetadata.getBranding()} /> ), 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 491a8a4138ad..8c4e6e7d2461 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,6 +60,15 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "serverBasePath": "/test", } } + branding={ + Object { + "darkMode": false, + "mark": Object { + "darkModeUrl": "/darkModeLogo", + "defaultUrl": "/defaultModeLogo", + }, + } + } closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -853,8 +862,9 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiFlexItem eui-yScroll" > @@ -893,6 +903,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="/defaultModeLogo" data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} @@ -903,6 +914,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -951,10 +963,10 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -1143,6 +1155,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="logoObservability" data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} @@ -1193,6 +1207,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -1394,6 +1409,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="logoSecurity" data-test-subj="collapsibleNavGroup-securitySolution" id="mockId" initialIsOpen={true} @@ -1444,6 +1461,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -1606,6 +1624,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="managementApp" data-test-subj="collapsibleNavGroup-management" id="mockId" initialIsOpen={true} @@ -1656,6 +1676,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -2083,6 +2104,15 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "serverBasePath": "/test", } } + branding={ + Object { + "darkMode": false, + "mark": Object { + "darkModeUrl": "/darkModeLogo", + "defaultUrl": "/defaultModeLogo", + }, + } + } closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -2318,6 +2348,15 @@ exports[`CollapsibleNav renders the default nav 2`] = ` "serverBasePath": "/test", } } + branding={ + Object { + "darkMode": false, + "mark": Object { + "darkModeUrl": "/darkModeLogo", + "defaultUrl": "/defaultModeLogo", + }, + } + } closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -2554,6 +2593,15 @@ exports[`CollapsibleNav renders the default nav 3`] = ` "serverBasePath": "/test", } } + branding={ + Object { + "darkMode": false, + "mark": Object { + "darkModeUrl": "/darkModeLogo", + "defaultUrl": "/defaultModeLogo", + }, + } + } closeNav={[Function]} customNavLink$={ BehaviorSubject { @@ -3176,3 +3224,6006 @@ exports[`CollapsibleNav renders the default nav 3`] = ` `; + +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 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 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 9cef3e2d9aea..5ad89191b0c9 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,6 +247,18 @@ exports[`Header renders 1`] = ` "serverBasePath": "/test", } } + branding={ + Object { + "applicationTitle": "OpenSearch Dashboards", + "darkMode": false, + "logo": Object { + "defaultUrl": "/", + }, + "mark": Object { + "defaultUrl": "/", + }, + } + } breadcrumbs$={ BehaviorSubject { "_isScalar": false, @@ -1665,6 +1677,18 @@ exports[`Header renders 1`] = ` "borders": "none", "items": Array [ - - - -
- - - +
+ +
@@ -5221,6 +5262,18 @@ exports[`Header renders 1`] = ` "serverBasePath": "/test", } } + branding={ + Object { + "applicationTitle": "OpenSearch Dashboards", + "darkMode": false, + "logo": Object { + "defaultUrl": "/", + }, + "mark": Object { + "defaultUrl": "/", + }, + } + } closeNav={[Function]} customNavLink$={ BehaviorSubject { diff --git a/src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap b/src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap new file mode 100644 index 000000000000..80b66e95bd41 --- /dev/null +++ b/src/core/public/chrome/ui/header/branding/__snapshots__/opensearch_dashboards_custom_logo.test.tsx.snap @@ -0,0 +1,1013 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header logo in dark mode rendered using logo dark mode URL 1`] = ` + +
+ custom title logo +
+
+`; + +exports[`Header logo in dark mode rendered using logo default mode URL 1`] = ` + +
+ custom title logo +
+
+`; + +exports[`Header logo in dark mode rendered using mark dark mode URL 1`] = ` + +
+ custom title logo +
+
+`; + +exports[`Header logo in dark mode rendered using mark default mode URL 1`] = ` + +
+ custom title logo +
+
+`; + +exports[`Header logo in dark mode rendered using original opensearch logo 1`] = ` + + custom title logo + +`; + +exports[`Header logo in default mode rendered using logo default mode URL 1`] = ` + +
+ custom title logo +
+
+`; + +exports[`Header logo in default mode rendered using mark default mode URL 1`] = ` + +
+ custom title logo +
+
+`; + +exports[`Header logo in default mode rendered using the original opensearch logo 1`] = ` + + custom title logo + +`; diff --git a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx b/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx new file mode 100644 index 000000000000..8de0812b9c24 --- /dev/null +++ b/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.test.tsx @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { CustomLogo } from './opensearch_dashboards_custom_logo'; + +describe('Header logo ', () => { + describe('in default mode ', () => { + it('rendered using logo default mode URL', () => { + const branding = { + darkMode: false, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: {}, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('rendered using mark default mode URL', () => { + const branding = { + darkMode: false, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('rendered using the original opensearch logo', () => { + const branding = { + darkMode: false, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('in dark mode ', () => { + it('rendered using logo dark mode URL', () => { + const branding = { + darkMode: true, + logo: { defaultUrl: '/defaultModeLogo', darkModeUrl: '/darkModeLogo' }, + mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('rendered using logo default mode URL', () => { + const branding = { + darkMode: true, + logo: { defaultUrl: '/defaultModeLogo' }, + mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('rendered using mark dark mode URL', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark', darkModeUrl: '/darkModeMark' }, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('rendered using mark default mode URL', () => { + const branding = { + darkMode: true, + logo: {}, + mark: { defaultUrl: '/defaultModeMark' }, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('rendered using original opensearch logo', () => { + const branding = { + darkMode: true, + logo: {}, + mark: {}, + applicationTitle: 'custom title', + }; + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx b/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx new file mode 100644 index 000000000000..aaa12a79a077 --- /dev/null +++ b/src/core/public/chrome/ui/header/branding/opensearch_dashboards_custom_logo.tsx @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import '../header_logo.scss'; +import { ChromeBranding } from '../../../chrome_service'; + +/** + * Use branding configurations to render the header logo on the nav bar. + * + * @param {ChromeBranding} - branding object consist of logo, mark and title + * @returns Custom branding logo component which is going to be rendered on the main page header bar. + * If logo default is valid, the full logo by logo default config will be rendered; + * if not, the logo icon by mark default config will be rendered; if both are not found, + * the default OpenSearch Dashboards logo will be rendered. + */ +export const CustomLogo = ({ ...branding }: ChromeBranding) => { + const darkMode = branding.darkMode; + const assetFolderUrl = branding.assetFolderUrl; + const logoDefault = branding.logo?.defaultUrl; + const logoDarkMode = branding.logo?.darkModeUrl; + const markDefault = branding.mark?.defaultUrl; + const markDarkMode = branding.mark?.darkModeUrl; + const applicationTitle = branding.applicationTitle; + + /** + * Use branding configurations to check which URL to use for rendering + * header logo in nav bar in default mode + * + * @returns a valid custom URL or undefined if no valid URL is provided + */ + const customHeaderLogoDefaultMode = () => { + return logoDefault ?? markDefault ?? undefined; + }; + + /** + * Use branding configurations to check which URL to use for rendering + * header logo in nav bar in dark mode + * + * @returns a valid custom URL or undefined if no valid URL is provided + */ + const customHeaderLogoDarkMode = () => { + return logoDarkMode ?? logoDefault ?? markDarkMode ?? markDefault ?? undefined; + }; + + /** + * Render custom header logo for both default mode and dark mode + * + * @returns a valid custom header logo URL, or undefined + */ + const customHeaderLogo = () => { + return darkMode ? customHeaderLogoDarkMode() : customHeaderLogoDefaultMode(); + }; + + return customHeaderLogo() ? ( +
+ {applicationTitle +
+ ) : ( + {applicationTitle + ); +}; diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 0a1cb0d9fa7a..e9824d09db38 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -82,6 +82,13 @@ function mockProps() { navigateToApp: () => Promise.resolve(), navigateToUrl: () => Promise.resolve(), customNavLink$: new BehaviorSubject(undefined), + branding: { + darkMode: false, + mark: { + defaultUrl: '/defaultModeLogo', + darkModeUrl: '/darkModeLogo', + }, + }, }; } @@ -206,4 +213,78 @@ describe('CollapsibleNav', () => { expect(onClose.callCount).toEqual(3); 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(); + + // check if nav bar renders the original default mode opensearch mark + component.setProps({ + branding: { + darkMode: false, + mark: {}, + }, + }); + 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', + }, + }, + }); + expect(component).toMatchSnapshot(); + + // check if nav bar renders default mode custom logo + component.setProps({ + branding: { + darkMode: true, + mark: { + defaultUrl: '/defaultModeLogo', + }, + }, + }); + expect(component).toMatchSnapshot(); + + // check if nav bar renders the original dark mode opensearch mark + component.setProps({ + branding: { + darkMode: false, + mark: {}, + }, + }); + 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 c4d415a5dcc9..0ed510933257 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -52,6 +52,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'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -102,6 +103,7 @@ interface Props { navigateToApp: InternalApplicationStart['navigateToApp']; navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; + branding: ChromeBranding; } export function CollapsibleNav({ @@ -115,6 +117,7 @@ export function CollapsibleNav({ closeNav, navigateToApp, navigateToUrl, + branding, ...observables }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); @@ -137,6 +140,42 @@ 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; return ( setIsCategoryOpen(category.id, isCategoryOpen, storage)} data-test-subj={`collapsibleNavGroup-${category.id}`} + data-test-opensearch-logo={opensearchLinkLogo} > {}, + branding: { + darkMode: false, + logo: { defaultUrl: '/' }, + mark: { defaultUrl: '/' }, + applicationTitle: 'OpenSearch Dashboards', + }, }; } diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 5021461dd0b6..2946b4eaa130 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -55,7 +55,7 @@ import { } from '../..'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; -import { ChromeHelpExtension } from '../../chrome_service'; +import { ChromeHelpExtension, ChromeBranding } from '../../chrome_service'; import { OnIsLockedUpdate } from './'; import { CollapsibleNav } from './collapsible_nav'; import { HeaderBadge } from './header_badge'; @@ -87,6 +87,7 @@ export interface HeaderProps { isLocked$: Observable; loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; + branding: ChromeBranding; } export function Header({ @@ -96,6 +97,7 @@ export function Header({ basePath, onIsLockedUpdate, homeHref, + branding, ...observables }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); @@ -125,6 +127,7 @@ export function Header({ forceNavigation$={observables.forceAppSwitcherNavigation$} navLinks$={observables.navLinks$} navigateToApp={application.navigateToApp} + branding={branding} />, , ], @@ -213,6 +216,7 @@ export function Header({ } }} customNavLink$={observables.customNavLink$} + branding={branding} /> diff --git a/src/core/public/chrome/ui/header/header_logo.scss b/src/core/public/chrome/ui/header/header_logo.scss new file mode 100644 index 000000000000..1aa9be7e9acf --- /dev/null +++ b/src/core/public/chrome/ui/header/header_logo.scss @@ -0,0 +1,9 @@ +.logoContainer { + height: 30px; + padding: 3px 3px 3px 10px; +} + +.logoImage { + height: 100%; + max-width: 100%; +} \ No newline at end of file diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 36262fb3e317..39e5f4ecfdbc 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -30,14 +30,15 @@ * GitHub history for details. */ -import { EuiHeaderLogo } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; -import { OpenSearchDashboardsLogoDarkMode } from './branding/opensearch_dashboards_logo_darkmode'; +import { CustomLogo } from './branding/opensearch_dashboards_custom_logo'; +import { ChromeBranding } from '../../chrome_service'; +import './header_logo.scss'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -104,21 +105,24 @@ interface Props { navLinks$: Observable; forceNavigation$: Observable; navigateToApp: (appId: string) => void; + branding: ChromeBranding; } -export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { +export function HeaderLogo({ href, navigateToApp, branding, ...observables }: Props) { const forceNavigation = useObservable(observables.forceNavigation$, false); const navLinks = useObservable(observables.navLinks$, []); return ( - onClick(e, forceNavigation, navLinks, navigateToApp)} href={href} aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { defaultMessage: 'Go to home page', })} - /> + className="logoContainer" + > + + ); } diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9a071bfdf2fa..bb873ddd3f45 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -239,7 +239,7 @@ export class CoreSystem { docLinks, http, i18n, - injectedMetadata: pick(injectedMetadata, ['getInjectedVar']), + injectedMetadata: pick(injectedMetadata, ['getInjectedVar', 'getBranding']), notifications, overlays, savedObjects, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index f4ecc3848b96..ad14deb03cf4 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -89,6 +89,7 @@ import { HandlerContextType, HandlerParameters, } from './context'; +import { Branding } from '../types'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ @@ -236,6 +237,7 @@ export interface CoreSetup unknown; + getBranding: () => Branding; }; /** {@link StartServicesAccessor} */ getStartServices: StartServicesAccessor; @@ -291,6 +293,7 @@ export interface CoreStart { * */ injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; + getBranding: () => Branding; }; } @@ -337,6 +340,7 @@ export { IUiSettingsClient, UiSettingsState, NavType, + Branding, }; export { __osdBootstrap__ } from './osd_bootstrap'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 3e2a80f7cc8c..e7ab88fffd20 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -45,6 +45,7 @@ const createSetupContractMock = () => { getInjectedVar: jest.fn(), getInjectedVars: jest.fn(), getOpenSearchDashboardsBuildNumber: jest.fn(), + getBranding: jest.fn(), }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getOpenSearchDashboardsVersion.mockReturnValue('opensearchDashboardsVersion'); diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index 8b34e9d5a1e3..d21de7e96b64 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -229,3 +229,16 @@ describe('setup.getInjectedVars()', () => { ); }); }); + +describe('setup.getBranding()', () => { + it('returns injectedMetadata.branding', () => { + const injectedMetadata = new InjectedMetadataService({ + injectedMetadata: { + branding: { fullLogoUrl: '/', logoUrl: '/', title: 'title' }, + }, + } as any); + + const branding = injectedMetadata.setup().getBranding(); + expect(branding).toEqual({ fullLogoUrl: '/', logoUrl: '/', title: 'title' }); + }); +}); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index ef0ba0bf0bce..f821eb61a072 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -39,7 +39,7 @@ import { UiSettingsParams, UserProvidedValues, } from '../../server/types'; -import { AppCategory } from '../'; +import { AppCategory, Branding } from '../'; export interface InjectedPluginMetadata { id: PluginName; @@ -76,6 +76,7 @@ export interface InjectedMetadataParams { user?: Record; }; }; + branding: Branding; }; } @@ -143,6 +144,10 @@ export class InjectedMetadataService { getOpenSearchDashboardsBranch: () => { return this.state.branch; }, + + getBranding: () => { + return this.state.branding; + }, }; } } @@ -176,6 +181,7 @@ export interface InjectedMetadataSetup { getInjectedVars: () => { [key: string]: unknown; }; + getBranding: () => Branding; } /** @internal */ diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 166a335a3556..ac96b745dc90 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -103,6 +103,7 @@ function createCoreStartMock({ basePath = '' } = {}) { savedObjects: savedObjectsServiceMock.createStartContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, + getBranding: injectedMetadataServiceMock.createStartContract().getBranding, }, fatalErrors: fatalErrorsServiceMock.createStartContract(), }; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 69779b3162c1..d6c98e7126b2 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -120,6 +120,7 @@ export function createPluginSetupContext< uiSettings: deps.uiSettings, injectedMetadata: { getInjectedVar: deps.injectedMetadata.getInjectedVar, + getBranding: deps.injectedMetadata.getBranding, }, getStartServices: () => plugin.startDependencies, }; @@ -166,6 +167,7 @@ export function createPluginStartContext< savedObjects: deps.savedObjects, injectedMetadata: { getInjectedVar: deps.injectedMetadata.getInjectedVar, + getBranding: deps.injectedMetadata.getBranding, }, fatalErrors: deps.fatalErrors, }; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index c95cb42869ad..eedcd5290ca4 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -113,7 +113,7 @@ describe('PluginsService', () => { ...mockSetupDeps, application: expect.any(Object), getStartServices: expect.any(Function), - injectedMetadata: pick(mockSetupDeps.injectedMetadata, 'getInjectedVar'), + injectedMetadata: pick(mockSetupDeps.injectedMetadata, 'getInjectedVar', 'getBranding'), }; mockStartDeps = { application: applicationServiceMock.createInternalStartContract(), @@ -132,7 +132,7 @@ describe('PluginsService', () => { ...mockStartDeps, application: expect.any(Object), chrome: omit(mockStartDeps.chrome, 'getComponent'), - injectedMetadata: pick(mockStartDeps.injectedMetadata, 'getInjectedVar'), + injectedMetadata: pick(mockStartDeps.injectedMetadata, 'getInjectedVar', 'getBranding'), }; // Reset these for each test. diff --git a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_logo_darkmode.tsx b/src/core/server/core_app/assets/default_branding/opensearch_logo.svg similarity index 92% rename from src/core/public/chrome/ui/header/branding/opensearch_dashboards_logo_darkmode.tsx rename to src/core/server/core_app/assets/default_branding/opensearch_logo.svg index 665c8615738e..344f5e645e18 100644 --- a/src/core/public/chrome/ui/header/branding/opensearch_dashboards_logo_darkmode.tsx +++ b/src/core/server/core_app/assets/default_branding/opensearch_logo.svg @@ -1,39 +1,33 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ + -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ -import React from 'react'; - -export function OpenSearchDashboardsLogoDarkMode() { - return ( - + - - ); -} + \ No newline at end of file diff --git a/src/core/server/core_app/assets/default_branding/opensearch_mark_dark_mode.svg b/src/core/server/core_app/assets/default_branding/opensearch_mark_dark_mode.svg new file mode 100644 index 000000000000..148f5ad117db --- /dev/null +++ b/src/core/server/core_app/assets/default_branding/opensearch_mark_dark_mode.svg @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/src/core/server/core_app/assets/default_branding/opensearch_mark_default_mode.svg b/src/core/server/core_app/assets/default_branding/opensearch_mark_default_mode.svg new file mode 100644 index 000000000000..c8b4e58f2676 --- /dev/null +++ b/src/core/server/core_app/assets/default_branding/opensearch_mark_default_mode.svg @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/src/core/server/opensearch_dashboards_config.ts b/src/core/server/opensearch_dashboards_config.ts index 3f9415212be2..e59747048395 100644 --- a/src/core/server/opensearch_dashboards_config.ts +++ b/src/core/server/opensearch_dashboards_config.ts @@ -52,6 +52,38 @@ export const config = { index: schema.string({ defaultValue: '.kibana' }), autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), autocompleteTimeout: schema.duration({ defaultValue: 1000 }), + branding: schema.object({ + logo: schema.object({ + defaultUrl: schema.string({ + defaultValue: '/', + }), + darkModeUrl: schema.string({ + defaultValue: '/', + }), + }), + mark: schema.object({ + defaultUrl: schema.string({ + defaultValue: '/', + }), + darkModeUrl: schema.string({ + defaultValue: '/', + }), + }), + loadingLogo: schema.object({ + defaultUrl: schema.string({ + defaultValue: '/', + }), + darkModeUrl: schema.string({ + defaultValue: '/', + }), + }), + faviconUrl: schema.string({ + defaultValue: '/', + }), + applicationTitle: schema.string({ + defaultValue: '', + }), + }), }), deprecations, }; diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index d3db5f16bb43..98576eea07cd 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -41,9 +41,13 @@ export const setupMock: jest.Mocked = { }; export const mockSetup = jest.fn().mockResolvedValue(setupMock); export const mockStop = jest.fn(); +export const mockIsUrlValid = jest.fn(); +export const mockIsTitleValid = jest.fn(); export const mockRenderingService: jest.Mocked = { setup: mockSetup, stop: mockStop, + isUrlValid: mockIsUrlValid, + isTitleValid: mockIsTitleValid, }; export const RenderingService = jest.fn( () => mockRenderingService diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 07ca59a48c6b..a92325814a81 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -5,6 +5,14 @@ Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, + "branding": Object { + "applicationTitle": "OpenSearch Dashboards", + "assetFolderUrl": "/mock-server-basepath/ui/default_branding", + "darkMode": false, + "loadingLogo": Object {}, + "logo": Object {}, + "mark": Object {}, + }, "buildNumber": Any, "csp": Object { "warnLegacyBrowsers": true, @@ -48,6 +56,14 @@ Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, + "branding": Object { + "applicationTitle": "OpenSearch Dashboards", + "assetFolderUrl": "/mock-server-basepath/ui/default_branding", + "darkMode": false, + "loadingLogo": Object {}, + "logo": Object {}, + "mark": Object {}, + }, "buildNumber": Any, "csp": Object { "warnLegacyBrowsers": true, @@ -91,6 +107,14 @@ Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, + "branding": Object { + "applicationTitle": "OpenSearch Dashboards", + "assetFolderUrl": "/mock-server-basepath/ui/default_branding", + "darkMode": true, + "loadingLogo": Object {}, + "logo": Object {}, + "mark": Object {}, + }, "buildNumber": Any, "csp": Object { "warnLegacyBrowsers": true, @@ -138,6 +162,14 @@ Object { "anonymousStatusPage": false, "basePath": "", "branch": Any, + "branding": Object { + "applicationTitle": "OpenSearch Dashboards", + "assetFolderUrl": "/ui/default_branding", + "darkMode": false, + "loadingLogo": Object {}, + "logo": Object {}, + "mark": Object {}, + }, "buildNumber": Any, "csp": Object { "warnLegacyBrowsers": true, @@ -181,6 +213,14 @@ Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, + "branding": Object { + "applicationTitle": "OpenSearch Dashboards", + "assetFolderUrl": "/mock-server-basepath/ui/default_branding", + "darkMode": false, + "loadingLogo": Object {}, + "logo": Object {}, + "mark": Object {}, + }, "buildNumber": Any, "csp": Object { "warnLegacyBrowsers": true, diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index efeb37dcb298..61a7f0ce3dea 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -34,9 +34,13 @@ import { load } from 'cheerio'; import { httpServerMock } from '../http/http_server.mocks'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params'; +import { mockRenderingSetupDeps } from './__mocks__/params'; import { InternalRenderingServiceSetup } from './types'; import { RenderingService } from './rendering_service'; +import { configServiceMock } from '../config/mocks'; +import { BehaviorSubject } from 'rxjs'; +import { config as RawOpenSearchDashboardsConfig } from '../opensearch_dashboards_config'; +import { mockCoreContext } from '../core_context.mock'; const INJECTED_METADATA = { version: expect.any(String), @@ -62,10 +66,15 @@ const { createOpenSearchDashboardsRequest, createRawRequest } = httpServerMock; describe('RenderingService', () => { let service: RenderingService; + const configService = configServiceMock.create(); + configService.atPath.mockImplementation(() => { + return new BehaviorSubject(RawOpenSearchDashboardsConfig.schema.validate({})); + }); + const context = mockCoreContext.create({ configService }); beforeEach(() => { jest.clearAllMocks(); - service = new RenderingService(mockRenderingServiceParams); + service = new RenderingService(context); }); describe('setup()', () => { @@ -127,4 +136,49 @@ describe('RenderingService', () => { }); }); }); + + describe('isUrlValid()', () => { + it('checks valid SVG URL', async () => { + const result = await service.isUrlValid( + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg', + 'config' + ); + expect(result).toEqual(true); + }); + + it('checks valid PNG URL', async () => { + const result = await service.isUrlValid( + 'https://opensearch.org/assets/brand/PNG/Mark/opensearch_mark_default.png', + 'config' + ); + expect(result).toEqual(true); + }); + + it('checks invalid URL that does not contain svg, png or gif', async () => { + const result = await service.isUrlValid('https://validUrl', 'config'); + expect(result).toEqual(false); + }); + + it('checks invalid URL', async () => { + const result = await service.isUrlValid('http://notfound.svg', 'config'); + expect(result).toEqual(false); + }); + }); + + describe('isTitleValid()', () => { + it('checks valid title', () => { + const result = service.isTitleValid('OpenSearch Dashboards', 'config'); + expect(result).toEqual(true); + }); + + it('checks invalid title with empty string', () => { + const result = service.isTitleValid('', 'config'); + expect(result).toEqual(false); + }); + + it('checks invalid title with length > 36 character', () => { + const result = service.isTitleValid('OpenSearch Dashboardssssssssssssssssssssss', 'config'); + expect(result).toEqual(false); + }); + }); }); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 3feb5ada2ca9..efaab2b94947 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -32,9 +32,12 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; -import { take } from 'rxjs/operators'; +import { first, take } from 'rxjs/operators'; import { i18n } from '@osd/i18n'; +import Axios from 'axios'; +// @ts-expect-error untyped internal module used to prevent axios from using xhr adapter in tests +import AxiosHttpAdapter from 'axios/lib/adapters/http'; import { UiPlugins } from '../plugins'; import { CoreContext } from '../core_context'; import { Template } from './views'; @@ -43,17 +46,27 @@ import { RenderingSetupDeps, InternalRenderingServiceSetup, RenderingMetadata, + BrandingValidation, + BrandingAssignment, } from './types'; +import { OpenSearchDashboardsConfigType } from '../opensearch_dashboards_config'; + +const DEFAULT_TITLE = 'OpenSearch Dashboards'; /** @internal */ export class RenderingService { constructor(private readonly coreContext: CoreContext) {} - + private logger = this.coreContext.logger; public async setup({ http, status, uiPlugins, }: RenderingSetupDeps): Promise { + const opensearchDashboardsConfig = await this.coreContext.configService + .atPath('opensearchDashboards') + .pipe(first()) + .toPromise(); + return { render: async ( request, @@ -65,20 +78,27 @@ export class RenderingService { packageInfo: this.coreContext.env.packageInfo, }; const basePath = http.basePath.get(request); + const uiPublicUrl = `${basePath}/ui`; const serverBasePath = http.basePath.serverBasePath; const settings = { defaults: uiSettings.getRegistered(), user: includeUserSettings ? await uiSettings.getUserProvided() : {}, }; + const darkMode = settings.user?.['theme:darkMode']?.userValue + ? Boolean(settings.user['theme:darkMode'].userValue) + : false; + const brandingAssignment = await this.assignBrandingConfig( + darkMode, + opensearchDashboardsConfig + ); + const metadata: RenderingMetadata = { strictCsp: http.csp.strict, - uiPublicUrl: `${basePath}/ui`, + uiPublicUrl, bootstrapScriptUrl: `${basePath}/bootstrap.js`, i18n: i18n.translate, locale: i18n.getLocale(), - darkMode: settings.user?.['theme:darkMode']?.userValue - ? Boolean(settings.user['theme:darkMode'].userValue) - : false, + darkMode, injectedMetadata: { version: env.packageInfo.version, buildNumber: env.packageInfo.buildNum, @@ -102,6 +122,24 @@ export class RenderingService { legacyMetadata: { uiSettings: settings, }, + branding: { + darkMode, + assetFolderUrl: `${uiPublicUrl}/default_branding`, + logo: { + defaultUrl: brandingAssignment.logoDefault, + darkModeUrl: brandingAssignment.logoDarkmode, + }, + mark: { + defaultUrl: brandingAssignment.markDefault, + darkModeUrl: brandingAssignment.markDarkmode, + }, + loadingLogo: { + defaultUrl: brandingAssignment.loadingLogoDefault, + darkModeUrl: brandingAssignment.loadingLogoDarkmode, + }, + faviconUrl: brandingAssignment.favicon, + applicationTitle: brandingAssignment.applicationTitle, + }, }, }; @@ -112,9 +150,203 @@ export class RenderingService { public async stop() {} + /** + * Assign values for branding related configurations based on branding validation + * by calling checkBrandingValid(). For dark mode URLs, add additonal 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. + * + * @param {boolean} darkMode + * @param {Readonly} opensearchDashboardsConfig + * @returns {BrandingAssignment} valid URLs or undefined assigned for each branding configs + */ + private assignBrandingConfig = async ( + darkMode: boolean, + opensearchDashboardsConfig: Readonly + ): Promise => { + const brandingValidation: BrandingValidation = await this.checkBrandingValid( + darkMode, + opensearchDashboardsConfig + ); + const branding = opensearchDashboardsConfig.branding; + + // assign default mode URL based on the brandingValidation function result + const logoDefault = brandingValidation.isLogoDefaultValid + ? branding.logo.defaultUrl + : undefined; + + const markDefault = brandingValidation.isMarkDefaultValid + ? branding.mark.defaultUrl + : undefined; + + const loadingLogoDefault = brandingValidation.isLoadingLogoDefaultValid + ? branding.loadingLogo.defaultUrl + : undefined; + + // assign dark mode URLs based on brandingValidation function result + let logoDarkmode = brandingValidation.isLogoDarkmodeValid + ? branding.logo.darkModeUrl + : undefined; + + let markDarkmode = brandingValidation.isMarkDarkmodeValid + ? branding.mark.darkModeUrl + : undefined; + + let loadingLogoDarkmode = brandingValidation.isLoadingLogoDarkmodeValid + ? branding.loadingLogo.darkModeUrl + : undefined; + + /** + * For dark mode URLs, we added another validation: + * user can only provide a dark mode URL after providing a valid default mode URL, + * If user provides a valid dark mode URL but fails to provide a valid default mode URL, + * return undefined for the dark mode URL + */ + if (logoDarkmode && !logoDefault) { + this.logger + .get('branding') + .error('Must provide a valid logo default mode URL before providing a logo dark mode URL'); + logoDarkmode = undefined; + } + + if (markDarkmode && !markDefault) { + this.logger + .get('branding') + .error('Must provide a valid mark default mode URL before providing a mark dark mode URL'); + markDarkmode = undefined; + } + + if (loadingLogoDarkmode && !loadingLogoDefault) { + this.logger + .get('branding') + .error( + 'Must provide a valid loading logo default mode URL before providing a loading logo dark mode URL' + ); + loadingLogoDarkmode = undefined; + } + + // assign favicon based on brandingValidation function result + const favicon = brandingValidation.isFaviconValid ? branding.faviconUrl : undefined; + + // assign applition title based on brandingValidation function result + const applicationTitle = brandingValidation.isTitleValid + ? branding.applicationTitle + : DEFAULT_TITLE; + + const brandingAssignment: BrandingAssignment = { + logoDefault, + logoDarkmode, + markDefault, + markDarkmode, + loadingLogoDefault, + loadingLogoDarkmode, + favicon, + applicationTitle, + }; + + return brandingAssignment; + }; + + /** + * Assign boolean values for branding related configurations to indicate if + * user inputs valid or invalid URLs by calling isUrlValid() function. Also + * check if title is valid by calling isTitleValid() function. + * + * @param {boolean} darkMode + * @param {Readonly} opensearchDashboardsConfig + * @returns {BrandingValidation} indicate valid/invalid URL for each branding config + */ + private checkBrandingValid = async ( + darkMode: boolean, + opensearchDashboardsConfig: Readonly + ): Promise => { + const branding = opensearchDashboardsConfig.branding; + const isLogoDefaultValid = await this.isUrlValid(branding.logo.defaultUrl, 'logo default'); + + const isLogoDarkmodeValid = darkMode + ? await this.isUrlValid(branding.logo.darkModeUrl, 'logo darkMode') + : false; + + const isMarkDefaultValid = await this.isUrlValid(branding.mark.defaultUrl, 'mark default'); + + const isMarkDarkmodeValid = darkMode + ? await this.isUrlValid(branding.mark.darkModeUrl, 'mark darkMode') + : false; + + const isLoadingLogoDefaultValid = await this.isUrlValid( + branding.loadingLogo.defaultUrl, + 'loadingLogo default' + ); + + const isLoadingLogoDarkmodeValid = darkMode + ? await this.isUrlValid(branding.loadingLogo.darkModeUrl, 'loadingLogo darkMode') + : false; + + const isFaviconValid = await this.isUrlValid(branding.faviconUrl, 'favicon'); + + const isTitleValid = this.isTitleValid(branding.applicationTitle, 'applicationTitle'); + + const brandingValidation: BrandingValidation = { + isLogoDefaultValid, + isLogoDarkmodeValid, + isMarkDefaultValid, + isMarkDarkmodeValid, + isLoadingLogoDefaultValid, + isLoadingLogoDarkmodeValid, + isFaviconValid, + isTitleValid, + }; + + return brandingValidation; + }; + private async getUiConfig(uiPlugins: UiPlugins, pluginId: string) { const browserConfig = uiPlugins.browserConfigs.get(pluginId); return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record; } + + /** + * Validation function for URLs. Use Axios to call URL and check validity. + * Also needs to be ended with png, svg, gif, PNG, SVG and GIF. + * + * @param {string} url + * @param {string} configName + * @returns {boolean} indicate if the URL is valid/invalid + */ + public isUrlValid = async (url: string, configName?: string): Promise => { + if (url.match(/\.(png|svg|gif|PNG|SVG|GIF)$/) === null) { + this.logger.get('branding').info(configName + ' config is not found or invalid.'); + return false; + } + return await Axios.get(url, { adapter: AxiosHttpAdapter, maxRedirects: 0 }) + .then(() => { + return true; + }) + .catch(() => { + this.logger.get('branding').info(configName + ' config is not found or invalid'); + return false; + }); + }; + + /** + * Validation function for applicationTitle config. + * Title length needs to be between 1 to 36 letters. + * + * @param {string} title + * @param {string} configName + * @returns {boolean} indicate if user input title is valid/invalid + */ + public isTitleValid = (title: string, configName?: string): boolean => { + if (!title || title.length > 36) { + this.logger + .get('branding') + .info( + configName + + ' config is not found or invalid. Title length should be between 1 to 36 characters.' + ); + return false; + } + return true; + }; } diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 7bba46eeb938..9caff4fb9499 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -32,6 +32,7 @@ import { i18n } from '@osd/i18n'; +import { Branding } from 'src/core/types'; import { EnvironmentMode, PackageInfo } from '../config'; import { ICspConfig } from '../csp'; import { InternalHttpServiceSetup, OpenSearchDashboardsRequest, LegacyRequest } from '../http'; @@ -74,6 +75,7 @@ export interface RenderingMetadata { user: Record>; }; }; + branding: Branding; }; } @@ -117,3 +119,36 @@ export interface InternalRenderingServiceSetup { options?: IRenderOptions ): Promise; } + +/** + * For each branding config: + * check if user provides a valid URL. + * Assign True -- if user provides a valid URL + * Assign False -- if user provides an invalid URL or user does not provide any URL + */ +export interface BrandingValidation { + isLogoDefaultValid: boolean; + isLogoDarkmodeValid: boolean; + isMarkDefaultValid: boolean; + isMarkDarkmodeValid: boolean; + isLoadingLogoDefaultValid: boolean; + isLoadingLogoDarkmodeValid: boolean; + isFaviconValid: boolean; + isTitleValid: boolean; +} + +/** + * For each branding config: + * if user provides a valid URL, the URL will be assigned; + * otherwise, undefined will be assigned. + */ +export interface BrandingAssignment { + logoDefault?: string; + logoDarkmode?: string; + markDefault?: string; + markDarkmode?: string; + loadingLogoDefault?: string; + loadingLogoDarkmode?: string; + favicon?: string; + applicationTitle?: string; +} diff --git a/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap b/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap new file mode 100644 index 000000000000..12cea1e9b430 --- /dev/null +++ b/src/core/server/rendering/views/__snapshots__/template.test.tsx.snap @@ -0,0 +1,1480 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Loading page logo in dark mode rendered using loading logo dark mode URL 1`] = ` +Array [ + , + , + , + , + null, + <link + href="[object Object]/ui/favicons/apple-touch-icon.png" + rel="apple-touch-icon" + sizes="180x180" + />, + <link + href="[object Object]/ui/favicons/favicon-32x32.png" + rel="icon" + sizes="32x32" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/favicon-16x16.png" + rel="icon" + sizes="16x16" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/manifest.json" + rel="manifest" + />, + <link + color="#e8488b" + href="[object Object]/ui/favicons/safari-pinned-tab.svg" + rel="mask-icon" + />, + <link + href="[object Object]/ui/favicons/favicon.ico" + rel="shortcut icon" + />, + <meta + content="[object Object]/ui/favicons/browserconfig.xml" + name="msapplication-config" + />, + <meta + content="#ffffff" + name="theme-color" + />, + null, + <meta + name="add-styles-here" + />, + <meta + name="add-scripts-here" + />, + <osd-csp + data="{\\"strictCsp\\":true}" + />, + <osd-injected-metadata + data="{\\"version\\":\\"opensearchDashboardsVersion\\",\\"buildNumber\\":1,\\"basePath\\":\\"\\",\\"serverBasePath\\":\\"\\",\\"env\\":{\\"packageInfo\\":{\\"version\\":\\"\\",\\"branch\\":\\"\\",\\"buildNum\\":1,\\"buildSha\\":\\"\\",\\"dist\\":true},\\"mode\\":{\\"name\\":\\"production\\",\\"dev\\":true,\\"prod\\":false}},\\"anonymousStatusPage\\":false,\\"i18n\\":{\\"translationsUrl\\":\\"\\"},\\"csp\\":{\\"warnLegacyBrowsers\\":true},\\"uiPlugins\\":[],\\"legacyMetadata\\":{\\"uiSettings\\":{\\"defaults\\":{\\"legacyInjectedUiSettingDefaults\\":true},\\"user\\":{}}},\\"branding\\":{\\"darkMode\\":false,\\"logo\\":{},\\"mark\\":{\\"defaultUrl\\":\\"/defaultModeMark\\",\\"darkModeUrl\\":\\"/darkModeMark\\"},\\"loadingLogo\\":{\\"defaultUrl\\":\\"/defaultModeLoadingLogo\\",\\"darkModeUrl\\":\\"/darkModeLoadingLogo\\"},\\"title\\":\\"custom title\\"}}" + />, + <div + class="osdWelcomeView" + data-test-subj="osdLoadingMessage" + id="osd_loading_message" + style="display:none" + > + <div + class="osdLoaderWrap" + data-test-subj="loadingLogo" + > + <div + class="loadingLogoContainer" + > + <img + alt="undefined logo" + class="loadingLogo" + src="/darkModeLoadingLogo" + /> + </div> + <div + class="osdWelcomeText" + data-error-message="" + /> + </div> + </div>, + <div + class="osdWelcomeView" + id="osd_legacy_browser_error" + style="display:none" + > + <svg + fill="none" + height="64" + viewBox="0 0 64 64" + width="64" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M61.7374 23.5C60.4878 23.5 59.4748 24.513 59.4748 25.7626C59.4748 44.3813 44.3813 59.4748 25.7626 59.4748C24.513 59.4748 23.5 60.4878 23.5 61.7374C23.5 62.987 24.513 64 25.7626 64C46.8805 64 64 46.8805 64 25.7626C64 24.513 62.987 23.5 61.7374 23.5Z" + fill="#005EB8" + /> + <path + d="M48.0814 38C50.2572 34.4505 52.3615 29.7178 51.9475 23.0921C51.0899 9.36725 38.6589 -1.04463 26.9206 0.0837327C22.3253 0.525465 17.6068 4.2712 18.026 10.9805C18.2082 13.8961 19.6352 15.6169 21.9544 16.9399C24.1618 18.1992 26.9978 18.9969 30.2128 19.9011C34.0962 20.9934 38.6009 22.2203 42.063 24.7717C46.2125 27.8295 49.0491 31.3743 48.0814 38Z" + fill="#003B5C" + /> + <path + d="M3.91861 14C1.74276 17.5495 -0.361506 22.2822 0.0524931 28.9079C0.910072 42.6327 13.3411 53.0446 25.0794 51.9163C29.6747 51.4745 34.3932 47.7288 33.974 41.0195C33.7918 38.1039 32.3647 36.3831 30.0456 35.0601C27.8382 33.8008 25.0022 33.0031 21.7872 32.0989C17.9038 31.0066 13.3991 29.7797 9.93694 27.2283C5.78746 24.1704 2.95092 20.6257 3.91861 14Z" + fill="#005EB8" + /> + </svg> + <h2 + class="osdWelcomeTitle" + /> + <div + class="osdWelcomeText" + /> + </div>, + <script> + + // Since this is an unsafe inline script, this code will not run + // in browsers that support content security policy(CSP). This is + // intentional as we check for the existence of __osdCspNotEnforced__ in + // bootstrap. + window.__osdCspNotEnforced__ = true; + + </script>, + <script + src="[object Object]/bootstrap.js" + />, +] +`; + +exports[`Loading page logo in dark mode rendered using loading logo default mode URL 1`] = ` +Array [ + <meta + charset="utf-8" + />, + <meta + content="IE=edge,chrome=1" + http-equiv="X-UA-Compatible" + />, + <meta + content="width=device-width" + name="viewport" + />, + <title />, + null, + <link + href="[object Object]/ui/favicons/apple-touch-icon.png" + rel="apple-touch-icon" + sizes="180x180" + />, + <link + href="[object Object]/ui/favicons/favicon-32x32.png" + rel="icon" + sizes="32x32" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/favicon-16x16.png" + rel="icon" + sizes="16x16" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/manifest.json" + rel="manifest" + />, + <link + color="#e8488b" + href="[object Object]/ui/favicons/safari-pinned-tab.svg" + rel="mask-icon" + />, + <link + href="[object Object]/ui/favicons/favicon.ico" + rel="shortcut icon" + />, + <meta + content="[object Object]/ui/favicons/browserconfig.xml" + name="msapplication-config" + />, + <meta + content="#ffffff" + name="theme-color" + />, + null, + <meta + name="add-styles-here" + />, + <meta + name="add-scripts-here" + />, + <osd-csp + data="{\\"strictCsp\\":true}" + />, + <osd-injected-metadata + data="{\\"version\\":\\"opensearchDashboardsVersion\\",\\"buildNumber\\":1,\\"basePath\\":\\"\\",\\"serverBasePath\\":\\"\\",\\"env\\":{\\"packageInfo\\":{\\"version\\":\\"\\",\\"branch\\":\\"\\",\\"buildNum\\":1,\\"buildSha\\":\\"\\",\\"dist\\":true},\\"mode\\":{\\"name\\":\\"production\\",\\"dev\\":true,\\"prod\\":false}},\\"anonymousStatusPage\\":false,\\"i18n\\":{\\"translationsUrl\\":\\"\\"},\\"csp\\":{\\"warnLegacyBrowsers\\":true},\\"uiPlugins\\":[],\\"legacyMetadata\\":{\\"uiSettings\\":{\\"defaults\\":{\\"legacyInjectedUiSettingDefaults\\":true},\\"user\\":{}}},\\"branding\\":{\\"darkMode\\":false,\\"logo\\":{},\\"mark\\":{\\"defaultUrl\\":\\"/defaultModeMark\\",\\"darkModeUrl\\":\\"/darkModeMark\\"},\\"loadingLogo\\":{\\"defaultUrl\\":\\"/defaultModeLoadingLogo\\"},\\"title\\":\\"custom title\\"}}" + />, + <div + class="osdWelcomeView" + data-test-subj="osdLoadingMessage" + id="osd_loading_message" + style="display:none" + > + <div + class="osdLoaderWrap" + data-test-subj="loadingLogo" + > + <div + class="loadingLogoContainer" + > + <img + alt="undefined logo" + class="loadingLogo" + src="/defaultModeLoadingLogo" + /> + </div> + <div + class="osdWelcomeText" + data-error-message="" + /> + </div> + </div>, + <div + class="osdWelcomeView" + id="osd_legacy_browser_error" + style="display:none" + > + <svg + fill="none" + height="64" + viewBox="0 0 64 64" + width="64" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M61.7374 23.5C60.4878 23.5 59.4748 24.513 59.4748 25.7626C59.4748 44.3813 44.3813 59.4748 25.7626 59.4748C24.513 59.4748 23.5 60.4878 23.5 61.7374C23.5 62.987 24.513 64 25.7626 64C46.8805 64 64 46.8805 64 25.7626C64 24.513 62.987 23.5 61.7374 23.5Z" + fill="#005EB8" + /> + <path + d="M48.0814 38C50.2572 34.4505 52.3615 29.7178 51.9475 23.0921C51.0899 9.36725 38.6589 -1.04463 26.9206 0.0837327C22.3253 0.525465 17.6068 4.2712 18.026 10.9805C18.2082 13.8961 19.6352 15.6169 21.9544 16.9399C24.1618 18.1992 26.9978 18.9969 30.2128 19.9011C34.0962 20.9934 38.6009 22.2203 42.063 24.7717C46.2125 27.8295 49.0491 31.3743 48.0814 38Z" + fill="#003B5C" + /> + <path + d="M3.91861 14C1.74276 17.5495 -0.361506 22.2822 0.0524931 28.9079C0.910072 42.6327 13.3411 53.0446 25.0794 51.9163C29.6747 51.4745 34.3932 47.7288 33.974 41.0195C33.7918 38.1039 32.3647 36.3831 30.0456 35.0601C27.8382 33.8008 25.0022 33.0031 21.7872 32.0989C17.9038 31.0066 13.3991 29.7797 9.93694 27.2283C5.78746 24.1704 2.95092 20.6257 3.91861 14Z" + fill="#005EB8" + /> + </svg> + <h2 + class="osdWelcomeTitle" + /> + <div + class="osdWelcomeText" + /> + </div>, + <script> + + // Since this is an unsafe inline script, this code will not run + // in browsers that support content security policy(CSP). This is + // intentional as we check for the existence of __osdCspNotEnforced__ in + // bootstrap. + window.__osdCspNotEnforced__ = true; + + </script>, + <script + src="[object Object]/bootstrap.js" + />, +] +`; + +exports[`Loading page logo in dark mode rendered using mark dark mode URL with loading bar 1`] = ` +Array [ + <meta + charset="utf-8" + />, + <meta + content="IE=edge,chrome=1" + http-equiv="X-UA-Compatible" + />, + <meta + content="width=device-width" + name="viewport" + />, + <title />, + null, + <link + href="[object Object]/ui/favicons/apple-touch-icon.png" + rel="apple-touch-icon" + sizes="180x180" + />, + <link + href="[object Object]/ui/favicons/favicon-32x32.png" + rel="icon" + sizes="32x32" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/favicon-16x16.png" + rel="icon" + sizes="16x16" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/manifest.json" + rel="manifest" + />, + <link + color="#e8488b" + href="[object Object]/ui/favicons/safari-pinned-tab.svg" + rel="mask-icon" + />, + <link + href="[object Object]/ui/favicons/favicon.ico" + rel="shortcut icon" + />, + <meta + content="[object Object]/ui/favicons/browserconfig.xml" + name="msapplication-config" + />, + <meta + content="#ffffff" + name="theme-color" + />, + null, + <meta + name="add-styles-here" + />, + <meta + name="add-scripts-here" + />, + <osd-csp + data="{\\"strictCsp\\":true}" + />, + <osd-injected-metadata + data="{\\"version\\":\\"opensearchDashboardsVersion\\",\\"buildNumber\\":1,\\"basePath\\":\\"\\",\\"serverBasePath\\":\\"\\",\\"env\\":{\\"packageInfo\\":{\\"version\\":\\"\\",\\"branch\\":\\"\\",\\"buildNum\\":1,\\"buildSha\\":\\"\\",\\"dist\\":true},\\"mode\\":{\\"name\\":\\"production\\",\\"dev\\":true,\\"prod\\":false}},\\"anonymousStatusPage\\":false,\\"i18n\\":{\\"translationsUrl\\":\\"\\"},\\"csp\\":{\\"warnLegacyBrowsers\\":true},\\"uiPlugins\\":[],\\"legacyMetadata\\":{\\"uiSettings\\":{\\"defaults\\":{\\"legacyInjectedUiSettingDefaults\\":true},\\"user\\":{}}},\\"branding\\":{\\"darkMode\\":false,\\"logo\\":{},\\"mark\\":{\\"defaultUrl\\":\\"/defaultModeMark\\",\\"darkModeUrl\\":\\"/darkModeMark\\"},\\"loadingLogo\\":{},\\"title\\":\\"custom title\\"}}" + />, + <div + class="osdWelcomeView" + data-test-subj="osdLoadingMessage" + id="osd_loading_message" + style="display:none" + > + <div + class="osdLoaderWrap" + data-test-subj="loadingLogo" + > + <div + class="loadingLogoContainer" + > + <img + alt="undefined logo" + class="loadingLogo" + src="/darkModeMark" + /> + </div> + <div + class="osdWelcomeText" + data-error-message="" + /> + <div + class="osdProgress" + /> + </div> + </div>, + <div + class="osdWelcomeView" + id="osd_legacy_browser_error" + style="display:none" + > + <svg + fill="none" + height="64" + viewBox="0 0 64 64" + width="64" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M61.7374 23.5C60.4878 23.5 59.4748 24.513 59.4748 25.7626C59.4748 44.3813 44.3813 59.4748 25.7626 59.4748C24.513 59.4748 23.5 60.4878 23.5 61.7374C23.5 62.987 24.513 64 25.7626 64C46.8805 64 64 46.8805 64 25.7626C64 24.513 62.987 23.5 61.7374 23.5Z" + fill="#005EB8" + /> + <path + d="M48.0814 38C50.2572 34.4505 52.3615 29.7178 51.9475 23.0921C51.0899 9.36725 38.6589 -1.04463 26.9206 0.0837327C22.3253 0.525465 17.6068 4.2712 18.026 10.9805C18.2082 13.8961 19.6352 15.6169 21.9544 16.9399C24.1618 18.1992 26.9978 18.9969 30.2128 19.9011C34.0962 20.9934 38.6009 22.2203 42.063 24.7717C46.2125 27.8295 49.0491 31.3743 48.0814 38Z" + fill="#003B5C" + /> + <path + d="M3.91861 14C1.74276 17.5495 -0.361506 22.2822 0.0524931 28.9079C0.910072 42.6327 13.3411 53.0446 25.0794 51.9163C29.6747 51.4745 34.3932 47.7288 33.974 41.0195C33.7918 38.1039 32.3647 36.3831 30.0456 35.0601C27.8382 33.8008 25.0022 33.0031 21.7872 32.0989C17.9038 31.0066 13.3991 29.7797 9.93694 27.2283C5.78746 24.1704 2.95092 20.6257 3.91861 14Z" + fill="#005EB8" + /> + </svg> + <h2 + class="osdWelcomeTitle" + /> + <div + class="osdWelcomeText" + /> + </div>, + <script> + + // Since this is an unsafe inline script, this code will not run + // in browsers that support content security policy(CSP). This is + // intentional as we check for the existence of __osdCspNotEnforced__ in + // bootstrap. + window.__osdCspNotEnforced__ = true; + + </script>, + <script + src="[object Object]/bootstrap.js" + />, +] +`; + +exports[`Loading page logo in dark mode rendered using mark default mode URL with loading bar 1`] = ` +Array [ + <meta + charset="utf-8" + />, + <meta + content="IE=edge,chrome=1" + http-equiv="X-UA-Compatible" + />, + <meta + content="width=device-width" + name="viewport" + />, + <title />, + null, + <link + href="[object Object]/ui/favicons/apple-touch-icon.png" + rel="apple-touch-icon" + sizes="180x180" + />, + <link + href="[object Object]/ui/favicons/favicon-32x32.png" + rel="icon" + sizes="32x32" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/favicon-16x16.png" + rel="icon" + sizes="16x16" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/manifest.json" + rel="manifest" + />, + <link + color="#e8488b" + href="[object Object]/ui/favicons/safari-pinned-tab.svg" + rel="mask-icon" + />, + <link + href="[object Object]/ui/favicons/favicon.ico" + rel="shortcut icon" + />, + <meta + content="[object Object]/ui/favicons/browserconfig.xml" + name="msapplication-config" + />, + <meta + content="#ffffff" + name="theme-color" + />, + null, + <meta + name="add-styles-here" + />, + <meta + name="add-scripts-here" + />, + <osd-csp + data="{\\"strictCsp\\":true}" + />, + <osd-injected-metadata + data="{\\"version\\":\\"opensearchDashboardsVersion\\",\\"buildNumber\\":1,\\"basePath\\":\\"\\",\\"serverBasePath\\":\\"\\",\\"env\\":{\\"packageInfo\\":{\\"version\\":\\"\\",\\"branch\\":\\"\\",\\"buildNum\\":1,\\"buildSha\\":\\"\\",\\"dist\\":true},\\"mode\\":{\\"name\\":\\"production\\",\\"dev\\":true,\\"prod\\":false}},\\"anonymousStatusPage\\":false,\\"i18n\\":{\\"translationsUrl\\":\\"\\"},\\"csp\\":{\\"warnLegacyBrowsers\\":true},\\"uiPlugins\\":[],\\"legacyMetadata\\":{\\"uiSettings\\":{\\"defaults\\":{\\"legacyInjectedUiSettingDefaults\\":true},\\"user\\":{}}},\\"branding\\":{\\"darkMode\\":false,\\"logo\\":{},\\"mark\\":{\\"defaultUrl\\":\\"/defaultModeMark\\"},\\"loadingLogo\\":{},\\"title\\":\\"custom title\\"}}" + />, + <div + class="osdWelcomeView" + data-test-subj="osdLoadingMessage" + id="osd_loading_message" + style="display:none" + > + <div + class="osdLoaderWrap" + data-test-subj="loadingLogo" + > + <div + class="loadingLogoContainer" + > + <img + alt="undefined logo" + class="loadingLogo" + src="/defaultModeMark" + /> + </div> + <div + class="osdWelcomeText" + data-error-message="" + /> + <div + class="osdProgress" + /> + </div> + </div>, + <div + class="osdWelcomeView" + id="osd_legacy_browser_error" + style="display:none" + > + <svg + fill="none" + height="64" + viewBox="0 0 64 64" + width="64" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M61.7374 23.5C60.4878 23.5 59.4748 24.513 59.4748 25.7626C59.4748 44.3813 44.3813 59.4748 25.7626 59.4748C24.513 59.4748 23.5 60.4878 23.5 61.7374C23.5 62.987 24.513 64 25.7626 64C46.8805 64 64 46.8805 64 25.7626C64 24.513 62.987 23.5 61.7374 23.5Z" + fill="#005EB8" + /> + <path + d="M48.0814 38C50.2572 34.4505 52.3615 29.7178 51.9475 23.0921C51.0899 9.36725 38.6589 -1.04463 26.9206 0.0837327C22.3253 0.525465 17.6068 4.2712 18.026 10.9805C18.2082 13.8961 19.6352 15.6169 21.9544 16.9399C24.1618 18.1992 26.9978 18.9969 30.2128 19.9011C34.0962 20.9934 38.6009 22.2203 42.063 24.7717C46.2125 27.8295 49.0491 31.3743 48.0814 38Z" + fill="#003B5C" + /> + <path + d="M3.91861 14C1.74276 17.5495 -0.361506 22.2822 0.0524931 28.9079C0.910072 42.6327 13.3411 53.0446 25.0794 51.9163C29.6747 51.4745 34.3932 47.7288 33.974 41.0195C33.7918 38.1039 32.3647 36.3831 30.0456 35.0601C27.8382 33.8008 25.0022 33.0031 21.7872 32.0989C17.9038 31.0066 13.3991 29.7797 9.93694 27.2283C5.78746 24.1704 2.95092 20.6257 3.91861 14Z" + fill="#005EB8" + /> + </svg> + <h2 + class="osdWelcomeTitle" + /> + <div + class="osdWelcomeText" + /> + </div>, + <script> + + // Since this is an unsafe inline script, this code will not run + // in browsers that support content security policy(CSP). This is + // intentional as we check for the existence of __osdCspNotEnforced__ in + // bootstrap. + window.__osdCspNotEnforced__ = true; + + </script>, + <script + src="[object Object]/bootstrap.js" + />, +] +`; + +exports[`Loading page logo in dark mode renders using original opensearch loading spinner 1`] = ` +Array [ + <meta + charset="utf-8" + />, + <meta + content="IE=edge,chrome=1" + http-equiv="X-UA-Compatible" + />, + <meta + content="width=device-width" + name="viewport" + />, + <title />, + null, + <link + href="[object Object]/ui/favicons/apple-touch-icon.png" + rel="apple-touch-icon" + sizes="180x180" + />, + <link + href="[object Object]/ui/favicons/favicon-32x32.png" + rel="icon" + sizes="32x32" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/favicon-16x16.png" + rel="icon" + sizes="16x16" + type="image/png" + />, + <link + href="[object Object]/ui/favicons/manifest.json" + rel="manifest" + />, + <link + color="#e8488b" + href="[object Object]/ui/favicons/safari-pinned-tab.svg" + rel="mask-icon" + />, + <link + href="[object Object]/ui/favicons/favicon.ico" + rel="shortcut icon" + />, + <meta + content="[object Object]/ui/favicons/browserconfig.xml" + name="msapplication-config" + />, + <meta + content="#ffffff" + name="theme-color" + />, + null, + <meta + name="add-styles-here" + />, + <meta + name="add-scripts-here" + />, + <osd-csp + data="{\\"strictCsp\\":true}" + />, + <osd-injected-metadata + data="{\\"version\\":\\"opensearchDashboardsVersion\\",\\"buildNumber\\":1,\\"basePath\\":\\"\\",\\"serverBasePath\\":\\"\\",\\"env\\":{\\"packageInfo\\":{\\"version\\":\\"\\",\\"branch\\":\\"\\",\\"buildNum\\":1,\\"buildSha\\":\\"\\",\\"dist\\":true},\\"mode\\":{\\"name\\":\\"production\\",\\"dev\\":true,\\"prod\\":false}},\\"anonymousStatusPage\\":false,\\"i18n\\":{\\"translationsUrl\\":\\"\\"},\\"csp\\":{\\"warnLegacyBrowsers\\":true},\\"uiPlugins\\":[],\\"legacyMetadata\\":{\\"uiSettings\\":{\\"defaults\\":{\\"legacyInjectedUiSettingDefaults\\":true},\\"user\\":{}}},\\"branding\\":{\\"darkMode\\":false,\\"logo\\":{},\\"mark\\":{},\\"loadingLogo\\":{},\\"title\\":\\"custom title\\"}}" + />, + <div + class="osdWelcomeView" + data-test-subj="osdLoadingMessage" + id="osd_loading_message" + style="display:none" + > + <div + class="osdLoaderWrap" + data-test-subj="loadingLogo" + > + <svg + fill="none" + viewBox="0 0 90 90" + xmlns="http://www.w3.org/2000/svg" + > + <g> + <path + d="M75.7374 37.5C74.4878 37.5 73.4748 38.513 73.4748 39.7626C73.4748 58.3813 58.3813 73.4748 39.7626 73.4748C38.513 73.4748 37.5 74.4878 37.5 75.7374C37.5 76.987 38.513 78 39.7626 78C60.8805 78 78 60.8805 78 39.7626C78 38.513 76.987 37.5 75.7374 37.5Z" + fill="#005EB8" + /> + <animateTransform + attributeName="transform" + dur="1.5s" + from="0 40 40" + keyTimes="0; .3; .7; 1" + repeatCount="indefinite" + to="359.9 40 40" + type="rotate" + values="0 40 40; 15 40 40; 340 40 40; 359.9 40 40" + /> + </g> + <path + d="M62.0814 52C64.2572 48.4505 66.3615 43.7178 65.9475 37.0921C65.0899 23.3673 52.6589 12.9554 40.9206 14.0837C36.3253 14.5255 31.6068 18.2712 32.026 24.9805C32.2082 27.8961 33.6352 29.6169 35.9544 30.9399C38.1618 32.1992 40.9978 32.9969 44.2128 33.9011C48.0962 34.9934 52.6009 36.2203 56.0631 38.7717C60.2125 41.8296 63.0491 45.3743 62.0814 52Z" + fill="#003B5C" + /> + <path + d="M17.9186 28C15.7428 31.5495 13.6385 36.2822 14.0525 42.9079C14.9101 56.6327 27.3411 67.0446 39.0794 65.9163C43.6747 65.4745 48.3932 61.7288 47.974 55.0195C47.7918 52.1039 46.3647 50.3831 44.0456 49.0601C41.8382 47.8008 39.0022 47.0031 35.7872 46.0989C31.9038 45.0066 27.3991 43.7797 23.9369 41.2283C19.7875 38.1704 16.9509 34.6257 17.9186 28Z" + fill="#005EB8" + /> + </svg> + <div + class="osdWelcomeText" + data-error-message="" + /> + </div> + </div>, + <div + class="osdWelcomeView" + id="osd_legacy_browser_error" + style="display:none" + > + <svg + fill="none" + height="64" + viewBox="0 0 64 64" + width="64" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M61.7374 23.5C60.4878 23.5 59.4748 24.513 59.4748 25.7626C59.4748 44.3813 44.3813 59.4748 25.7626 59.4748C24.513 59.4748 23.5 60.4878 23.5 61.7374C23.5 62.987 24.513 64 25.7626 64C46.8805 64 64 46.8805 64 25.7626C64 24.513 62.987 23.5 61.7374 23.5Z" + fill="#005EB8" + /> + <path + d="M48.0814 38C50.2572 34.4505 52.3615 29.7178 51.9475 23.0921C51.0899 9.36725 38.6589 -1.04463 26.9206 0.0837327C22.3253 0.525465 17.6068 4.2712 18.026 10.9805C18.2082 13.8961 19.6352 15.6169 21.9544 16.9399C24.1618 18.1992 26.9978 18.9969 30.2128 19.9011C34.0962 20.9934 38.6009 22.2203 42.063 24.7717C46.2125 27.8295 49.0491 31.3743 48.0814 38Z" + fill="#003B5C" + /> + <path + d="M3.91861 14C1.74276 17.5495 -0.361506 22.2822 0.0524931 28.9079C0.910072 42.6327 13.3411 53.0446 25.0794 51.9163C29.6747 51.4745 34.3932 47.7288 33.974 41.0195C33.7918 38.1039 32.3647 36.3831 30.0456 35.0601C27.8382 33.8008 25.0022 33.0031 21.7872 32.0989C17.9038 31.0066 13.3991 29.7797 9.93694 27.2283C5.78746 24.1704 2.95092 20.6257 3.91861 14Z" + fill="#005EB8" + /> + </svg> + <h2 + class="osdWelcomeTitle" + /> + <div + class="osdWelcomeText" + /> + </div>, + <script> + + // Since this is an unsafe inline script, this code will not run + // in browsers that support content security policy(CSP). This is + // intentional as we check for the existence of __osdCspNotEnforced__ in + // bootstrap. + window.__osdCspNotEnforced__ = true; + + </script>, + <script + src="[object Object]/bootstrap.js" + />, +] +`; + +exports[`Loading page logo in default mode rendered using loading logo default mode URL 1`] = ` +Array [ + <meta + charset="utf-8" + />, + <meta + content="IE=edge,chrome=1" + http-equiv="X-UA-Compatible" + />, + <meta + content="width=device-width" + name="viewport" + />, + <title> + custom title + , + null, + , + , + , + , + , + , + , + , + null, + , + , + , + , +