Skip to content

Commit 4d11b53

Browse files
authored
OCPBUGS-53412,CONSOLE-4404,CONSOLE-4062: Add the ability to specify second logo, favicons (#14749)
* CONSOLE-4404: Adopt new customLogos configuration in bridge and console to make it possible to specify separate masthead logo for dark and light theme. * CONSOLE-4404: bump api changes * CONSOLE-4404: Adjustments after recent API changes, parsing custom logos from config file to two separate flags for favicon and masthead. * CONSOLE-4404: consolidations after recent API changes * Fix the problem with customLogo endpoint not retrieving favicons * CONSOLE-4062: Add custom favicon * Adjust about modal logos logic * fix failing tests * update customLogos and customFavicons description * make logic for the LogosKeyValue Set method correspond to MultiKeyValue Set * introduce new SERVER_FLAGS that provide console frontend with information if there is a favicon or customLogo customization * Added a loading logic so that we prevent frontend from rendering static logos when custom logos have not been fetched yet. Used the newly introduced SERVER_FLAGS customFaviconsConfigured and customLogosConfigured in a logic that prevents attempts to fetch custom logo type that is not configured. * revert the capitalization of the theme global var values to fix test regressions, capitalize the custom logo query parameter to match the expected backend syntax.
1 parent bd6ef9a commit 4d11b53

1,442 files changed

Lines changed: 69967 additions & 64044 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/bridge/main.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ func main() {
111111

112112
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.")
113113
fCustomProductName := fs.String("custom-product-name", "", "Custom product name for console branding.")
114-
fCustomLogoFile := fs.String("custom-logo-file", "", "Custom product image for console branding.")
114+
115+
customLogoFlags := serverconfig.LogosKeyValue{}
116+
fs.Var(&customLogoFlags, "custom-logo-files", "List of custom product images used for console branding. Each entry consist of theme type (Dark | Light ) as a key and path to image file as value.")
117+
customFaviconFlags := serverconfig.LogosKeyValue{}
118+
fs.Var(&customFaviconFlags, "custom-favicon-files", "List of custom favicon images used for console branding. Each entry consist of theme type (Dark | Light ) as a key and path to image file as value.")
115119
fStatuspageID := fs.String("statuspage-id", "", "Unique ID assigned by statuspage.io page that provides status info.")
116120
fDocumentationBaseURL := fs.String("documentation-base-url", "", "The base URL for documentation links.")
117121

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

210-
if *fCustomLogoFile != "" {
211-
if _, err := os.Stat(*fCustomLogoFile); err != nil {
212-
klog.Fatalf("could not read logo file: %v", err)
213-
}
214-
}
215-
216214
if len(consolePluginsFlags) > 0 {
217215
klog.Infoln("The following console plugins are enabled:")
218216
for pluginName := range consolePluginsFlags {
@@ -266,7 +264,8 @@ func main() {
266264
BaseURL: baseURL,
267265
Branding: branding,
268266
CustomProductName: *fCustomProductName,
269-
CustomLogoFile: *fCustomLogoFile,
267+
CustomLogoFiles: customLogoFlags,
268+
CustomFaviconFiles: customFaviconFlags,
270269
ControlPlaneTopology: *fControlPlaneTopology,
271270
StatuspageID: *fStatuspageID,
272271
DocumentationBaseURL: documentationBaseURL,

frontend/@types/console/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ declare interface Window {
2222
branding: string;
2323
consoleVersion: string;
2424
customLogoURL: string;
25+
customLogosConfigured: boolean;
26+
customFaviconsConfigured: boolean;
2527
customProductName: string;
2628
documentationBaseURL: string;
2729
kubeAPIServerURL: string;

frontend/public/components/ThemeProvider.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,40 @@ export const THEME_LOCAL_STORAGE_KEY = 'bridge/theme';
66
const THEME_SYSTEM_DEFAULT = 'systemDefault';
77
const THEME_DARK_CLASS = 'pf-v6-theme-dark';
88
const THEME_DARK_CLASS_LEGACY = 'pf-v5-theme-dark'; // legacy class name needed to support PF5
9-
const THEME_DARK = 'dark';
10-
const THEME_LIGHT = 'light';
9+
export const THEME_DARK = 'dark';
10+
export const THEME_LIGHT = 'light';
11+
export const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');
1112

1213
type PROCESSED_THEME = typeof THEME_DARK | typeof THEME_LIGHT;
1314

14-
export const updateThemeClass = (htmlTagElement: HTMLElement, theme: string): PROCESSED_THEME => {
15-
let systemTheme: string;
16-
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
17-
systemTheme = THEME_DARK;
15+
export const applyThemeBehaviour = (
16+
theme: string,
17+
onDarkBehaviour?: () => string,
18+
onLightBehaviour?: () => string,
19+
) => {
20+
if (darkThemeMq.matches && theme === THEME_SYSTEM_DEFAULT) {
21+
theme = THEME_DARK;
1822
}
19-
if (theme === THEME_DARK || (theme === THEME_SYSTEM_DEFAULT && systemTheme === THEME_DARK)) {
20-
htmlTagElement.classList.add(THEME_DARK_CLASS);
21-
htmlTagElement.classList.add(THEME_DARK_CLASS_LEGACY);
22-
return THEME_DARK;
23+
if (theme === THEME_DARK) {
24+
return onDarkBehaviour();
2325
}
26+
return onLightBehaviour();
27+
};
2428

25-
htmlTagElement.classList.remove(THEME_DARK_CLASS);
26-
htmlTagElement.classList.remove(THEME_DARK_CLASS_LEGACY);
27-
return THEME_LIGHT;
29+
export const updateThemeClass = (htmlTagElement: HTMLElement, theme: string): PROCESSED_THEME => {
30+
return applyThemeBehaviour(
31+
theme,
32+
() => {
33+
htmlTagElement.classList.add(THEME_DARK_CLASS);
34+
htmlTagElement.classList.add(THEME_DARK_CLASS_LEGACY);
35+
return THEME_DARK;
36+
},
37+
() => {
38+
htmlTagElement.classList.remove(THEME_DARK_CLASS);
39+
htmlTagElement.classList.remove(THEME_DARK_CLASS_LEGACY);
40+
return THEME_LIGHT;
41+
},
42+
) as PROCESSED_THEME;
2843
};
2944

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

5974
React.useEffect(() => {
60-
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');
6175
if (theme === THEME_SYSTEM_DEFAULT) {
6276
darkThemeMq.addEventListener('change', mqListener);
6377
}

frontend/public/components/about-modal.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next';
1010
import { useClusterVersion, BlueArrowCircleUpIcon, useCanClusterUpgrade } from '@console/shared';
1111
import { isLoadedDynamicPluginInfo } from '@console/plugin-sdk/src';
1212
import { useDynamicPluginInfo } from '@console/plugin-sdk/src/api/useDynamicPluginInfo';
13-
import { getBrandingDetails } from './utils/branding';
13+
import { getBrandingDetails, MASTHEAD_TYPE, useCustomLogoURL } from './utils/branding';
1414
import {
1515
ReleaseNotesLink,
1616
ServiceLevel,
@@ -162,16 +162,18 @@ AboutModalItems.displayName = 'AboutModalItems';
162162
export const AboutModal: React.FC<AboutModalProps> = (props) => {
163163
const { isOpen, closeAboutModal } = props;
164164
const { t } = useTranslation();
165-
const details = getBrandingDetails();
166-
const customBranding = window.SERVER_FLAGS.customLogoURL || window.SERVER_FLAGS.customProductName;
167-
const openShiftBranding = window.SERVER_FLAGS.branding !== 'okd' && !customBranding;
165+
const { productName } = getBrandingDetails();
166+
const { logoUrl: customLogoUrl } = useCustomLogoURL(MASTHEAD_TYPE);
167+
168+
const customBranding = customLogoUrl || window.SERVER_FLAGS.customProductName;
169+
const openShiftBranding = window.SERVER_FLAGS.branding !== 'okd';
168170
return (
169171
<PfAboutModal
170172
isOpen={isOpen}
171173
onClose={closeAboutModal}
172-
productName={details.productName}
173-
brandImageSrc={openShiftBranding && redHatFedoraImg}
174-
brandImageAlt={openShiftBranding && details.productName}
174+
productName={productName}
175+
brandImageSrc={customLogoUrl || (openShiftBranding && redHatFedoraImg)}
176+
brandImageAlt={(openShiftBranding || customLogoUrl) && productName}
175177
backgroundImageSrc={openShiftBranding && `/${redHatFedoraWatermarkImg}`}
176178
hasNoContentContainer
177179
aria-label="About modal"

frontend/public/components/masthead-toolbar.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { action as reduxAction } from 'typesafe-actions';
5555
import feedbackImage from '@patternfly/react-user-feedback/dist/esm/images/rh_feedback.svg';
5656
import darkFeedbackImage from '@patternfly/react-user-feedback/dist/esm/images/rh_feedback-dark.svg';
5757
import QuickCreate, { QuickCreateImportFromGit, QuickCreateContainerImages } from './QuickCreate';
58-
import { ThemeContext } from './ThemeProvider';
58+
import { ThemeContext, THEME_DARK } from './ThemeProvider';
5959

6060
const LAST_CONSOLE_ACTIVITY_TIMESTAMP_LOCAL_STORAGE_KEY = 'last-console-activity-timestamp';
6161

@@ -94,7 +94,7 @@ const FeedbackModalLocalized = ({ isOpen, onClose, reportBugLink }) => {
9494
onOpenSupportCase={reportBugLink.href}
9595
feedbackLocale={feedbackLocales}
9696
onJoinMailingList="https://console.redhat.com/self-managed-research-form?source=openshift"
97-
feedbackImg={theme === 'dark' ? darkFeedbackImage : feedbackImage}
97+
feedbackImg={theme === THEME_DARK ? darkFeedbackImage : feedbackImage}
9898
isOpen={isOpen}
9999
onClose={onClose}
100100
/>

frontend/public/components/masthead.jsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,32 @@ import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon';
1414
import { useNavigate } from 'react-router-dom-v5-compat';
1515
import { ReactSVG } from 'react-svg';
1616
import { MastheadToolbar } from './masthead-toolbar';
17-
import { getBrandingDetails } from './utils/branding';
17+
import {
18+
FAVICON_TYPE,
19+
getBrandingDetails,
20+
MASTHEAD_TYPE,
21+
useCustomLogoURL,
22+
} from './utils/branding';
1823

1924
export const Masthead = React.memo(({ isMastheadStacked, isNavOpen, onNavToggle }) => {
20-
const details = getBrandingDetails();
25+
const { productName, staticLogo } = getBrandingDetails();
2126
const navigate = useNavigate();
27+
28+
const { logoUrl: customMastheadUrl, loading } = useCustomLogoURL(MASTHEAD_TYPE);
29+
const { logoUrl: customFaviconUrl } = useCustomLogoURL(FAVICON_TYPE);
30+
31+
React.useEffect(() => {
32+
if (customFaviconUrl) {
33+
let link = document.querySelector("link[rel='icon']");
34+
if (!link) {
35+
link = document.createElement('link');
36+
link.rel = 'icon';
37+
document.head.appendChild(link);
38+
}
39+
link.href = customFaviconUrl;
40+
}
41+
}, [customFaviconUrl]);
42+
2243
const defaultRoute = '/';
2344
const logoProps = {
2445
href: defaultRoute,
@@ -40,14 +61,14 @@ export const Masthead = React.memo(({ isMastheadStacked, isNavOpen, onNavToggle
4061
<MastheadBrand>
4162
<MastheadLogo
4263
component="a"
43-
aria-label={window.SERVER_FLAGS.customLogoURL ? undefined : details.productName}
64+
aria-label={productName}
4465
data-test="masthead-logo"
4566
{...logoProps}
4667
>
47-
{window.SERVER_FLAGS.customLogoURL ? (
48-
<Brand src={details.logoImg} alt={details.productName} />
68+
{customMastheadUrl ? (
69+
<Brand src={customMastheadUrl} alt={productName} />
4970
) : (
50-
<ReactSVG src={details.logoImg} aria-hidden className="pf-v6-c-brand" />
71+
!loading && <ReactSVG src={staticLogo} aria-hidden className="pf-v6-c-brand" />
5172
)}
5273
</MastheadLogo>
5374
</MastheadBrand>
Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,121 @@
1+
import * as React from 'react';
2+
import {
3+
ThemeContext,
4+
THEME_DARK,
5+
THEME_LIGHT,
6+
applyThemeBehaviour,
7+
darkThemeMq,
8+
} from '@console/internal/components/ThemeProvider';
19
import okdLogoImg from '../../imgs/okd-logo.svg';
210
import openshiftLogoImg from '../../imgs/openshift-logo.svg';
311
import onlineLogoImg from '../../imgs/openshift-online-logo.svg';
412
import dedicatedLogoImg from '../../imgs/openshift-dedicated-logo.svg';
513
import rosaLogoImg from '../../imgs/openshift-service-on-aws-logo.svg';
14+
import { capitalize } from 'lodash';
15+
16+
type CUSTOM_LOGO = typeof FAVICON_TYPE | typeof MASTHEAD_TYPE;
17+
export const FAVICON_TYPE = 'Favicon';
18+
export const MASTHEAD_TYPE = 'Masthead';
619

720
export const getBrandingDetails = () => {
8-
let logoImg, productName;
21+
let staticLogo, productName;
922
// Webpack won't bundle these images if we don't directly reference them, hence the switch
1023
switch (window.SERVER_FLAGS.branding) {
1124
case 'openshift':
12-
logoImg = openshiftLogoImg;
25+
staticLogo = openshiftLogoImg;
1326
productName = 'Red Hat OpenShift';
1427
break;
1528
case 'ocp':
16-
logoImg = openshiftLogoImg;
29+
staticLogo = openshiftLogoImg;
1730
productName = 'Red Hat OpenShift';
1831
break;
1932
case 'online':
20-
logoImg = onlineLogoImg;
33+
staticLogo = onlineLogoImg;
2134
productName = 'Red Hat OpenShift Online';
2235
break;
2336
case 'dedicated':
24-
logoImg = dedicatedLogoImg;
37+
staticLogo = dedicatedLogoImg;
2538
productName = 'Red Hat OpenShift Dedicated';
2639
break;
2740
case 'azure':
28-
logoImg = openshiftLogoImg;
41+
staticLogo = openshiftLogoImg;
2942
productName = 'Azure Red Hat OpenShift';
3043
break;
3144
case 'rosa':
32-
logoImg = rosaLogoImg;
45+
staticLogo = rosaLogoImg;
3346
productName = 'Red Hat OpenShift Service on AWS';
3447
break;
3548
default:
36-
logoImg = okdLogoImg;
49+
staticLogo = okdLogoImg;
3750
productName = 'OKD';
3851
}
39-
if (window.SERVER_FLAGS.customLogoURL) {
40-
logoImg = window.SERVER_FLAGS.customLogoURL;
41-
}
4252
if (window.SERVER_FLAGS.customProductName) {
4353
productName = window.SERVER_FLAGS.customProductName;
4454
}
45-
return { logoImg, productName };
55+
return { staticLogo, productName };
56+
};
57+
58+
/** Fetches given custom logo and returns its blob URL.
59+
* Fetches the bridge's custom logo endpoint with query parameters that specify the type
60+
* and the theme of the requested custom logo.
61+
* The hook listens for theme changes to provide correct custom logo for given theme.
62+
* results into redux, adjusts the API polling frequency based on the poll success.
63+
* @param type - The type of the custom logo to query.
64+
* @returns Returns logoURL blob URL and the loading state of the API request.
65+
*/
66+
export const useCustomLogoURL = (type: CUSTOM_LOGO): { logoUrl: string; loading: Boolean } => {
67+
const [logoUrl, setLogoUrl] = React.useState('');
68+
const [loading, setLoading] = React.useState(false);
69+
const theme = React.useContext(ThemeContext);
70+
71+
React.useEffect(() => {
72+
// return when requested custom logo type is not configured
73+
if (
74+
(type === MASTHEAD_TYPE && !window.SERVER_FLAGS.customLogosConfigured) ||
75+
(type === FAVICON_TYPE && !window.SERVER_FLAGS.customFaviconsConfigured)
76+
) {
77+
return;
78+
}
79+
setLoading(true);
80+
let reqTheme;
81+
const fetchData = async () => {
82+
if (type === FAVICON_TYPE) {
83+
if (!darkThemeMq.matches) {
84+
// Fetch Light theme favicon if the Dark preference is not set via the system preference
85+
reqTheme = THEME_LIGHT;
86+
} else {
87+
reqTheme = THEME_DARK;
88+
}
89+
} else {
90+
reqTheme = applyThemeBehaviour(
91+
theme,
92+
() => {
93+
return THEME_DARK;
94+
},
95+
() => {
96+
return THEME_LIGHT;
97+
},
98+
);
99+
}
100+
const fetchURL = `${window.SERVER_FLAGS.basePath}custom-logo?type=${type}&theme=${capitalize(
101+
reqTheme,
102+
)}`;
103+
const response = await fetch(fetchURL);
104+
if (response.ok) {
105+
const blob = await response.blob();
106+
setLogoUrl(URL.createObjectURL(blob));
107+
setLoading(false);
108+
} else if (response.status === 404) {
109+
return;
110+
} else {
111+
throw new Error(`Failed to fetch ${fetchURL}: ${response.statusText}`);
112+
}
113+
};
114+
fetchData().catch((err) => {
115+
// eslint-disable-next-line no-console
116+
console.warn(`Error while fetching ${type} logo: ${err}`);
117+
});
118+
}, [theme, type]);
119+
120+
return { logoUrl, loading };
46121
};
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

frontend/public/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<title>Red Hat OpenShift Service on AWS</title>
3030
<meta name="application-name" content="Red Hat OpenShift Service on AWS" />
3131
[[ end ]] [[ if eq .ServerFlags.Branding "okd" ]]
32-
<link rel="shortcut icon" href="<%= require('./imgs/okd-favicon.png') %>" />
32+
<link rel="icon" href="<%= require('./imgs/okd-favicon.png') %>" />
3333
<link
3434
rel="apple-touch-icon-precomposed"
3535
sizes="144x144"
@@ -42,7 +42,7 @@
4242
content="<%= require('./imgs/okd-mstile-144x144.png') %>"
4343
/>
4444
[[ else ]]
45-
<link rel="shortcut icon" href="<%= require('./imgs/openshift-favicon.png') %>" />
45+
<link rel="icon" href="<%= require('./imgs/openshift-favicon.png') %>" />
4646
<link
4747
rel="apple-touch-icon-precomposed"
4848
sizes="144x144"

0 commit comments

Comments
 (0)