Skip to content

OCPBUGS-53412,CONSOLE-4404,CONSOLE-4062: Add the ability to specify second logo, favicons #14749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 7 additions & 8 deletions cmd/bridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ func main() {

fBranding := fs.String("branding", "okd", "Console branding for the masthead logo and title. One of okd, openshift, ocp, online, dedicated, azure, or rosa. Defaults to okd.")
fCustomProductName := fs.String("custom-product-name", "", "Custom product name for console branding.")
fCustomLogoFile := fs.String("custom-logo-file", "", "Custom product image for console branding.")

customLogoFlags := serverconfig.LogosKeyValue{}
fs.Var(&customLogoFlags, "custom-logo-files", "List of custom product images used for console branding. Each entry consist of theme type (Dark | Light | Default) as a key and path to image file as value.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets add better description for these two fields, since one sets custom logo for favicon and the other for masthead

Copy link
Contributor

@yapei yapei Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec.customization.customLogoFile field is still valid for consoles.operator configuration although only one of spec.customization.customLogoFile and spec.customization.logos can take effect, shall we keep flag -custom-logo-file ?

Copy link
Contributor Author

@Mylanos Mylanos Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, even though spec.customization.customLogoFile is marked as deprecated console.operator had to keep the configuration for backwards compatibility, so we won't affect customers still having the customLogoFile OpenShift API configured. In reality what CO does in case there is a customLogoFile OpenShift configuration still present is that it just feeds it to the new Logos API to configure the Masthead type custom logo for all themes ( light and dark currently). So in the end console.operator will always pass the customLogo configuration to bridge/console via the new Logos API inside the console-config.

I think we have decided to remove this configuration from bridge, due to its nature of being only a secondary API to console.operator, meaning this configuration is meant for developers of the bridge/console only and no real customer should be dependent on this.

cc @jhadvig @TheRealJon to confirm or deny.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense, thank you @Mylanos

customFaviconFlags := serverconfig.LogosKeyValue{}
fs.Var(&customFaviconFlags, "custom-favicon-files", "List of custom product images used for console branding. Each entry consist of theme type (Dark | Light | Default) as a key and path to image file as value.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we update the help message for -custom-favicon-files flag? It's exactly the same as -custom-logo-files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, will update.

fStatuspageID := fs.String("statuspage-id", "", "Unique ID assigned by statuspage.io page that provides status info.")
fDocumentationBaseURL := fs.String("documentation-base-url", "", "The base URL for documentation links.")

Expand Down Expand Up @@ -207,12 +211,6 @@ func main() {
flags.FatalIfFailed(flags.NewInvalidFlagError("branding", "value must be one of okd, openshift, ocp, online, dedicated, azure, or rosa"))
}

if *fCustomLogoFile != "" {
if _, err := os.Stat(*fCustomLogoFile); err != nil {
klog.Fatalf("could not read logo file: %v", err)
}
}

if len(consolePluginsFlags) > 0 {
klog.Infoln("The following console plugins are enabled:")
for pluginName := range consolePluginsFlags {
Expand Down Expand Up @@ -266,7 +264,8 @@ func main() {
BaseURL: baseURL,
Branding: branding,
CustomProductName: *fCustomProductName,
CustomLogoFile: *fCustomLogoFile,
CustomLogoFiles: customLogoFlags,
CustomFaviconFiles: customFaviconFlags,
ControlPlaneTopology: *fControlPlaneTopology,
StatuspageID: *fStatuspageID,
DocumentationBaseURL: documentationBaseURL,
Expand Down
4 changes: 2 additions & 2 deletions frontend/packages/console-app/console-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,8 @@
"value": "systemDefault",
"label": "%console-app~System default%"
},
{ "value": "light", "label": "%console-app~Light%" },
{ "value": "dark", "label": "%console-app~Dark%" }
{ "value": "Light", "label": "%console-app~Light%" },
{ "value": "Dark", "label": "%console-app~Dark%" }
]
}
}
Expand Down
42 changes: 28 additions & 14 deletions frontend/public/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,40 @@ export const THEME_LOCAL_STORAGE_KEY = 'bridge/theme';
const THEME_SYSTEM_DEFAULT = 'systemDefault';
const THEME_DARK_CLASS = 'pf-v6-theme-dark';
const THEME_DARK_CLASS_LEGACY = 'pf-v5-theme-dark'; // legacy class name needed to support PF5
const THEME_DARK = 'dark';
const THEME_LIGHT = 'light';
export const THEME_DARK = 'Dark';
export const THEME_LIGHT = 'Light';
export const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');

type PROCESSED_THEME = typeof THEME_DARK | typeof THEME_LIGHT;

export const updateThemeClass = (htmlTagElement: HTMLElement, theme: string): PROCESSED_THEME => {
let systemTheme: string;
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
systemTheme = THEME_DARK;
export const applyThemeBehaviour = (
theme: string,
onDarkBehaviour?: () => string,
onLightBehaviour?: () => string,
) => {
if (darkThemeMq.matches && theme === THEME_SYSTEM_DEFAULT) {
theme = THEME_DARK;
}
if (theme === THEME_DARK || (theme === THEME_SYSTEM_DEFAULT && systemTheme === THEME_DARK)) {
htmlTagElement.classList.add(THEME_DARK_CLASS);
htmlTagElement.classList.add(THEME_DARK_CLASS_LEGACY);
return THEME_DARK;
if (theme === THEME_DARK) {
return onDarkBehaviour();
}
return onLightBehaviour();
};

htmlTagElement.classList.remove(THEME_DARK_CLASS);
htmlTagElement.classList.remove(THEME_DARK_CLASS_LEGACY);
return THEME_LIGHT;
export const updateThemeClass = (htmlTagElement: HTMLElement, theme: string): PROCESSED_THEME => {
return applyThemeBehaviour(
theme,
() => {
htmlTagElement.classList.add(THEME_DARK_CLASS);
htmlTagElement.classList.add(THEME_DARK_CLASS_LEGACY);
return THEME_DARK;
},
() => {
htmlTagElement.classList.remove(THEME_DARK_CLASS);
htmlTagElement.classList.remove(THEME_DARK_CLASS_LEGACY);
return THEME_LIGHT;
},
) as PROCESSED_THEME;
};

export const ThemeContext = React.createContext<string>('');
Expand Down Expand Up @@ -57,7 +72,6 @@ export const ThemeProvider: React.FC<{}> = ({ children }) => {
);

React.useEffect(() => {
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');
if (theme === THEME_SYSTEM_DEFAULT) {
darkThemeMq.addEventListener('change', mqListener);
}
Expand Down
16 changes: 9 additions & 7 deletions frontend/public/components/about-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next';
import { useClusterVersion, BlueArrowCircleUpIcon, useCanClusterUpgrade } from '@console/shared';
import { isLoadedDynamicPluginInfo } from '@console/plugin-sdk/src';
import { useDynamicPluginInfo } from '@console/plugin-sdk/src/api/useDynamicPluginInfo';
import { getBrandingDetails } from './utils/branding';
import { getBrandingDetails, MASTHEAD_TYPE, useCustomLogoURL } from './utils/branding';
import {
ReleaseNotesLink,
ServiceLevel,
Expand Down Expand Up @@ -162,16 +162,18 @@ AboutModalItems.displayName = 'AboutModalItems';
export const AboutModal: React.FC<AboutModalProps> = (props) => {
const { isOpen, closeAboutModal } = props;
const { t } = useTranslation();
const details = getBrandingDetails();
const customBranding = window.SERVER_FLAGS.customLogoURL || window.SERVER_FLAGS.customProductName;
const openShiftBranding = window.SERVER_FLAGS.branding !== 'okd' && !customBranding;
const { productName } = getBrandingDetails();
const customLogoUrl = useCustomLogoURL(MASTHEAD_TYPE);

const customBranding = customLogoUrl || window.SERVER_FLAGS.customProductName;
const openShiftBranding = window.SERVER_FLAGS.branding !== 'okd';
return (
<PfAboutModal
isOpen={isOpen}
onClose={closeAboutModal}
productName={details.productName}
brandImageSrc={openShiftBranding && redHatFedoraImg}
brandImageAlt={openShiftBranding && details.productName}
productName={productName}
brandImageSrc={customLogoUrl || (openShiftBranding && redHatFedoraImg)}
brandImageAlt={(openShiftBranding || customLogoUrl) && productName}
backgroundImageSrc={openShiftBranding && `/${redHatFedoraWatermarkImg}`}
hasNoContentContainer
aria-label="About modal"
Expand Down
33 changes: 27 additions & 6 deletions frontend/public/components/masthead.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,32 @@ import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon';
import { useNavigate } from 'react-router-dom-v5-compat';
import { ReactSVG } from 'react-svg';
import { MastheadToolbar } from './masthead-toolbar';
import { getBrandingDetails } from './utils/branding';
import {
FAVICON_TYPE,
getBrandingDetails,
MASTHEAD_TYPE,
useCustomLogoURL,
} from './utils/branding';

export const Masthead = React.memo(({ isMastheadStacked, isNavOpen, onNavToggle }) => {
const details = getBrandingDetails();
const { productName, staticLogo } = getBrandingDetails();
const navigate = useNavigate();

const customLogoUrl = useCustomLogoURL(MASTHEAD_TYPE);
const customFaviconUrl = useCustomLogoURL(FAVICON_TYPE);

React.useEffect(() => {
if (customFaviconUrl) {
let link = document.querySelector("link[rel='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = customFaviconUrl;
}
}, [customFaviconUrl]);

const defaultRoute = '/';
const logoProps = {
href: defaultRoute,
Expand All @@ -40,14 +61,14 @@ export const Masthead = React.memo(({ isMastheadStacked, isNavOpen, onNavToggle
<MastheadBrand>
<MastheadLogo
component="a"
aria-label={window.SERVER_FLAGS.customLogoURL ? undefined : details.productName}
aria-label={productName}
data-test="masthead-logo"
{...logoProps}
>
{window.SERVER_FLAGS.customLogoURL ? (
<Brand src={details.logoImg} alt={details.productName} />
{customLogoUrl ? (
<Brand src={customLogoUrl} alt={productName} />
) : (
<ReactSVG src={details.logoImg} aria-hidden className="pf-v6-c-brand" />
<ReactSVG src={staticLogo} aria-hidden className="pf-v6-c-brand" />
)}
</MastheadLogo>
</MastheadBrand>
Expand Down
84 changes: 72 additions & 12 deletions frontend/public/components/utils/branding.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,106 @@
import * as React from 'react';
import {
ThemeContext,
THEME_DARK,
THEME_LIGHT,
applyThemeBehaviour,
darkThemeMq,
} from '@console/internal/components/ThemeProvider';
import okdLogoImg from '../../imgs/okd-logo.svg';
import openshiftLogoImg from '../../imgs/openshift-logo.svg';
import onlineLogoImg from '../../imgs/openshift-online-logo.svg';
import dedicatedLogoImg from '../../imgs/openshift-dedicated-logo.svg';
import rosaLogoImg from '../../imgs/openshift-service-on-aws-logo.svg';

type CUSTOM_LOGO = typeof FAVICON_TYPE | typeof MASTHEAD_TYPE;
export const FAVICON_TYPE = 'Favicon';
export const MASTHEAD_TYPE = 'Masthead';

export const getBrandingDetails = () => {
let logoImg, productName;
let staticLogo, productName;
// Webpack won't bundle these images if we don't directly reference them, hence the switch
switch (window.SERVER_FLAGS.branding) {
case 'openshift':
logoImg = openshiftLogoImg;
staticLogo = openshiftLogoImg;
productName = 'Red Hat OpenShift';
break;
case 'ocp':
logoImg = openshiftLogoImg;
staticLogo = openshiftLogoImg;
productName = 'Red Hat OpenShift';
break;
case 'online':
logoImg = onlineLogoImg;
staticLogo = onlineLogoImg;
productName = 'Red Hat OpenShift Online';
break;
case 'dedicated':
logoImg = dedicatedLogoImg;
staticLogo = dedicatedLogoImg;
productName = 'Red Hat OpenShift Dedicated';
break;
case 'azure':
logoImg = openshiftLogoImg;
staticLogo = openshiftLogoImg;
productName = 'Azure Red Hat OpenShift';
break;
case 'rosa':
logoImg = rosaLogoImg;
staticLogo = rosaLogoImg;
productName = 'Red Hat OpenShift Service on AWS';
break;
default:
logoImg = okdLogoImg;
staticLogo = okdLogoImg;
productName = 'OKD';
}
if (window.SERVER_FLAGS.customLogoURL) {
logoImg = window.SERVER_FLAGS.customLogoURL;
}
if (window.SERVER_FLAGS.customProductName) {
productName = window.SERVER_FLAGS.customProductName;
}
return { logoImg, productName };
return { staticLogo, productName };
};

// when user specifies logo with customLogoFile instead of customLogoFiles the URL
// query parameters will be ignored and the single specified logo will always be provided
export const useCustomLogoURL = (type: CUSTOM_LOGO): string => {
const [logoUrl, setLogoUrl] = React.useState('');
const theme = React.useContext(ThemeContext);

React.useEffect(() => {
// return when customLogos have not been configured
if (!window.SERVER_FLAGS.customLogoURL) {
return;
}
let reqTheme;
const fetchData = async () => {
if (type === FAVICON_TYPE) {
if (!darkThemeMq.matches) {
// Fetch Light theme favicon if the Dark preference is not set via the system preference
reqTheme = THEME_LIGHT;
} else {
reqTheme = THEME_DARK;
}
} else {
reqTheme = applyThemeBehaviour(
theme,
() => {
return THEME_DARK;
},
() => {
return THEME_LIGHT;
},
);
}
const fetchURL = `${window.SERVER_FLAGS.basePath}custom-logo?type=${type}&theme=${reqTheme}`;
const response = await fetch(fetchURL);
if (response.ok) {
const blob = await response.blob();
setLogoUrl(URL.createObjectURL(blob));
} else if (response.status === 404) {
return;
} else {
throw new Error(`Failed to fetch ${fetchURL}: ${response.statusText}`);
}
};
fetchData().catch((err) => {
// eslint-disable-next-line no-console
console.warn(`Error while fetching ${type} logo: ${err}`);
});
}, [theme, type]);

return logoUrl;
};
1 change: 1 addition & 0 deletions frontend/public/imgs/okd-logo-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/imgs/okd-logo-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<title>Red Hat OpenShift Service on AWS</title>
<meta name="application-name" content="Red Hat OpenShift Service on AWS" />
[[ end ]] [[ if eq .ServerFlags.Branding "okd" ]]
<link rel="shortcut icon" href="<%= require('./imgs/okd-favicon.png') %>" />
<link rel="icon" href="<%= require('./imgs/okd-favicon.png') %>" />
<link
rel="apple-touch-icon-precomposed"
sizes="144x144"
Expand All @@ -42,7 +42,7 @@
content="<%= require('./imgs/okd-mstile-144x144.png') %>"
/>
[[ else ]]
<link rel="shortcut icon" href="<%= require('./imgs/openshift-favicon.png') %>" />
<link rel="icon" href="<%= require('./imgs/openshift-favicon.png') %>" />
<link
rel="apple-touch-icon-precomposed"
sizes="144x144"
Expand Down
Loading