Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 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
12 changes: 6 additions & 6 deletions package-lock.json

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

26 changes: 25 additions & 1 deletion packages/plugin/src/PluginTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const PluginType = Object.freeze({
WIDGET_PLUGIN: 'WidgetPlugin',
TABLE_PLUGIN: 'TablePlugin',
THEME_PLUGIN: 'ThemePlugin',
ELEMENT_PLUGIN: 'ElementPlugin',
});

/**
Expand Down Expand Up @@ -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<ElementName, React.ComponentType>;

/** 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)
);
}
48 changes: 48 additions & 0 deletions packages/plugin/src/PluginUtils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +14,7 @@ import {
pluginSupportsType,
getIconForPlugin,
getThemeDataFromPlugins,
getPluginsElementMap,
} from './PluginUtils';

function TestWidget() {
Expand All @@ -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);
Expand Down Expand Up @@ -170,3 +189,32 @@ describe('getThemeDataFromPlugins', () => {
expect(actual).toEqual(expected);
});
});

describe('getElementPluginMap', () => {
it('should return a mapping of element plugins', () => {
const pluginMap = new Map<string, PluginModule>([
[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<string, PluginModule>([
[widgetPlugin.name, widgetPlugin],
[dashboardPlugin.name, dashboardPlugin],
]);

const elementMapping = getPluginsElementMap(pluginMap);

expect(elementMapping.size).toBe(0);
});
});
21 changes: 21 additions & 0 deletions packages/plugin/src/PluginUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
type PluginModuleMap,
type ThemePlugin,
isThemePlugin,
isElementPlugin,
type ElementPlugin,
type ElementMap,
} from './PluginTypes';

const log = Log.module('@deephaven/plugin.PluginUtils');
Expand Down Expand Up @@ -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))
);
}
1 change: 1 addition & 0 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './usePlugins';
export * from './WidgetView';
export * from './PersistentStateContext';
export * from './usePersistentState';
export * from './usePluginsElementMap';
28 changes: 28 additions & 0 deletions packages/plugin/src/usePluginsElementMap.test.ts
Original file line number Diff line number Diff line change
@@ -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 './useElementPluginMapping';
import { getPluginsElementMap } from './PluginUtils';
import { usePlugins } from './usePlugins';

jest.mock('./PluginUtils');
jest.mock('./usePlugins');

const { asMock } = TestUtils;

const mockElementPluginMapping = new Map<
string,
React.ComponentType<unknown>
>();

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);
});
22 changes: 22 additions & 0 deletions packages/plugin/src/usePluginsElementMap.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading