From 1889334cca5c6fe198adea078145a5d2f2d1b200 Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Sun, 26 Sep 2021 20:44:51 -0700 Subject: [PATCH] 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: Abby Hu --- config/opensearch_dashboards.yml | 2 +- src/core/public/chrome/chrome_service.tsx | 4 + .../opensearch_dashboards_custom_logo.tsx | 31 +-- src/core/public/chrome/ui/header/header.tsx | 15 +- .../public/chrome/ui/header/header_logo.tsx | 5 +- src/core/public/index.ts | 19 +- .../injected_metadata_service.ts | 38 +-- src/core/server/rendering/types.ts | 19 +- src/core/server/rendering/views/template.tsx | 10 +- src/core/server/types.ts | 1 + src/core/types/custom_branding.ts | 60 +++++ src/core/types/index.ts | 1 + .../__snapshots__/home.test.js.snap | 1 + .../components/_solutions_section.scss | 10 + .../public/application/components/home.js | 1 + .../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 | 119 ++++++++- .../solutions_section.test.tsx | 13 + .../solutions_section/solutions_section.tsx | 12 +- .../opensearch_dashboards_services.ts | 9 +- src/plugins/home/public/index.ts | 1 + src/plugins/home/public/plugin.ts | 4 + .../apps/visualize/_custom_branding.js | 32 +++ 28 files changed, 727 insertions(+), 146 deletions(-) create mode 100644 src/core/types/custom_branding.ts diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 23f84ae71535..8fc3f50f5ad6 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -162,4 +162,4 @@ # defaultUrl: "" # darkModeUrl: "" # faviconUrl: "" - # applicationTitle: "" + # 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 68e4f8318598..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 { /** 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 index 5ef77548f86b..ad639aec1673 100644 --- 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 @@ -33,40 +33,23 @@ import React from 'react'; import '../header_logo.scss'; import { OpenSearchDashboardsLogoDarkMode } from './opensearch_dashboards_logo_darkmode'; - -/** - * @param {object} logo - full logo on main screen: defaultUrl will be used in default mode; darkModeUrl will be used in dark mode - * @param {object} mark - thumbnail logo: defaultUrl will be used in default mode; darkModeUrl will be used in dark mode - * @param {string} applicationTitle - custom title for the application - */ -export interface CustomLogoType { - darkMode: boolean; - logo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - applicationTitle?: string; -} +import { ChromeBranding } from '../../../chrome_service'; /** * Use branding configurations to render the header logo on the nab bar. * - * @param {CustomLogoType} - branding object consist of logo, mark and title + * @param {ChromeBranding} - branding object consist of logo, mark and title * @returns A image 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 logo will be rendered. */ -export const CustomLogo = ({ ...branding }: CustomLogoType) => { +export const CustomLogo = ({ ...branding }: ChromeBranding) => { const darkMode = branding.darkMode; - const logoDefault = branding.logo.defaultUrl; - const logoDarkMode = branding.logo.darkModeUrl; - const markDefault = branding.mark.defaultUrl; - const markDarkMode = branding.mark.darkModeUrl; + const logoDefault = branding.logo?.defaultUrl; + const logoDarkMode = branding.logo?.darkModeUrl; + const markDefault = branding.mark?.defaultUrl; + const markDarkMode = branding.mark?.darkModeUrl; const applicationTitle = branding.applicationTitle; /** diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 50e0a3a1e6b6..7d09aa46cdbc 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,18 +87,7 @@ export interface HeaderProps { isLocked$: Observable; loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; - branding: { - darkMode: boolean; - logo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - applicationTitle?: string; - }; + branding: ChromeBranding; } export function Header({ diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index b042da3c0d69..39e5f4ecfdbc 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -36,7 +36,8 @@ import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; -import { CustomLogo, CustomLogoType } from './branding/opensearch_dashboards_custom_logo'; +import { CustomLogo } from './branding/opensearch_dashboards_custom_logo'; +import { ChromeBranding } from '../../chrome_service'; import './header_logo.scss'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { @@ -104,7 +105,7 @@ interface Props { navLinks$: Observable; forceNavigation$: Observable; navigateToApp: (appId: string) => void; - branding: CustomLogoType; + branding: ChromeBranding; } export function HeaderLogo({ href, navigateToApp, branding, ...observables }: Props) { diff --git a/src/core/public/index.ts b/src/core/public/index.ts index db61347a2181..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,13 +237,7 @@ export interface CoreSetup unknown; - getBranding: () => { - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - title?: string; - }; + getBranding: () => Branding; }; /** {@link StartServicesAccessor} */ getStartServices: StartServicesAccessor; @@ -298,14 +293,7 @@ export interface CoreStart { * */ injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; - getBranding: () => { - darkMode: boolean; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - applicationTitle?: string; - }; + getBranding: () => Branding; }; } @@ -352,6 +340,7 @@ export { IUiSettingsClient, UiSettingsState, NavType, + Branding, }; export { __osdBootstrap__ } from './osd_bootstrap'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index ae1895a0d632..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,23 +76,7 @@ export interface InjectedMetadataParams { user?: Record; }; }; - branding: { - darkMode: boolean; - logo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - loadingLogo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - faviconUrl?: string; - applicationTitle?: string; - }; + branding: Branding; }; } @@ -197,23 +181,7 @@ export interface InjectedMetadataSetup { getInjectedVars: () => { [key: string]: unknown; }; - getBranding: () => { - darkMode: boolean; - logo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - loadingLogo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - faviconUrl?: string; - applicationTitle?: string; - }; + getBranding: () => Branding; } /** @internal */ diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index d0442e22d05c..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,23 +75,7 @@ export interface RenderingMetadata { user: Record>; }; }; - branding: { - darkMode: boolean; - logo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - loadingLogo: { - defaultUrl?: string; - darkModeUrl?: string; - }; - faviconUrl?: string; - applicationTitle?: string; - }; + branding: Branding; }; } diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index 5c9f80f92b83..1daff8bc6cea 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -96,10 +96,10 @@ export const Template: FunctionComponent = ({ ); - const loadingLogoDefault = injectedMetadata.branding.loadingLogo.defaultUrl; - const loadingLogoDarkMode = injectedMetadata.branding.loadingLogo.darkModeUrl; - const markDefault = injectedMetadata.branding.mark.defaultUrl; - const markDarkMode = injectedMetadata.branding.mark.darkModeUrl; + const loadingLogoDefault = injectedMetadata.branding.loadingLogo?.defaultUrl; + const loadingLogoDarkMode = injectedMetadata.branding.loadingLogo?.darkModeUrl; + const markDefault = injectedMetadata.branding.mark?.defaultUrl; + const markDarkMode = injectedMetadata.branding.mark?.darkModeUrl; const favicon = injectedMetadata.branding.faviconUrl; const applicationTitle = injectedMetadata.branding.applicationTitle; @@ -149,7 +149,7 @@ export const Template: FunctionComponent = ({ * @returns a loading bar component or no loading bar component */ const renderBrandingEnabledOrDisabledLoadingBar = () => { - if (customLoadingLogo() && !injectedMetadata.branding.loadingLogo.defaultUrl) { + if (customLoadingLogo() && !loadingLogoDefault) { return
; } }; diff --git a/src/core/server/types.ts b/src/core/server/types.ts index abe4bca3b4e6..60fcd5e507d4 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -36,3 +36,4 @@ export * from './saved_objects/types'; export * from './ui_settings/types'; export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@osd/config'; +export { Branding } from '../../core/types'; diff --git a/src/core/types/custom_branding.ts b/src/core/types/custom_branding.ts new file mode 100644 index 000000000000..6ae445abfd3e --- /dev/null +++ b/src/core/types/custom_branding.ts @@ -0,0 +1,60 @@ +/* + * 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. + */ + +/** + * A type definition for custom branding configurations from yml file + * @public + */ + +export interface Branding { + /** Default mode or Dark mode*/ + darkMode?: boolean; + /** Small logo icon that will be used in most logo occurrences */ + mark?: { + defaultUrl?: string; + darkModeUrl?: string; + }; + /** Fuller logo that will be rendered on nav bar header */ + logo?: { + defaultUrl?: string; + darkModeUrl?: string; + }; + /** Loading logo that will be rendered on the loading page */ + loadingLogo?: { + defaultUrl?: string; + darkModeUrl?: string; + }; + /** Custom favicon that will be rendered on the browser tab */ + faviconUrl?: string; + /** Application title that will replace the default opensearch dashboard string */ + applicationTitle?: string; +} diff --git a/src/core/types/index.ts b/src/core/types/index.ts index cc1840f0f071..9cafdb5d686e 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -40,3 +40,4 @@ export * from './app_category'; export * from './ui_settings'; export * from './saved_objects'; export * from './serializable'; +export * from './custom_branding'; diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap index 2b522815e339..da1f2d4a03e5 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap @@ -248,6 +248,7 @@ exports[`home directories should render solutions in the "solution section" 1`] > ) : null} diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap index 3b09c41d89f1..ef96583b37e4 100644 --- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap +++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap @@ -24,6 +24,16 @@ exports[`SolutionPanel renders the solution panel for the given solution 1`] = ` grow={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`] = ` + + + + +

