Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
172d5c2
Theme variable loading
bmingles Sep 15, 2023
dd445fc
Loading themes from plugins
bmingles Sep 25, 2023
f554d4f
Cleanup theme files
bmingles Sep 18, 2023
6caa97c
Renamed function and added comments
bmingles Sep 25, 2023
af7a38c
Simplified preload cache
bmingles Sep 25, 2023
32c844d
Cleanup and comments to theme utils
bmingles Sep 18, 2023
8fa4828
Logging
bmingles Sep 18, 2023
7846b1f
Comments
bmingles Sep 18, 2023
cfad94b
Refactored theme utils
bmingles Sep 19, 2023
bf93a0b
Unit tests
bmingles Sep 19, 2023
1c269cd
ThemeCache tests
bmingles Sep 20, 2023
39dfbd9
ThemeUtils tests
bmingles Sep 20, 2023
bf6c436
Theme tests
bmingles Sep 20, 2023
cd19788
useTheme and useThemeCache tests
bmingles Sep 20, 2023
85b6966
Check to make sure plugin export is defined
bmingles Sep 20, 2023
81c821b
Renamed config to themes
bmingles Sep 20, 2023
c8a93e3
Reverted some color styles
bmingles Sep 20, 2023
a51fed6
Adjusted a color
bmingles Sep 20, 2023
cf59e0e
Removed ThemeCache for a simpler mechanism
bmingles Sep 25, 2023
e3170ac
Moved theme registration to ThemeBootstrap
bmingles Sep 25, 2023
52f3e44
Improved error messages
bmingles Sep 25, 2023
e771241
Simplified test
bmingles Sep 25, 2023
4943cd9
Fixed comments
bmingles Sep 25, 2023
8a2da85
Debug log registered themes
bmingles Sep 25, 2023
8209fd4
Plugin types test
bmingles Sep 25, 2023
9ca3b4b
Addressed code review comments
bmingles Sep 28, 2023
0e24329
Removed ThemeRegistration mapping
bmingles Sep 28, 2023
7b092b4
Moved themes inside of src/
bmingles Sep 28, 2023
0587bde
Changed back to inline to avoid duplicates
bmingles Sep 28, 2023
ab10c3d
Added a comment regarding Webpack inline
bmingles Sep 28, 2023
a1a4bfd
Added inline to babel transform
bmingles Sep 28, 2023
724065a
Refactored theme provider
bmingles Sep 29, 2023
69627b7
Refactored ThemeProvider
bmingles Sep 29, 2023
52d774f
Removed 'only' from test
bmingles Sep 29, 2023
749a1a7
Added assertions
bmingles Sep 29, 2023
3b3dbeb
Removed nullish
bmingles Sep 29, 2023
892b5fe
Added test cases
bmingles Sep 29, 2023
8e2a510
Fixed a theme flicker bug
bmingles Sep 29, 2023
8a83265
Added named effect function
bmingles Oct 3, 2023
029d349
Fixed key format in failing test
bmingles Oct 3, 2023
ac83f90
Fallback $background color
bmingles Oct 3, 2023
2d9d2fa
Fallback $background color
bmingles Oct 3, 2023
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
4 changes: 4 additions & 0 deletions jest.config.base.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ module.exports = {
'./__mocks__/spectrumTheme$1Mock.js'
),
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(css|less|scss|sass)\\?inline$': path.join(
__dirname,
'./__mocks__/fileMock.js'
),
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
path.join(__dirname, './__mocks__/fileMock.js'),
'^fira$': 'identity-obj-proxy',
Expand Down
2 changes: 2 additions & 0 deletions packages/app-utils/src/components/AppBootstrap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ beforeEach(() => {
});

