Skip to content
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

RHIDP-2943: Allow users to configure MUI/Backstage all/more theme options #24

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-npm-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
run: |
latest_version=$(npm view @redhat-developer/red-hat-developer-hub-theme@latest version)
npm version $latest_version --no-git-tag-version
npm version patch --no-git-tag-version
npm version minor --no-git-tag-version
npm publish --access=public --tag=latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
146 changes: 130 additions & 16 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ import {
themes as backstageTheme,
createUnifiedTheme,
UnifiedTheme,
UnifiedThemeOptions,
UnifiedThemeProvider,
} from "@backstage/theme";

import * as rhdh10 from "../src/themes/rhdh-1.0";
import * as rhdh11 from "../src/themes/rhdh-1.1";
import * as rhdh from "../src/themes/rhdh";
import * as rhdh120 from "../src/themes/rhdh-1.2.0";
import { ThemeConfig } from "../src/types";
import { useTheme } from "../src/hooks/useTheme";

const configApi = new ConfigReader({});
const alertApi = new AlertApiForwarder();
Expand All @@ -51,16 +54,94 @@ const rhdhColors = {
},
};

const themes: Record<string, UnifiedTheme> = {
"Backstage Light": backstageTheme.light,
"Backstage Dark": backstageTheme.dark,
type Theme =
| { unifiedTheme: UnifiedTheme }
| { unifiedThemeOptions: UnifiedThemeOptions }
| { themeConfig: ThemeConfig };

"RHDH Light 1-0": rhdh10.customLightTheme(rhdhColors.light),
"RHDH Dark 1-0": rhdh10.customDarkTheme(rhdhColors.dark),
"RHDH Light 1-1": rhdh11.customLightTheme(rhdhColors.light),
"RHDH Dark 1-1": rhdh11.customDarkTheme(rhdhColors.dark),
"RHDH Light latest": rhdh.customLightTheme(rhdhColors.light),
"RHDH Dark latest": rhdh.customDarkTheme(rhdhColors.dark),
const themes: Record<string, Theme> = {
"Backstage Light": { unifiedTheme: backstageTheme.light },
"Backstage Dark": { unifiedTheme: backstageTheme.dark },

// Use underline instead of dot, because otherwise the theme switcher
// will not save the selected theme in local storage.
"RHDH Light 1-0": {
unifiedThemeOptions: rhdh10.customLightTheme(rhdhColors.light),
},
"RHDH Dark 1-0": {
unifiedThemeOptions: rhdh10.customDarkTheme(rhdhColors.dark),
},
"RHDH Light 1-1": {
unifiedThemeOptions: rhdh11.customLightTheme(rhdhColors.light),
},
"RHDH Dark 1-1": {
unifiedThemeOptions: rhdh11.customDarkTheme(rhdhColors.dark),
},
"RHDH Light 1-2-0": { unifiedThemeOptions: rhdh120.customLightTheme({}) },
"RHDH Dark 1-2-0": { unifiedThemeOptions: rhdh120.customDarkTheme({}) },

"RHDH Light latest": {
themeConfig: {},
},
"RHDH Dark latest": {
themeConfig: {
mode: "dark",
},
},
"RHDH Light customized old": {
themeConfig: {
primaryColor: "#be0000",
headerColor1: "#be0000",
headerColor2: "#f56d6d",
navigationIndicatorColor: "#be0000",
} as ThemeConfig,
},
"RHDH Dark customized old": {
themeConfig: {
mode: "dark",
primaryColor: "#be0000",
headerColor1: "#be0000",
headerColor2: "#f56d6d",
navigationIndicatorColor: "#be0000",
} as ThemeConfig,
},
"RHDH Light customized new": {
themeConfig: {
palette: {
primary: {
main: "#ff0000",
},
secondary: {
main: "#00ff00",
},
},
defaultPageTheme: "home",
pageTheme: {
home: {
backgroundColor: ["#ff0000", "#00ff00"],
},
},
},
},
"RHDH Dark customized new": {
themeConfig: {
mode: "dark",
palette: {
primary: {
main: "#ff0000",
},
secondary: {
main: "#00ff00",
},
},
defaultPageTheme: "home",
pageTheme: {
home: {
backgroundColor: ["#ff0000", "#00ff00"],
},
},
},
},
};

const defaultTheme = "RHDH Light latest";
Expand All @@ -71,20 +152,53 @@ declare global {
}
}

