Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 16 additions & 5 deletions packages/app-utils/src/components/PluginsBootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
type Plugin,
type PluginModuleMap,
PluginsContext,
isMultiPlugin,
type MultiPlugin,
} from '@deephaven/plugin';
import { loadModulePlugins } from '../plugins';

Expand All @@ -12,8 +14,11 @@ export type PluginsBootstrapProps = {
*/
pluginsUrl: string;

/** The core plugins to load. */
getCorePlugins?: () => Promise<Plugin[]>;
/**
* The core plugins to load.
* Can include MultiPlugin instances which will be flattened.
*/
getCorePlugins?: () => Promise<(Plugin | MultiPlugin)[]>;

/**
* The children to render wrapped with the PluginsContext.
Expand All @@ -38,9 +43,15 @@ export function PluginsBootstrap({
const corePlugins = (await getCorePlugins?.()) ?? [];
const pluginModules = await loadModulePlugins(pluginsUrl);
if (!isCanceled) {
const corePluginPairs = corePlugins.map(
plugin => [plugin.name, plugin] as const
);
// Flatten MultiPlugins in core plugins
const corePluginPairs = corePlugins.flatMap(plugin => {
if (isMultiPlugin(plugin)) {
return plugin.plugins.map(
innerPlugin => [innerPlugin.name, innerPlugin] as const
);
}
return [[plugin.name, plugin] as const];
});
setPlugins(new Map([...corePluginPairs, ...pluginModules]));
}
}
Expand Down
39 changes: 38 additions & 1 deletion packages/app-utils/src/plugins/PluginUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { type LegacyPlugin, type Plugin, PluginType } from '@deephaven/plugin';
import {
type LegacyPlugin,
type MultiPlugin,
type Plugin,
PluginType,
} from '@deephaven/plugin';
import { getPluginModuleValue } from './PluginUtils';

describe('getPluginModuleValue', () => {
Expand Down Expand Up @@ -126,4 +131,36 @@ describe('getPluginModuleValue', () => {
const moduleValue = getPluginModuleValue({} as Plugin);
expect(moduleValue).toBeNull();
});

describe('MultiPlugin', () => {
const multiPlugin: MultiPlugin = {
name: 'test-multi-plugin',
type: PluginType.MULTI_PLUGIN,
plugins: [
{ name: 'widget-plugin', type: PluginType.WIDGET_PLUGIN },
{ name: 'dashboard-plugin', type: PluginType.DASHBOARD_PLUGIN },
{ name: 'theme-plugin', type: PluginType.THEME_PLUGIN },
] as Plugin[],
};

it('supports MultiPlugin format', () => {
const moduleValue = getPluginModuleValue(multiPlugin);
expect(moduleValue).toBe(multiPlugin);
});

it('supports MultiPlugin with default export', () => {
const moduleWithDefault = { default: multiPlugin };
const moduleValue = getPluginModuleValue(moduleWithDefault);
expect(moduleValue).toBe(multiPlugin);
});

it('supports MultiPlugin with named exports', () => {
const moduleWithNamedExports = {
default: multiPlugin,
SomeNamedExport: 'value',
};
const moduleValue = getPluginModuleValue(moduleWithNamedExports);
expect(moduleValue).toBe(multiPlugin);
});
});
});
32 changes: 31 additions & 1 deletion packages/app-utils/src/plugins/PluginUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isLegacyPlugin,
type PluginModule,
isPlugin,
isMultiPlugin,
} from '@deephaven/plugin';
import loadRemoteModule from './loadRemoteModule';

Expand Down Expand Up @@ -81,6 +82,27 @@ export function getPluginModuleValue(
return null;
}

/**
* Register a plugin in the plugin map, logging a warning if a plugin with the same name already exists.
* @param pluginMap The plugin map to register the plugin in
* @param name The name to register the plugin under
* @param plugin The plugin to register
* @param version Optional version to attach to the plugin
*/
function registerPlugin(
pluginMap: PluginModuleMap,
name: string,
plugin: PluginModule,
version?: string
): void {
if (pluginMap.has(name)) {
log.warn(
`Plugin '${name}' is already registered. The existing plugin will be replaced.`
);
}
pluginMap.set(name, { ...plugin, version });
}

/**
* Load all plugin modules available based on the manifest file at the provided base URL
* @param modulePluginsUrl The base URL of the module plugins to load
Expand Down Expand Up @@ -115,8 +137,16 @@ export async function loadModulePlugins(
const moduleValue = getPluginModuleValue(module.value);
if (moduleValue == null) {
log.error(`Plugin '${name}' is missing an exported value.`);
} else if (isMultiPlugin(moduleValue)) {
// Flatten MultiPlugin: register each inner plugin by its own name
log.debug(
`MultiPlugin '${name}' contains ${moduleValue.plugins.length} plugins`
);
moduleValue.plugins.forEach(innerPlugin => {
registerPlugin(pluginMap, innerPlugin.name, innerPlugin, version);
});
} else {
pluginMap.set(name, { ...moduleValue, version });
registerPlugin(pluginMap, name, moduleValue, version);
}
} else {
log.error(`Unable to load plugin '${name}'`, module.reason);
Expand Down
6 changes: 5 additions & 1 deletion packages/plugin/src/PluginTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
PluginType,
isAuthPlugin,
isDashboardPlugin,
isElementPlugin,
isMultiPlugin,
isTablePlugin,
isThemePlugin,
isWidgetPlugin,
Expand All @@ -10,8 +12,10 @@ import {
} from './PluginTypes';

const pluginTypeToTypeGuardMap = [
[PluginType.DASHBOARD_PLUGIN, isDashboardPlugin],
[PluginType.AUTH_PLUGIN, isAuthPlugin],
[PluginType.DASHBOARD_PLUGIN, isDashboardPlugin],
[PluginType.ELEMENT_PLUGIN, isElementPlugin],
[PluginType.MULTI_PLUGIN, isMultiPlugin],
[PluginType.TABLE_PLUGIN, isTablePlugin],
[PluginType.THEME_PLUGIN, isThemePlugin],
[PluginType.WIDGET_PLUGIN, isWidgetPlugin],
Expand Down
84 changes: 61 additions & 23 deletions packages/plugin/src/PluginTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import type { TablePluginComponent } from './TablePlugin';
export const PluginType = Object.freeze({
AUTH_PLUGIN: 'AuthPlugin',
DASHBOARD_PLUGIN: 'DashboardPlugin',
WIDGET_PLUGIN: 'WidgetPlugin',
ELEMENT_PLUGIN: 'ElementPlugin',
MULTI_PLUGIN: 'MultiPlugin',
TABLE_PLUGIN: 'TablePlugin',
THEME_PLUGIN: 'ThemePlugin',
ELEMENT_PLUGIN: 'ElementPlugin',
WIDGET_PLUGIN: 'WidgetPlugin',
});

/**
Expand All @@ -22,7 +23,7 @@ export const PluginType = Object.freeze({
export type LegacyDashboardPlugin = { DashboardPlugin: React.ComponentType };

export function isLegacyDashboardPlugin(
plugin: PluginModule
plugin: PluginModuleExport
): plugin is LegacyDashboardPlugin {
return 'DashboardPlugin' in plugin;
}
Expand All @@ -38,12 +39,12 @@ export type LegacyAuthPlugin = {
};

export function isLegacyAuthPlugin(
plugin: PluginModule
plugin: PluginModuleExport
): plugin is LegacyAuthPlugin {
return 'AuthPlugin' in plugin;
}

export type PluginModuleMap = Map<string, VersionedPluginModule>;
export type PluginModuleMap = Map<string, VersionedPluginModuleExport>;

/**
* @deprecated Use TablePlugin instead
Expand All @@ -53,7 +54,7 @@ export type LegacyTablePlugin = {
};

export function isLegacyTablePlugin(
plugin: PluginModule
plugin: PluginModuleExport
): plugin is LegacyTablePlugin {
return 'TablePlugin' in plugin;
}
Expand All @@ -68,15 +69,23 @@ export type LegacyPlugin =

export function isLegacyPlugin(plugin: unknown): plugin is LegacyPlugin {
return (
isLegacyDashboardPlugin(plugin as PluginModule) ||
isLegacyAuthPlugin(plugin as PluginModule) ||
isLegacyTablePlugin(plugin as PluginModule)
isLegacyDashboardPlugin(plugin as PluginModuleExport) ||
isLegacyAuthPlugin(plugin as PluginModuleExport) ||
isLegacyTablePlugin(plugin as PluginModuleExport)
);
}

export type PluginModule = Plugin | LegacyPlugin;
export type PluginModuleExport = Plugin | LegacyPlugin;

export type VersionedPluginModule = PluginModule & { version?: string };
/** @deprecated Use PluginModuleExport instead */
export type PluginModule = PluginModuleExport;

export type VersionedPluginModuleExport = PluginModuleExport & {
version?: string;
};

/** @deprecated Use VersionedPluginModuleExport instead */
export type VersionedPluginModule = VersionedPluginModuleExport;

export interface Plugin {
/**
Expand Down Expand Up @@ -104,7 +113,7 @@ export interface DashboardPlugin extends Plugin {
}

export function isDashboardPlugin(
plugin: PluginModule
plugin: PluginModuleExport
): plugin is DashboardPlugin {
return 'type' in plugin && plugin.type === PluginType.DASHBOARD_PLUGIN;
}
Expand Down Expand Up @@ -173,7 +182,9 @@ export interface WidgetPlugin<T = unknown> extends Plugin {
icon?: IconDefinition | React.ReactElement<unknown>;
}

export function isWidgetPlugin(plugin: PluginModule): plugin is WidgetPlugin {
export function isWidgetPlugin(
plugin: PluginModuleExport
): plugin is WidgetPlugin {
return 'type' in plugin && plugin.type === PluginType.WIDGET_PLUGIN;
}

Expand All @@ -182,7 +193,9 @@ export interface TablePlugin extends Plugin {
component: TablePluginComponent;
}

export function isTablePlugin(plugin: PluginModule): plugin is TablePlugin {
export function isTablePlugin(
plugin: PluginModuleExport
): plugin is TablePlugin {
return 'type' in plugin && plugin.type === PluginType.TABLE_PLUGIN;
}

Expand Down Expand Up @@ -219,7 +232,7 @@ export interface AuthPlugin extends Plugin {
isAvailable: (authHandlers: string[], authConfig: AuthConfigMap) => boolean;
}

export function isAuthPlugin(plugin: PluginModule): plugin is AuthPlugin {
export function isAuthPlugin(plugin: PluginModuleExport): plugin is AuthPlugin {
return 'type' in plugin && plugin.type === PluginType.AUTH_PLUGIN;
}

Expand All @@ -235,7 +248,9 @@ export interface ThemePlugin extends Plugin {
}

/** Type guard to check if given plugin is a `ThemePlugin` */
export function isThemePlugin(plugin: PluginModule): plugin is ThemePlugin {
export function isThemePlugin(
plugin: PluginModuleExport
): plugin is ThemePlugin {
return 'type' in plugin && plugin.type === PluginType.THEME_PLUGIN;
}

Expand Down Expand Up @@ -263,17 +278,40 @@ export interface ElementPlugin extends Plugin {
mapping: ElementPluginMappingDefinition;
}

export function isElementPlugin(plugin: PluginModule): plugin is ElementPlugin {
export function isElementPlugin(
plugin: PluginModuleExport
): plugin is ElementPlugin {
return 'type' in plugin && plugin.type === PluginType.ELEMENT_PLUGIN;
}

/**
* A plugin that contains multiple plugins.
* When loaded, each plugin in the `plugins` array will be registered individually.
*/
export interface MultiPlugin extends Plugin {
type: typeof PluginType.MULTI_PLUGIN;
/**
* The plugins to register. Each plugin will be registered by its own name.
* Note: Nested MultiPlugins are not supported.
*/
plugins: Plugin[];
}

/** Type guard to check if given plugin is a `MultiPlugin` */
export function isMultiPlugin(
plugin: PluginModuleExport
): plugin is MultiPlugin {
return 'type' in plugin && plugin.type === PluginType.MULTI_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) ||
isElementPlugin(plugin as PluginModule)
isDashboardPlugin(plugin as PluginModuleExport) ||
isAuthPlugin(plugin as PluginModuleExport) ||
isElementPlugin(plugin as PluginModuleExport) ||
isMultiPlugin(plugin as PluginModuleExport) ||
isTablePlugin(plugin as PluginModuleExport) ||
isThemePlugin(plugin as PluginModuleExport) ||
isWidgetPlugin(plugin as PluginModuleExport)
);
}
3 changes: 2 additions & 1 deletion packages/redux/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export type PluginModule =
| 'WidgetPlugin'
| 'TablePlugin'
| 'ThemePlugin'
| 'ElementPlugin';
| 'ElementPlugin'
| 'MultiPlugin';
}
| {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading