Skip to content

Commit 42a74ec

Browse files
authored
feat: DH-19307: External theme support (#2425)
DH-19307: External theme support via `postMessage` apis - Moved common MessageUtils into @deephaven/utils - External themes can be enabled by setting `theme=[EXTERNAL_THEME_KEY]` query string param - ThemeBootstrap now checks if external themes are enabled. If so, the theme is requested from the parent Window via `MSG_REQUEST_GET_THEME` postMessage - Current or parent Window can explicitly set external theme via `MSG_REQUEST_SET_THEME` postMessage ### Testing You can download this html file: https://gist.github.com/bmingles/02451e09d218cdbe61d8c506e556562b . You should be able to open it in browser from local filesystem, no need for server. 1. Start DH locally in this branch (assumes port 4000) 1. Open the html file 1. Initial load should show red background color showing DH requesting theme from the parent 1. Clicking the "Update Theme" button in bottom left should change to random bg color. Demonstrates explicit setting of theme from parent Window
1 parent 09077bf commit 42a74ec

28 files changed

Lines changed: 1244 additions & 315 deletions

package-lock.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app-utils/src/components/LoginNotifier.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useBroadcastChannel } from '@deephaven/jsapi-components';
2-
import { BROADCAST_LOGIN_MESSAGE, makeMessage } from '@deephaven/jsapi-utils';
2+
import { BROADCAST_LOGIN_MESSAGE } from '@deephaven/jsapi-utils';
3+
import { makeMessage } from '@deephaven/utils';
34
import { useEffect } from 'react';
45

