diff --git a/package-lock.json b/package-lock.json index 67bea2a3ba..4e78e866b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11590,9 +11590,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001720", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", - "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==", + "version": "1.0.30001722", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz", + "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==", "funding": [ { "type": "opencollective", @@ -41114,9 +41114,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001720", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", - "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==" + "version": "1.0.30001722", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz", + "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==" }, "canvas-fit": { "version": "1.5.0", diff --git a/packages/plugin/src/PluginTypes.ts b/packages/plugin/src/PluginTypes.ts index d19b79e61e..b1356d76b2 100644 --- a/packages/plugin/src/PluginTypes.ts +++ b/packages/plugin/src/PluginTypes.ts @@ -13,6 +13,7 @@ export const PluginType = Object.freeze({ WIDGET_PLUGIN: 'WidgetPlugin', TABLE_PLUGIN: 'TablePlugin', THEME_PLUGIN: 'ThemePlugin', + ELEMENT_PLUGIN: 'ElementPlugin', }); /** @@ -238,12 +239,35 @@ export function isThemePlugin(plugin: PluginModule): plugin is ThemePlugin { return 'type' in plugin && plugin.type === PluginType.THEME_PLUGIN; } +export type ElementName = string; + +/** A mapping of element names to their React components. */ +export type ElementPluginMappingDefinition = Record< + ElementName, + React.ComponentType +>; + +export type ElementMap = ReadonlyMap; + +/** An element plugin is used by deephaven.ui to render custom components + * The mapping contains the element names as keys and the React components as values. + */ +export interface ElementPlugin extends Plugin { + type: typeof PluginType.ELEMENT_PLUGIN; + mapping: ElementPluginMappingDefinition; +} + +export function isElementPlugin(plugin: PluginModule): plugin is ElementPlugin { + return 'type' in plugin && plugin.type === PluginType.ELEMENT_PLUGIN; +} + export function isPlugin(plugin: unknown): plugin is Plugin { return ( isDashboardPlugin(plugin as PluginModule) || isAuthPlugin(plugin as PluginModule) || isTablePlugin(plugin as PluginModule) || isThemePlugin(plugin as PluginModule) || - isWidgetPlugin(plugin as PluginModule) + isWidgetPlugin(plugin as PluginModule) || + isElementPlugin(plugin as PluginModule) ); } diff --git a/packages/plugin/src/PluginUtils.test.tsx b/packages/plugin/src/PluginUtils.test.tsx index a600411f97..0d9f6afef2 100644 --- a/packages/plugin/src/PluginUtils.test.tsx +++ b/packages/plugin/src/PluginUtils.test.tsx @@ -4,6 +4,7 @@ import { type ThemeData } from '@deephaven/components'; import { dhTruck, vsPreview } from '@deephaven/icons'; import { type DashboardPlugin, + type ElementPlugin, type PluginModule, PluginType, type ThemePlugin, @@ -13,6 +14,7 @@ import { pluginSupportsType, getIconForPlugin, getThemeDataFromPlugins, + getPluginsElementMap, } from './PluginUtils'; function TestWidget() { @@ -32,6 +34,23 @@ const dashboardPlugin: DashboardPlugin = { component: TestWidget, }; +const ElementPluginOne: ElementPlugin = { + name: 'test-element-plugin-one', + type: PluginType.ELEMENT_PLUGIN, + mapping: { + 'test-element-one': TestWidget, + 'test-element-two': TestWidget, + }, +}; + +const ElementPluginTwo: ElementPlugin = { + name: 'test-element-plugin-two', + type: PluginType.ELEMENT_PLUGIN, + mapping: { + 'test-element-three': TestWidget, + }, +}; + test('pluginSupportsType', () => { expect(pluginSupportsType(widgetPlugin, 'test-widget')).toBe(true); expect(pluginSupportsType(widgetPlugin, 'test-widget-two')).toBe(true); @@ -170,3 +189,32 @@ describe('getThemeDataFromPlugins', () => { expect(actual).toEqual(expected); }); }); + +describe('getElementPluginMap', () => { + it('should return a mapping of element plugins', () => { + const pluginMap = new Map([ + [ElementPluginOne.name, ElementPluginOne], + [ElementPluginTwo.name, ElementPluginTwo], + [dashboardPlugin.name, dashboardPlugin], + [widgetPlugin.name, widgetPlugin], + ]); + + const elementMapping = getPluginsElementMap(pluginMap); + + expect(elementMapping.size).toBe(3); + expect(elementMapping.get('test-element-one')).toBe(TestWidget); + expect(elementMapping.get('test-element-two')).toBe(TestWidget); + expect(elementMapping.get('test-element-three')).toBe(TestWidget); + }); + + it('should return an empty map if no element plugins are present', () => { + const pluginMap = new Map([ + [widgetPlugin.name, widgetPlugin], + [dashboardPlugin.name, dashboardPlugin], + ]); + + const elementMapping = getPluginsElementMap(pluginMap); + + expect(elementMapping.size).toBe(0); + }); +}); diff --git a/packages/plugin/src/PluginUtils.tsx b/packages/plugin/src/PluginUtils.tsx index 051aa3d5ed..e4ea1fa617 100644 --- a/packages/plugin/src/PluginUtils.tsx +++ b/packages/plugin/src/PluginUtils.tsx @@ -9,6 +9,9 @@ import { type PluginModuleMap, type ThemePlugin, isThemePlugin, + isElementPlugin, + type ElementPlugin, + type ElementMap, } from './PluginTypes'; const log = Log.module('@deephaven/plugin.PluginUtils'); @@ -76,3 +79,21 @@ export function getThemeDataFromPlugins( }) .flat(); } + +/** + * Get a mapping of element names to their React components from the given plugin map. + * @param pluginMap The plugin map to extract element plugins from. + * @returns A Map of element names to their React components. + */ +export function getPluginsElementMap(pluginMap: PluginModuleMap): ElementMap { + const elementPluginEntries = [...pluginMap.entries()].filter( + (entry): entry is [string, ElementPlugin] => + isElementPlugin(entry[1]) && entry[1].mapping != null + ); + + log.debug('Getting element plugin mapping', elementPluginEntries); + + return new Map( + elementPluginEntries.flatMap(([, plugin]) => Object.entries(plugin.mapping)) + ); +} diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 9fbc8b13c6..fdbdb315d3 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -8,3 +8,4 @@ export * from './usePlugins'; export * from './WidgetView'; export * from './PersistentStateContext'; export * from './usePersistentState'; +export * from './usePluginsElementMap'; diff --git a/packages/plugin/src/usePluginsElementMap.test.ts b/packages/plugin/src/usePluginsElementMap.test.ts new file mode 100644 index 0000000000..00f38d5774 --- /dev/null +++ b/packages/plugin/src/usePluginsElementMap.test.ts @@ -0,0 +1,28 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { TestUtils } from '@deephaven/test-utils'; +import type { PluginModuleMap } from './PluginTypes'; +import { usePluginsElementMap } from './usePluginsElementMap'; +import { getPluginsElementMap } from './PluginUtils'; +import { usePlugins } from './usePlugins'; + +jest.mock('./PluginUtils'); +jest.mock('./usePlugins'); + +const { asMock } = TestUtils; + +const mockElementPluginMapping = new Map< + string, + React.ComponentType +>(); + +const mockPlugins: PluginModuleMap = new Map(); + +it('should return element plugin mapping from plugins context', () => { + asMock(getPluginsElementMap).mockReturnValue(mockElementPluginMapping); + asMock(usePlugins).mockReturnValue(mockPlugins); + + const { result } = renderHook(() => usePluginsElementMap()); + + expect(getPluginsElementMap).toHaveBeenCalledWith(mockPlugins); + expect(result.current).toEqual(mockElementPluginMapping); +}); diff --git a/packages/plugin/src/usePluginsElementMap.tsx b/packages/plugin/src/usePluginsElementMap.tsx new file mode 100644 index 0000000000..f91f737d9f --- /dev/null +++ b/packages/plugin/src/usePluginsElementMap.tsx @@ -0,0 +1,22 @@ +import { useMemo } from 'react'; +import { usePlugins } from './usePlugins'; +import { getPluginsElementMap } from './PluginUtils'; +import type { ElementMap } from './PluginTypes'; + +/** + * Get all ElementPlugin elements from the plugins context + * @returns ElementPlugin mapping as a Map of plugin name to component type + */ +export function usePluginsElementMap(): ElementMap { + // Get all plugins from the context + const plugins = usePlugins(); + + const elementPlugins = useMemo( + () => getPluginsElementMap(plugins), + [plugins] + ); + + return elementPlugins; +} + +export default usePluginsElementMap;