it('should throw if api has not been bootstrapped', () => {
TestUtils.disableConsoleOutput();

expect(() =>
render(
<AppBootstrap serverUrl={API_URL} pluginsUrl={PLUGINS_URL}>
Expand Down
37 changes: 20 additions & 17 deletions packages/app-utils/src/components/AppBootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getConnectOptions } from '../utils';
import FontsLoaded from './FontsLoaded';
import UserBootstrap from './UserBootstrap';
import ServerConfigBootstrap from './ServerConfigBootstrap';
import ThemeBootstrap from './ThemeBootstrap';

export type AppBootstrapProps = {
/** URL of the server. */
Expand Down Expand Up @@ -57,23 +58,25 @@ export function AppBootstrap({
return (
<FontBootstrap fontClassNames={fontClassNames}>
<PluginsBootstrap getCorePlugins={getCorePlugins} pluginsUrl={pluginsUrl}>
<ClientBootstrap
serverUrl={serverUrl}
options={clientOptions}
key={logoutCount}
>
<RefreshTokenBootstrap>
<AuthBootstrap>
<ServerConfigBootstrap>
<UserBootstrap>
<ConnectionBootstrap>
<FontsLoaded>{children}</FontsLoaded>
</ConnectionBootstrap>
</UserBootstrap>
</ServerConfigBootstrap>
</AuthBootstrap>
</RefreshTokenBootstrap>
</ClientBootstrap>
<ThemeBootstrap>
<ClientBootstrap
serverUrl={serverUrl}
options={clientOptions}
key={logoutCount}
>
<RefreshTokenBootstrap>
<AuthBootstrap>
<ServerConfigBootstrap>
<UserBootstrap>
<ConnectionBootstrap>
<FontsLoaded>{children}</FontsLoaded>
</ConnectionBootstrap>
</UserBootstrap>
</ServerConfigBootstrap>
</AuthBootstrap>
</RefreshTokenBootstrap>
</ClientBootstrap>
</ThemeBootstrap>
</PluginsBootstrap>
</FontBootstrap>
);
Expand Down
25 changes: 25 additions & 0 deletions packages/app-utils/src/components/ThemeBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ThemeProvider } from '@deephaven/components';
import { useContext, useMemo } from 'react';
import { getThemeDataFromPlugins } from '../plugins';
import { PluginsContext } from './PluginsBootstrap';

export interface ThemeBootstrapProps {
children: React.ReactNode;
}

export function ThemeBootstrap({ children }: ThemeBootstrapProps): JSX.Element {
// The `usePlugins` hook throws if the context value is null. Since this is
// the state while plugins load asynchronously, we are using `useContext`
// directly to avoid the exception.
const pluginModules = useContext(PluginsContext);

const themes = useMemo(
() =>
pluginModules == null ? null : getThemeDataFromPlugins(pluginModules),
[pluginModules]
);

return <ThemeProvider themes={themes}>{children}</ThemeProvider>;
}

export default ThemeBootstrap;
1 change: 1 addition & 0 deletions packages/app-utils/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './ConnectionBootstrap';
export * from './FontBootstrap';
export * from './FontsLoaded';
export * from './PluginsBootstrap';
export * from './ThemeBootstrap';
export * from './usePlugins';
export * from './useConnection';
export * from './useServerConfig';
Expand Down
2 changes: 1 addition & 1 deletion packages/app-utils/src/components/usePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PluginsContext } from './PluginsBootstrap';
export function usePlugins(): PluginModuleMap {
return useContextOrThrow(
PluginsContext,
'No Plugins available in usePlugins. Was code wrapped in PluginsBootstrap or PluginsContext.Provider?'
'No Plugins available in usePlugins. This can happen when plugins have not finished loading or if code is not wrapped in PluginsBootstrap or PluginsContext.Provider.'
);
}

Expand Down
104 changes: 104 additions & 0 deletions packages/app-utils/src/plugins/PluginUtils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ThemeData } from '@deephaven/components';
import { DashboardPlugin, PluginModule, ThemePlugin } from '@deephaven/plugin';
import { getThemeDataFromPlugins } from './PluginUtils';

beforeEach(() => {
document.body.removeAttribute('style');
document.head.innerHTML = '';
jest.clearAllMocks();
expect.hasAssertions();
});

describe('getThemeDataFromPlugins', () => {
const themePluginSingleDark: ThemePlugin = {
name: 'mock.themePluginNameA',
type: 'ThemePlugin',
themes: {
name: 'mock.customDark',
baseTheme: 'dark',
styleContent: 'mock.styleContent',
},
};

const themePluginSingleLight: ThemePlugin = {
name: 'mock.themePluginNameB',
type: 'ThemePlugin',
themes: {
name: 'mock.customLight',
baseTheme: 'light',
styleContent: 'mock.styleContent',
},
};

const themePluginMultiConfig: ThemePlugin = {
name: 'mock.themePluginNameC',
type: 'ThemePlugin',
themes: [
{
name: 'mock.customDark',
baseTheme: 'dark',
styleContent: 'mock.styleContent',
},
{
name: 'mock.customLight',
baseTheme: 'light',
styleContent: 'mock.styleContent',
},
{
name: 'mock.customUndefined',
styleContent: 'mock.styleContent',
},
],
};

const otherPlugin: DashboardPlugin = {
name: 'mock.otherPluginName',
type: 'DashboardPlugin',
component: () => null,
};

const pluginMap = new Map<string, PluginModule>([
['mock.themePluginNameA', themePluginSingleDark],
['mock.themePluginNameB', themePluginSingleLight],
['mock.themePluginNameC', themePluginMultiConfig],
['mock.otherPluginName', otherPlugin],
]);

it('should return theme data from plugins', () => {
const actual = getThemeDataFromPlugins(pluginMap);
const expected: ThemeData[] = [
{
name: 'mock.customDark',
baseThemeKey: 'default-dark',
themeKey: 'mock-themepluginnamea_mock-customdark',
styleContent: 'mock.styleContent',
},
{
name: 'mock.customLight',
baseThemeKey: 'default-light',
themeKey: 'mock-themepluginnameb_mock-customlight',
styleContent: 'mock.styleContent',
},
{
name: 'mock.customDark',
baseThemeKey: 'default-dark',
themeKey: 'mock-themepluginnamec_mock-customdark',
styleContent: 'mock.styleContent',
},
{
name: 'mock.customLight',
baseThemeKey: 'default-light',
themeKey: 'mock-themepluginnamec_mock-customlight',
styleContent: 'mock.styleContent',
},
{
name: 'mock.customUndefined',
baseThemeKey: 'default-dark',
themeKey: 'mock-themepluginnamec_mock-customundefined',
styleContent: 'mock.styleContent',
},
];

expect(actual).toEqual(expected);
});
});
57 changes: 52 additions & 5 deletions packages/app-utils/src/plugins/PluginUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getThemeKey, ThemeData } from '@deephaven/components';
import Log from '@deephaven/log';
import {
type PluginModule,
Expand All @@ -10,6 +11,8 @@ import {
PluginType,
isLegacyAuthPlugin,
isLegacyPlugin,
isThemePlugin,
ThemePlugin,
} from '@deephaven/plugin';
import loadRemoteModule from './loadRemoteModule';

Expand Down Expand Up @@ -77,19 +80,29 @@ export async function loadModulePlugins(
const pluginMainUrl = `${modulePluginsUrl}/${name}/${main}`;
pluginPromises.push(loadModulePlugin(pluginMainUrl));
}

const pluginModules = await Promise.allSettled(pluginPromises);

const pluginMap: PluginModuleMap = new Map();
for (let i = 0; i < pluginModules.length; i += 1) {
const module = pluginModules[i];
const { name } = manifest.plugins[i];
if (module.status === 'fulfilled') {
pluginMap.set(
name,
isLegacyPlugin(module.value) ? module.value : module.value.default
);
const moduleValue = isLegacyPlugin(module.value)
? module.value
: // TypeScript builds CJS default exports differently depending on
// whether there are also named exports. If the default is the only
// export, it will be the value. If there are also named exports,
// it will be assigned to the `default` property on the value.
module.value.default ?? module.value;

if (moduleValue == null) {
log.error(`Plugin '${name}' is missing an exported value.`);
} else {
pluginMap.set(name, moduleValue);
}
} else {
log.error(`Unable to load plugin ${name}`, module.reason);
log.error(`Unable to load plugin '${name}'`, module.reason);
}
}
log.info('Plugins loaded:', pluginMap);
Expand Down Expand Up @@ -161,3 +174,37 @@ export function getAuthPluginComponent(

return component;
}

/**
* Extract theme data from theme plugins in the given plugin map.
* @param pluginMap
*/
export function getThemeDataFromPlugins(
pluginMap: PluginModuleMap
): ThemeData[] {
const themePluginEntries = [...pluginMap.entries()].filter(
(entry): entry is [string, ThemePlugin] => isThemePlugin(entry[1])
);

log.debug('Getting theme data from plugins', themePluginEntries);

return themePluginEntries
.map(([pluginName, plugin]) => {
// Normalize to an array since config can be an array of configs or a
// single config
const configs = Array.isArray(plugin.themes)
? plugin.themes
: [plugin.themes];

return configs.map(
({ name, baseTheme, styleContent }) =>
({
baseThemeKey: `default-${baseTheme ?? 'dark'}`,
Comment thread
bmingles marked this conversation as resolved.
themeKey: getThemeKey(pluginName, name),
name,
styleContent,
}) as const
);
})
.flat();
}
4 changes: 2 additions & 2 deletions packages/babel-preset/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ module.exports = api => ({
'transform-rename-import',
{
// The babel-plugin-add-import-extension adds the .js to .scss imports, just convert them back to .css
original: '^(.+?)\\.s?css.js$',
replacement: '$1.css',
original: '^(.+?)\\.s?css(\\?inline)?\\.js$',
replacement: '$1.css$2',
},
],
].filter(Boolean),
Expand Down
4 changes: 3 additions & 1 deletion packages/code-studio/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import '@deephaven/components/scss/BaseStyleSheet.scss';
import { LoadingOverlay } from '@deephaven/components';
import { LoadingOverlay, preloadTheme } from '@deephaven/components';
import { ApiBootstrap } from '@deephaven/jsapi-bootstrap';
import logInit from './log/LogInit';

logInit();

preloadTheme();

// Lazy load components for code splitting and also to avoid importing the jsapi-shim before API is bootstrapped.
// eslint-disable-next-line react-refresh/only-export-components
const AppRoot = React.lazy(() => import('./AppRoot'));
Expand Down
4 changes: 2 additions & 2 deletions packages/components/scss/BaseStyleSheet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ html {

body {
min-height: 100%;
background-color: $background;
background-color: var(--dh-background-color);
color: $foreground;
margin: 0;
padding: 0;
Expand All @@ -30,7 +30,7 @@ body {
}

#root {
background-color: $background;
background-color: var(--dh-background-color);

.app {
height: 100vh;
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/EditableItemList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import classNames from 'classnames';
import clamp from 'lodash.clamp';
import { vsAdd, vsTrash } from '@deephaven/icons';
import { Range, RangeUtils } from '@deephaven/utils';
import { Button, ItemList } from '.';
import Button from './Button';
import ItemList from './ItemList';

export interface EditableItemListProps {
isInvalid?: boolean;
Expand Down
10 changes: 10 additions & 0 deletions packages/components/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@ declare module '*.module.scss' {
export default content;
}

declare module '*.css?inline' {
const content: string;
export default content;
}

declare module '*.scss?inline' {
const content: string;
export default content;
}

declare module '*.scss';
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export { default as SocketedButton } from './SocketedButton';
export * from './SpectrumUtils';
export * from './TableViewEmptyState';
export * from './TextWithTooltip';
export * from './theme';
export { default as ThemeExport } from './ThemeExport';
export { default as TimeInput } from './TimeInput';
export { default as TimeSlider } from './TimeSlider';
Expand Down
Loading