Skip to content

Commit bc16dbd

Browse files
authored
feat: DH-19146: ElementPlugin mapping hook (#2477)
Will also need deephaven/deephaven-plugins#1201 for the plugin side, but this implements a hook that pulls the necessary mapping for element plugins
1 parent 7785090 commit bc16dbd

File tree

7 files changed

+151
-7
lines changed

7 files changed

+151
-7
lines changed

package-lock.json

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

packages/plugin/src/PluginTypes.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const PluginType = Object.freeze({
1313
WIDGET_PLUGIN: 'WidgetPlugin',
1414
TABLE_PLUGIN: 'TablePlugin',
1515
THEME_PLUGIN: 'ThemePlugin',
16+
ELEMENT_PLUGIN: 'ElementPlugin',
1617
});
1718

1819
/**
@@ -238,12 +239,35 @@ export function isThemePlugin(plugin: PluginModule): plugin is ThemePlugin {
238239
return 'type' in plugin && plugin.type === PluginType.THEME_PLUGIN;
239240
}
240241

242+
export type ElementName = string;
243+
244+
/** A mapping of element names to their React components. */
245+
export type ElementPluginMappingDefinition = Record<
246+
ElementName,
247+
React.ComponentType
248+
>;
249+
250+
export type ElementMap = ReadonlyMap<ElementName, React.ComponentType>;
251+
252+
/** An element plugin is used by deephaven.ui to render custom components
253+
* The mapping contains the element names as keys and the React components as values.
254+
*/
255+
export interface ElementPlugin extends Plugin {
256+
type: typeof PluginType.ELEMENT_PLUGIN;
257+
mapping: ElementPluginMappingDefinition;
258+
}
259+
260+
export function isElementPlugin(plugin: PluginModule): plugin is ElementPlugin {
261+
return 'type' in plugin && plugin.type === PluginType.ELEMENT_PLUGIN;
262+
}
263+
241264
export function isPlugin(plugin: unknown): plugin is Plugin {
242265
return (
243266
isDashboardPlugin(plugin as PluginModule) ||
244267
isAuthPlugin(plugin as PluginModule) ||
245268
isTablePlugin(plugin as PluginModule) ||
246269
isThemePlugin(plugin as PluginModule) ||
247-
isWidgetPlugin(plugin as PluginModule)
270+
isWidgetPlugin(plugin as PluginModule) ||
271+
isElementPlugin(plugin as PluginModule)
248272
);
249273
}

packages/plugin/src/PluginUtils.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type ThemeData } from '@deephaven/components';
44
import { dhTruck, vsPreview } from '@deephaven/icons';
55
import {
66
type DashboardPlugin,
7+
type ElementPlugin,
78
type PluginModule,
89
PluginType,
910
type ThemePlugin,
@@ -13,6 +14,7 @@ import {
1314
pluginSupportsType,
1415
getIconForPlugin,
1516
getThemeDataFromPlugins,
17+
getPluginsElementMap,
1618
} from './PluginUtils';
1719

1820
function TestWidget() {
@@ -32,6 +34,23 @@ const dashboardPlugin: DashboardPlugin = {
3234
component: TestWidget,
3335
};
3436

37+
const ElementPluginOne: ElementPlugin = {
38+
name: 'test-element-plugin-one',
39+
type: PluginType.ELEMENT_PLUGIN,
40+
mapping: {
41+
'test-element-one': TestWidget,
42+
'test-element-two': TestWidget,
43+
},
44+
};
45+
46+
const ElementPluginTwo: ElementPlugin = {
47+
name: 'test-element-plugin-two',
48+
type: PluginType.ELEMENT_PLUGIN,
49+
mapping: {
50+
'test-element-three': TestWidget,
51+
},
52+
};
53+
3554
test('pluginSupportsType', () => {
3655
expect(pluginSupportsType(widgetPlugin, 'test-widget')).toBe(true);
3756
expect(pluginSupportsType(widgetPlugin, 'test-widget-two')).toBe(true);
@@ -170,3 +189,32 @@ describe('getThemeDataFromPlugins', () => {
170189
expect(actual).toEqual(expected);
171190
});
172191
});
192+
193+
describe('getElementPluginMap', () => {
194+
it('should return a mapping of element plugins', () => {
195+
const pluginMap = new Map<string, PluginModule>([
196+
[ElementPluginOne.name, ElementPluginOne],
197+
[ElementPluginTwo.name, ElementPluginTwo],
198+
[dashboardPlugin.name, dashboardPlugin],
199+
[widgetPlugin.name, widgetPlugin],
200+
]);
201+
202+
const elementMapping = getPluginsElementMap(pluginMap);
203+
204+
expect(elementMapping.size).toBe(3);
205+
expect(elementMapping.get('test-element-one')).toBe(TestWidget);
206+
expect(elementMapping.get('test-element-two')).toBe(TestWidget);
207+
expect(elementMapping.get('test-element-three')).toBe(TestWidget);
208+
});
209+
210+
it('should return an empty map if no element plugins are present', () => {
211+
const pluginMap = new Map<string, PluginModule>([
212+
[widgetPlugin.name, widgetPlugin],
213+
[dashboardPlugin.name, dashboardPlugin],
214+
]);
215+
216+
const elementMapping = getPluginsElementMap(pluginMap);
217+
218+
expect(elementMapping.size).toBe(0);
219+
});
220+
});

packages/plugin/src/PluginUtils.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
type PluginModuleMap,
1010
type ThemePlugin,
1111
isThemePlugin,
12+
isElementPlugin,
13+
type ElementPlugin,
14+
type ElementMap,
1215
} from './PluginTypes';
1316

1417
const log = Log.module('@deephaven/plugin.PluginUtils');
@@ -76,3 +79,21 @@ export function getThemeDataFromPlugins(
7679
})
7780
.flat();
7881
}
82+
83+
/**
84+
* Get a mapping of element names to their React components from the given plugin map.
85+
* @param pluginMap The plugin map to extract element plugins from.
86+
* @returns A Map of element names to their React components.
87+
*/
88+
export function getPluginsElementMap(pluginMap: PluginModuleMap): ElementMap {
89+
const elementPluginEntries = [...pluginMap.entries()].filter(
90+
(entry): entry is [string, ElementPlugin] =>
91+
isElementPlugin(entry[1]) && entry[1].mapping != null
92+
);
93+
94+
log.debug('Getting element plugin mapping', elementPluginEntries);
95+
96+
return new Map(
97+
elementPluginEntries.flatMap(([, plugin]) => Object.entries(plugin.mapping))
98+
);
99+
}