+ custom title +

+
+ +

+ Visualize & analyze + + +

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

- OpenSearch Dashboards + custom title

(path ? path : 'path'); +const branding = { + darkMode: false, + mark: { + defaultUrl: '/defaultModeLogo', + darkModeUrl: '/darkModeLogo', + }, + applicationTitle: 'custom title', +}; + describe('SolutionPanel', () => { test('renders the solution panel for the given solution', () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx b/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx index 99ccce937390..fd5a99932e44 100644 --- a/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx @@ -35,6 +35,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elasti import { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../../'; import { createAppNavigationHandler } from '../app_navigation_handler'; import { SolutionTitle } from './solution_title'; +import { HomePluginBranding } from '../../../plugin'; const getDescriptionText = (description: string): JSX.Element => ( @@ -64,9 +65,10 @@ interface Props { addBasePath: (path: string) => string; solution: FeatureCatalogueSolution; apps?: FeatureCatalogueEntry[]; + branding: HomePluginBranding; } -export const SolutionPanel: FC = ({ addBasePath, solution, apps = [] }) => ( +export const SolutionPanel: FC = ({ addBasePath, solution, apps = [], branding }) => ( = ({ addBasePath, solution, apps = [] }) = iconType={solution.icon} title={solution.title} subtitle={solution.subtitle} + branding={branding} /> 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 a0ba5f48bfc0..b865589f448c 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 @@ -44,15 +44,121 @@ const solutionEntry = { order: 1, }; -describe('SolutionTitle', () => { - test('renders the title section of the solution panel', () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); +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(); + }); + + 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(); + }); + + 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(); + }); + }); + + 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(); + }); + + 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(); + }); + + 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(); + }); }); }); 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 503d8caf2c80..4dd5b863adf8 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 @@ -40,26 +40,125 @@ import { EuiIcon, IconType, } from '@elastic/eui'; +import { HomePluginBranding } from '../../../plugin'; interface Props { + /** + * @deprecated + * Title will be deprecated because we will use title config from branding + */ title: string; subtitle: string; + /** + * @deprecated + * IconType will be deprecated because we will make rendering custom dashboard logo logic consistent with other logos' logic + */ iconType: IconType; + branding: HomePluginBranding; } -export const SolutionTitle: FC = ({ title, subtitle, iconType }) => ( +const DEFAULT_OPENSEARCH_MARK = + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg'; +const DARKMODE_OPENSEARCH_MARK = + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg'; + +/** + * 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 + */ +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) => { + if (branding.darkMode) { + return customHomeLogoDarkMode(branding); + } + return 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 +
+ ); + } + 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 }) => ( - + {renderBrandingEnabledOrDisabledLogo(branding)} - -

{title}

+ +

{branding.applicationTitle}

diff --git a/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx b/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx index 2ecbb7a3ca41..79d78d862216 100644 --- a/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx @@ -107,6 +107,15 @@ const mockDirectories = [ const addBasePathMock = (path: string) => (path ? path : 'path'); +const branding = { + darkMode: false, + mark: { + defaultUrl: '/defaultModeLogo', + darkModeUrl: '/darkModeLogo', + }, + applicationTitle: 'custom title', +}; + describe('SolutionsSection', () => { test('only renders a spacer if no solutions are available', () => { const component = shallow( @@ -114,6 +123,7 @@ describe('SolutionsSection', () => { addBasePath={addBasePathMock} solutions={[]} directories={mockDirectories} + branding={branding} /> ); expect(component).toMatchSnapshot(); @@ -125,6 +135,7 @@ describe('SolutionsSection', () => { addBasePath={addBasePathMock} solutions={[solutionEntry1]} directories={mockDirectories} + branding={branding} /> ); expect(component).toMatchSnapshot(); @@ -136,6 +147,7 @@ describe('SolutionsSection', () => { addBasePath={addBasePathMock} solutions={[solutionEntry1, solutionEntry2, solutionEntry3, solutionEntry4]} directories={mockDirectories} + branding={branding} /> ); expect(component).toMatchSnapshot(); @@ -146,6 +158,7 @@ describe('SolutionsSection', () => { addBasePath={addBasePathMock} solutions={[solutionEntry2, solutionEntry3, solutionEntry4]} directories={mockDirectories} + branding={branding} /> ); expect(component).toMatchSnapshot(); diff --git a/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx b/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx index 1e6834fa23f4..d747f5feffc7 100644 --- a/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx @@ -35,6 +35,7 @@ import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { SolutionPanel } from './solution_panel'; +import { HomePluginBranding } from '../../../plugin'; import { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../../'; const sortByOrder = ( @@ -46,9 +47,10 @@ interface Props { addBasePath: (path: string) => string; solutions: FeatureCatalogueSolution[]; directories: FeatureCatalogueEntry[]; + branding: HomePluginBranding; } -export const SolutionsSection: FC = ({ addBasePath, solutions, directories }) => { +export const SolutionsSection: FC = ({ addBasePath, solutions, directories, branding }) => { // Separate OpenSearch Dashboards from other solutions const opensearchDashboards = solutions.find(({ id }) => id === 'opensearchDashboards'); const opensearchDashboardsApps = directories @@ -73,7 +75,12 @@ export const SolutionsSection: FC = ({ addBasePath, solutions, directorie {solutions.map((solution) => ( - + ))} @@ -83,6 +90,7 @@ export const SolutionsSection: FC = ({ addBasePath, solutions, directorie solution={opensearchDashboards} addBasePath={addBasePath} apps={opensearchDashboardsApps.length ? opensearchDashboardsApps : undefined} + branding={branding} /> ) : null}
diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts index fdd09cbe649b..ba5df59466ea 100644 --- a/src/plugins/home/public/application/opensearch_dashboards_services.ts +++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts @@ -47,6 +47,7 @@ import { TutorialService } from '../services/tutorials'; import { FeatureCatalogueRegistry } from '../services/feature_catalogue'; import { EnvironmentService } from '../services/environment'; import { ConfigSchema } from '../../config'; +import { HomePluginBranding } from '..'; export interface HomeOpenSearchDashboardsServices { indexPatternService: any; @@ -70,13 +71,7 @@ export interface HomeOpenSearchDashboardsServices { tutorialService: TutorialService; injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; - getBranding: () => { - mark: { - defaultUrl?: string; - darkModeUrl?: string; - }; - applicationTitle?: string; - }; + getBranding: () => HomePluginBranding; }; } diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index f2f54f9300e7..5c268914e1b5 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -38,6 +38,7 @@ export { TutorialSetup, HomePublicPluginSetup, HomePublicPluginStart, + HomePluginBranding, } from './plugin'; export { FeatureCatalogueEntry, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index c0ef1c2b56ff..9c9d021190d0 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -40,6 +40,7 @@ import { import { i18n } from '@osd/i18n'; import { first } from 'rxjs/operators'; +import { Branding } from 'src/core/types'; import { EnvironmentService, EnvironmentServiceSetup, @@ -188,6 +189,9 @@ export type EnvironmentSetup = EnvironmentServiceSetup; /** @public */ export type TutorialSetup = TutorialServiceSetup; +/** @public */ +export type HomePluginBranding = Branding; + /** @public */ export interface HomePublicPluginSetup { tutorials: TutorialServiceSetup; diff --git a/test/functional/apps/visualize/_custom_branding.js b/test/functional/apps/visualize/_custom_branding.js index 0edf116d37b5..e94b081731b5 100644 --- a/test/functional/apps/visualize/_custom_branding.js +++ b/test/functional/apps/visualize/_custom_branding.js @@ -106,6 +106,11 @@ export default function ({ getService, getPageObjects }) { 'https://opensearch.org/assets/brand/SVG/Logo/opensearch_logo_default.svg'; const expectedHeaderLogoDarkMode = 'https://opensearch.org/assets/brand/SVG/Logo/opensearch_logo_darkmode.svg'; + const expectedMarkLogo = + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg'; + const expectedMarkLogoDarkMode = + 'https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg'; + const applicationTitle = 'OpenSearch'; before(async function () { await PageObjects.common.navigateToApp('home'); @@ -123,6 +128,24 @@ export default function ({ getService, getPageObjects }) { expect(url.includes('/app/home')).to.be(true); }); + it('with customized logo in home dashboard card', async () => { + await testSubjects.existOrFail('dashboardCustomLogo'); + const actualLabel = await testSubjects.getAttribute( + 'dashboardCustomLogo', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogo.toUpperCase()); + }); + + it('with customized title in home dashboard card', async () => { + await testSubjects.existOrFail('dashboardCustomTitle'); + const actualLabel = await testSubjects.getAttribute( + 'dashboardCustomTitle', + 'data-test-title' + ); + expect(actualLabel.toUpperCase()).to.equal(applicationTitle.toUpperCase()); + }); + it('with customized logo in header bar in dark mode', async () => { await PageObjects.common.navigateToApp('management/opensearch-dashboards/settings'); await PageObjects.settings.toggleAdvancedSettingCheckbox('theme:darkMode'); @@ -137,6 +160,15 @@ export default function ({ getService, getPageObjects }) { const url = await browser.getCurrentUrl(); expect(url.includes('/app/home')).to.be(true); }); + + it('with customized logo in home dashboard card in dark mode', async () => { + await testSubjects.existOrFail('dashboardCustomLogo'); + const actualLabel = await testSubjects.getAttribute( + 'dashboardCustomLogo', + 'data-test-image-url' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedMarkLogoDarkMode.toUpperCase()); + }); }); }); }