From 990502e057a5b0fdb90bd4443a0589402990437b Mon Sep 17 00:00:00 2001 From: Abby Hu Date: Mon, 20 Sep 2021 08:15:58 +0000 Subject: [PATCH] Make home page primary dashboard card logo and title configurable 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 | 14 +- .../public/chrome/ui/header/header_logo.tsx | 5 +- src/core/public/index.ts | 19 +- .../injected_metadata_service.ts | 37 +-- 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 | 54 ++++ 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 | 5 +- .../apps/visualize/_custom_branding.js | 32 +++ 28 files changed, 721 insertions(+), 145 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..edf88f7d6ae5 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 HeaderLogoBranding = 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..2784f965bf07 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 { HeaderLogoBranding } 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 {HeaderLogoBranding} - 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 }: HeaderLogoBranding) => { 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..e46c249a1e95 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -64,6 +64,7 @@ import { HeaderHelpMenu } from './header_help_menu'; import { HeaderLogo } from './header_logo'; import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; +import { HeaderLogoBranding } from '../../chrome_service'; export interface HeaderProps { opensearchDashboardsVersion: string; @@ -87,18 +88,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: HeaderLogoBranding; } 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..3f325f348161 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 { HeaderLogoBranding } 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: HeaderLogoBranding; } 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..5a0ed7791069 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -40,6 +40,7 @@ import { UserProvidedValues, } from '../../server/types'; import { AppCategory } from '../'; +import { Branding } from '../'; export interface InjectedPluginMetadata { id: PluginName; @@ -76,23 +77,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 +182,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..e1a52a659854 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() && !injectedMetadata.branding.loadingLogo?.defaultUrl) { 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..9214a7e33bb7 --- /dev/null +++ b/src/core/types/custom_branding.ts @@ -0,0 +1,54 @@ +/* + * 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 + * @public + */ + +export interface Branding { + darkMode?: boolean; + mark?: { + defaultUrl?: string; + darkModeUrl?: string; + }; + logo?: { + defaultUrl?: string; + darkModeUrl?: string; + }; + loadingLogo?: { + defaultUrl?: string; + darkModeUrl?: string; + }; + faviconUrl?: 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..696e62de3583 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, @@ -57,7 +58,6 @@ import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; - export interface HomePluginStartDependencies { data: DataPublicPluginStart; telemetry?: TelemetryPluginStart; @@ -188,6 +188,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()); + }); }); }); }