packages/plugin/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './usePlugins';
88
export * from './WidgetView';
99
export * from './PersistentStateContext';
1010
export * from './usePersistentState';
11+
export * from './usePluginsElementMap';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import { TestUtils } from '@deephaven/test-utils';
3+
import type { PluginModuleMap } from './PluginTypes';
4+
import { usePluginsElementMap } from './usePluginsElementMap';
5+
import { getPluginsElementMap } from './PluginUtils';
6+
import { usePlugins } from './usePlugins';
7+
8+
jest.mock('./PluginUtils');
9+
jest.mock('./usePlugins');
10+
11+
const { asMock } = TestUtils;
12+
13+
const mockElementPluginMapping = new Map<
14+
string,
15+
React.ComponentType<unknown>
16+
>();
17+
18+
const mockPlugins: PluginModuleMap = new Map();
19+
20+
it('should return element plugin mapping from plugins context', () => {
21+
asMock(getPluginsElementMap).mockReturnValue(mockElementPluginMapping);
22+
asMock(usePlugins).mockReturnValue(mockPlugins);
23+
24+
const { result } = renderHook(() => usePluginsElementMap());
25+
26+
expect(getPluginsElementMap).toHaveBeenCalledWith(mockPlugins);
27+
expect(result.current).toEqual(mockElementPluginMapping);
28+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useMemo } from 'react';
2+
import { usePlugins } from './usePlugins';
3+
import { getPluginsElementMap } from './PluginUtils';
4+
import type { ElementMap } from './PluginTypes';
5+
6+
/**
7+
* Get all ElementPlugin elements from the plugins context
8+
* @returns ElementPlugin mapping as a Map of plugin name to component type
9+
*/
10+
export function usePluginsElementMap(): ElementMap {
11+
// Get all plugins from the context
12+
const plugins = usePlugins();
13+
14+
const elementPlugins = useMemo(
15+
() => getPluginsElementMap(plugins),
16+
[plugins]
17+
);
18+
19+
return elementPlugins;
20+
}
21+
22+
export default usePluginsElementMap;

0 commit comments

Comments
 (0)