const ThemeProvider = ({ theme, children }) => {
const ThemeProvider = ({
theme,
children,
}: {
theme: Theme;
children: React.ReactNode;
}) => {
const [overrideTheme, setOverrideTheme] = React.useState<string | undefined>(
undefined,
);
window.changeTheme = setOverrideTheme;

const actualTheme = (overrideTheme && themes[overrideTheme]) || theme;

if ("unifiedTheme" in actualTheme) {
return (
<UnifiedThemeProvider theme={actualTheme.unifiedTheme}>
<TestApiProvider apis={apis}>{children}</TestApiProvider>
</UnifiedThemeProvider>
);
} else if ("unifiedThemeOptions" in actualTheme) {
return (
<UnifiedThemeProvider
theme={createUnifiedTheme(actualTheme.unifiedThemeOptions)}
>
<TestApiProvider apis={apis}>{children}</TestApiProvider>
</UnifiedThemeProvider>
);
} else {
return (
<ThemeConfigProvider themeConfig={actualTheme.themeConfig}>
{children}
</ThemeConfigProvider>
);
}
};

const ThemeConfigProvider = ({
themeConfig,
children,
}: {
themeConfig: ThemeConfig;
children: React.ReactNode;
}) => {
const theme = useTheme(themeConfig);
return (
<UnifiedThemeProvider
theme={
actualTheme.getTheme ? actualTheme : createUnifiedTheme(actualTheme)
}
>
<UnifiedThemeProvider theme={theme}>
<TestApiProvider apis={apis}>{children}</TestApiProvider>
</UnifiedThemeProvider>
);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"redhat"
],
"homepage": "https://developers.redhat.com/rhdh",
"version": "0.0.4",
"version": "0.0.0",
"author": "Red Hat",
"maintainers": [
{
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./useBranding";
export * from "./useThemeConfig";
export * from "./useThemeOptions";
65 changes: 65 additions & 0 deletions src/hooks/useBranding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { renderHook } from "@testing-library/react-hooks";
import { useApi } from "@backstage/core-plugin-api";
import { MockConfigApi } from "@backstage/test-utils";
import { JsonObject } from "@backstage/types";
import { Config } from "../types";
import { useBranding } from "./useBranding";

jest.mock("@backstage/core-plugin-api", () => ({
...jest.requireActual("@backstage/core-plugin-api"),
useApi: jest.fn(),
}));

const mockAppConfig = (appConfig: Config) => {
(useApi as jest.Mock).mockReturnValue(
new MockConfigApi(appConfig as unknown as JsonObject),
);
};

describe("useBranding", () => {
it("returns undefined if the config contains no branding", () => {
mockAppConfig({
app: {},
});
const { result } = renderHook(() => useBranding());
expect(result.current).toBeUndefined();
});

it("returns the branding without theme", () => {
mockAppConfig({
app: {
branding: {},
},
});
const { result } = renderHook(() => useBranding());
expect(result.current).toEqual({});
});

it("returns the branding with theme", () => {
mockAppConfig({
app: {
branding: {
theme: {
emptyTheme: {},
anotherTheme: {
palette: {
divider: "red",
},
},
},
},
},
});
const { result } = renderHook(() => useBranding());
expect(result.current).toEqual({
theme: {
emptyTheme: {},
anotherTheme: {
palette: {
divider: "red",
},
},
},
});
});
});
16 changes: 16 additions & 0 deletions src/hooks/useBranding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import { ConfigApi, configApiRef, useApi } from "@backstage/core-plugin-api";
import { Branding } from "../types";

export const useBranding = (): Branding | undefined => {
let configApi: ConfigApi | undefined = undefined;
try {
configApi = useApi(configApiRef);
} catch (err) {
// useApi won't be initialized initially in createApp theme provider, and will get updated later
}
return React.useMemo(() => {
const branding = configApi?.getOptional<Branding>("app.branding");
return branding;
}, [configApi]);
};
86 changes: 86 additions & 0 deletions src/hooks/useTheme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { renderHook } from "@testing-library/react-hooks";
import { useTheme } from "./useTheme";

describe("useTheme", () => {
it("returns rhdh theming options for an empty theme config", () => {
const { result } = renderHook(() => useTheme({}));
const unifiedTheme = result.current;
const v5Theme = unifiedTheme.getTheme("v5")!;
expect(v5Theme).toEqual({
applyStyles: expect.any(Function),
breakpoints: expect.any(Object),
components: expect.any(Object),
direction: "ltr",
getPageTheme: expect.any(Function),
mixins: expect.any(Object),
page: expect.any(Object),
palette: expect.any(Object),
shadows: expect.any(Object),
shape: expect.any(Object),
spacing: expect.any(Function),
transitions: expect.any(Object),
typography: expect.any(Object),
unstable_sx: expect.any(Function),
unstable_sxConfig: expect.any(Object),
zIndex: expect.any(Object),
});
});

it("returns rhdh theming options for variant rhdh", () => {
const { result } = renderHook(() =>
useTheme({
mode: "dark",
variant: "rhdh",
}),
);
const unifiedTheme = result.current;
const v5Theme = unifiedTheme.getTheme("v5");
expect(v5Theme).toEqual({
applyStyles: expect.any(Function),
breakpoints: expect.any(Object),
components: expect.any(Object),
direction: "ltr",
getPageTheme: expect.any(Function),
mixins: expect.any(Object),
page: expect.any(Object),
palette: expect.any(Object),
shadows: expect.any(Object),
shape: expect.any(Object),
spacing: expect.any(Function),
transitions: expect.any(Object),
typography: expect.any(Object),
unstable_sx: expect.any(Function),
unstable_sxConfig: expect.any(Object),
zIndex: expect.any(Object),
});
});

it("returns backstage theming options for backstage variant", () => {
const { result } = renderHook(() =>
useTheme({
mode: "dark",
variant: "backstage",
}),
);
const unifiedTheme = result.current;
const v5Theme = unifiedTheme.getTheme("v5");
expect(v5Theme).toEqual({
applyStyles: expect.any(Function),
breakpoints: expect.any(Object),
components: expect.any(Object),
direction: "ltr",
getPageTheme: expect.any(Function),
mixins: expect.any(Object),
page: expect.any(Object),
palette: expect.any(Object),
shadows: expect.any(Object),
shape: expect.any(Object),
spacing: expect.any(Function),
transitions: expect.any(Object),
typography: expect.any(Object),
unstable_sx: expect.any(Function),
unstable_sxConfig: expect.any(Object),
zIndex: expect.any(Object),
});
});
});
Loading