56
/**

packages/app-utils/src/components/ThemeBootstrap.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
1-
import { useContext, useMemo } from 'react';
1+
import { useContext } from 'react';
22
import { ChartThemeProvider } from '@deephaven/chart';
33
import { MonacoThemeProvider } from '@deephaven/console';
4-
import { ThemeProvider } from '@deephaven/components';
4+
import { isExternalThemeEnabled, ThemeProvider } from '@deephaven/components';
5+
import { useAppSelector } from '@deephaven/dashboard';
56
import { IrisGridThemeProvider } from '@deephaven/iris-grid';
6-
import { getThemeDataFromPlugins, PluginsContext } from '@deephaven/plugin';
7+
import { PluginsContext, useCustomThemes } from '@deephaven/plugin';
78
import { getSettings } from '@deephaven/redux';
8-
import { useAppSelector } from '@deephaven/dashboard';
99

1010
export interface ThemeBootstrapProps {
1111
children: React.ReactNode;
1212
}
1313

14-
export function ThemeBootstrap({ children }: ThemeBootstrapProps): JSX.Element {
14+
export function ThemeBootstrap({
15+
children,
16+
}: ThemeBootstrapProps): JSX.Element | null {
17+
const settings = useAppSelector(getSettings);
18+
1519
// The `usePlugins` hook throws if the context value is null. Since this is
1620
// the state while plugins load asynchronously, we are using `useContext`
1721
// directly to avoid the exception.
1822
const pluginModules = useContext(PluginsContext);
19-
20-
const themes = useMemo(
21-
() =>
22-
pluginModules == null ? null : getThemeDataFromPlugins(pluginModules),
23-
[pluginModules]
24-
);
25-
26-
const settings = useAppSelector(getSettings);
23+
const themes = useCustomThemes(pluginModules);
24+
const waitForActivation = isExternalThemeEnabled();
2725

2826
return (
29-
<ThemeProvider themes={themes}>
27+
<ThemeProvider themes={themes} waitForActivation={waitForActivation}>
3028
<ChartThemeProvider>
3129
<MonacoThemeProvider>
3230
<IrisGridThemeProvider density={settings.gridDensity}>

packages/auth-plugins/src/AuthPluginParent.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ let mockParentResponse: Promise<LoginOptions>;
1111
jest.mock('@deephaven/jsapi-utils', () => ({
1212
...jest.requireActual('@deephaven/jsapi-utils'),
1313
LOGIN_OPTIONS_REQUEST: 'mock-login-options-request',
14+
}));
15+
jest.mock('@deephaven/utils', () => ({
16+
...jest.requireActual('@deephaven/utils'),
1417
requestParentResponse: jest.fn(() => mockParentResponse),
1518
}));
1619

packages/auth-plugins/src/AuthPluginParent.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import React from 'react';
22
import type { dh } from '@deephaven/jsapi-types';
3-
import {
4-
getWindowParent,
5-
LOGIN_OPTIONS_REQUEST,
6-
requestParentResponse,
7-
} from '@deephaven/jsapi-utils';
3+
import { LOGIN_OPTIONS_REQUEST } from '@deephaven/jsapi-utils';
84
import Log from '@deephaven/log';
5+
import { getWindowParent, requestParentResponse } from '@deephaven/utils';
96
import { type AuthPlugin, type AuthPluginProps } from './AuthPlugin';
107
import AuthPluginBase from './AuthPluginBase';
118
import {

packages/code-studio/src/settings/SettingsMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ import { type ServerConfigValues, type User, store } from '@deephaven/redux';
2929
import {
3030
BROADCAST_CHANNEL_NAME,
3131
BROADCAST_LOGOUT_MESSAGE,
32-
makeMessage,
3332
} from '@deephaven/jsapi-utils';
3433
import { type PluginModuleMap } from '@deephaven/plugin';
3534
import { exportLogs, logHistory } from '@deephaven/log';
35+
import { makeMessage } from '@deephaven/utils';
3636
import FormattingSectionContent from './FormattingSectionContent';
3737
import LegalNotice from './LegalNotice';
3838
import SettingsMenuSection from './SettingsMenuSection';

packages/components/src/theme/ThemeModel.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type BaseThemeType = 'dark' | 'light';
22
export type BaseThemeKey = `default-${BaseThemeType}`;
33
export type CssVariableStyleContent = `:root{${string}`;
44
export type ThemeCssVariableName = `--dh-${string}`;
5+
export type ThemeCssColorVariableName = `--dh-color-${string}`;
56

67
// DHC should only need to preload variables that are required by the empty page
78
// with loading spinner that shows while plugins are loading. The rest of the
@@ -43,7 +44,14 @@ export type ThemeIconsRequiringManualColorChanges =
4344

4445
export const DEFAULT_DARK_THEME_KEY = 'default-dark' satisfies BaseThemeKey;
4546
export const DEFAULT_LIGHT_THEME_KEY = 'default-light' satisfies BaseThemeKey;
46-
export const THEME_KEY_OVERRIDE_QUERY_PARAM = 'theme';
47+
export const EXTERNAL_THEME_KEY = 'external-theme' as const;
48+
export const MSG_REQUEST_GET_THEME =
49+
'io.deephaven.message.ThemeModel.requestExternalTheme';
50+
export const MSG_REQUEST_SET_THEME =
51+
'io.deephaven.message.ThemeModel.requestSetTheme';
52+
export const PRELOAD_TRANSPARENT_THEME_QUERY_PARAM =
53+
'preloadTransparentTheme' as const;
54+
export const THEME_KEY_OVERRIDE_QUERY_PARAM = 'theme' as const;
4755

4856
// Hex versions of some of the default dark theme color palette needed for
4957
// preload defaults.
@@ -109,6 +117,14 @@ export const DEFAULT_PRELOAD_DATA_VARIABLES: Record<
109117
DEFAULT_DARK_THEME_PALETTE.gray[300],
110118
};
111119

120+
export const TRANSPARENT_PRELOAD_DATA_VARIABLES: Partial<
121+
Record<ThemePreloadColorVariable, string>
122+
> = {
123+
'--dh-color-bg': 'transparent',
124+
'--dh-color-loading-spinner-primary': 'transparent',
125+
'--dh-color-loading-spinner-secondary': 'transparent',
126+
};
127+
112128
/**
113129
* Some inline SVGs require manually updating their fill color via
114130
* `updateSVGFillColors`. This object maps these variables to their respective
@@ -142,3 +158,9 @@ export interface ThemeRegistrationData {
142158
base: ThemeData[];
143159
custom: ThemeData[];
144160
}
161+
162+
export interface ExternalThemeData {
163+
baseThemeKey?: BaseThemeKey;
164+
name: string;
165+
cssVars: Record<ThemeCssColorVariableName, string>;
166+
}

packages/components/src/theme/ThemeProvider.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,23 @@ export interface ThemeProviderProps {
3636
* tell the provider to activate the base themes.
3737
*/
3838
themes: ThemeData[] | null;
39+
// In DHC web, custom themes are typically loaded from plugins. Since these
40+
// get loaded after login, we have to be able to render the children containing
41+
// the `Login` component before themes are activated. This means that `children`
42+
// get loaded before all styles are available. In cases where themes don't require
43+
// children to be rendered such as external themes requested from a parent Window,
44+
// we can defer the rendering of children until the themes are activated which
45+
// is less likely to render initial content with the wrong theme. We can get
46+
// rid of this prop if we ever find a way to load themes without depending on
47+
// children. See https://deephaven.atlassian.net/browse/DH-19400
48+
waitForActivation?: boolean;
3949
defaultPreloadValues?: Record<string, string>;
4050
children: ReactNode;
4151
}
4252

4353
export function ThemeProvider({
4454
themes: customThemes,
55+
waitForActivation = false,
4556
defaultPreloadValues = DEFAULT_PRELOAD_DATA_VARIABLES,
4657
children,
4758
}: ThemeProviderProps): JSX.Element | null {
@@ -108,6 +119,10 @@ export function ThemeProvider({
108119
});
109120
}, [activeThemes, selectedThemeKey, themes]);
110121

122+
if (waitForActivation && activeThemes == null) {
123+
return null;
124+
}
125+
111126
return (
112127
<>
113128
{activeThemes == null ? null : (

0 commit comments

Comments
 (0)