From eac1baaef7769a4db221d749bfddb333d3e5f56b Mon Sep 17 00:00:00 2001 From: mikebender Date: Wed, 13 May 2026 15:46:42 -0400 Subject: [PATCH 1/3] feat: DH-21757: Allow widget plugins to register types as dashboard types - Allows widget plugins to register a type as a dashboard type, and create the dashboard payload - Avoids the current hack where the ui DashboardPlugin intercepts the panel open and then emits a dashboard creation, is more explicit about how it's handled - Also allows other plugins to potential register dashboard types --- .../src/panels/ConsolePanel.test.tsx | 99 ++++++++++++++++++- .../src/panels/ConsolePanel.tsx | 64 +++++++++++- packages/dashboard/src/PanelEvent.ts | 6 ++ packages/plugin/src/PluginTypes.test.ts | 59 +++++++++++ packages/plugin/src/PluginTypes.ts | 36 ++++++- packages/plugin/src/PluginUtils.test.tsx | 95 ++++++++++++++++++ packages/plugin/src/PluginUtils.tsx | 38 +++++++ 7 files changed, 390 insertions(+), 7 deletions(-) diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx index adc4cb1416..001181e66b 100644 --- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx +++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { render } from '@testing-library/react'; import { type CommandHistoryStorage } from '@deephaven/console'; +import { + CREATE_DASHBOARD, + PanelEvent, +} from '@deephaven/dashboard'; import type { Container, EventEmitter } from '@deephaven/golden-layout'; import type { IdeConnection, IdeSession } from '@deephaven/jsapi-types'; import { dh } from '@deephaven/jsapi-shim'; @@ -8,6 +12,12 @@ import { type SessionConfig, type SessionWrapper, } from '@deephaven/jsapi-utils'; +import { + PluginType, + type PluginModuleMap, + type WidgetDashboardPlugin, + type WidgetPlugin, +} from '@deephaven/plugin'; import { TestUtils } from '@deephaven/test-utils'; import { ConsolePanel } from './ConsolePanel'; @@ -61,16 +71,27 @@ function renderConsolePanel({ commandHistoryStorage = makeCommandHistoryStorage(), timeZone = 'MockTimeZone', sessionWrapper = makeSessionWrapper(), + plugins = new Map() as PluginModuleMap, + ref, +}: { + eventHub?: EventEmitter; + container?: Container; + commandHistoryStorage?: CommandHistoryStorage; + timeZone?: string; + sessionWrapper?: SessionWrapper; + plugins?: PluginModuleMap; + ref?: React.Ref; } = {}) { return render( ); } @@ -88,3 +109,79 @@ it('renders without crashing', () => { const { unmount } = renderConsolePanel(); unmount(); }); + +describe('openWidget', () => { + function TestWidget() { + return null; + } + + const widgetPlugin: WidgetPlugin = { + name: 'test-widget-plugin', + type: PluginType.WIDGET_PLUGIN, + component: TestWidget, + supportedTypes: 'test-widget', + }; + + const dashboardPayload = { + pluginId: 'test-widget-dashboard-plugin', + title: 'Test Dashboard', + data: { foo: 'bar' }, + }; + + const widgetDashboardPlugin: WidgetDashboardPlugin = { + name: 'test-widget-dashboard-plugin', + type: PluginType.WIDGET_PLUGIN, + component: TestWidget, + supportedTypes: 'test-widget', + dashboardTypes: 'test-dashboard', + createDashboardPayload: jest.fn(() => dashboardPayload), + }; + + it('emits CREATE_DASHBOARD when a matching widget dashboard plugin is found', () => { + const eventHub = TestUtils.createMockProxy(); + const ref = React.createRef(); + const plugins: PluginModuleMap = new Map([ + [widgetDashboardPlugin.name, widgetDashboardPlugin], + ]); + renderConsolePanel({ eventHub, plugins, ref }); + + const widget = { type: 'test-dashboard', name: 'test', title: 'Test' }; + ref.current?.openWidget(widget); + + expect(widgetDashboardPlugin.createDashboardPayload).toHaveBeenCalledWith( + widget + ); + expect(eventHub.emit).toHaveBeenCalledWith( + CREATE_DASHBOARD, + dashboardPayload + ); + expect(eventHub.emit).not.toHaveBeenCalledWith( + PanelEvent.OPEN, + expect.anything() + ); + }); + + it('emits PanelEvent.OPEN when no widget dashboard plugin matches', () => { + const eventHub = TestUtils.createMockProxy(); + const ref = React.createRef(); + const plugins: PluginModuleMap = new Map([ + [widgetPlugin.name, widgetPlugin], + [widgetDashboardPlugin.name, widgetDashboardPlugin], + ]); + renderConsolePanel({ eventHub, plugins, ref }); + + const widget = { type: 'test-widget', name: 'test', title: 'Test' }; + ref.current?.openWidget(widget); + + expect(eventHub.emit).toHaveBeenCalledWith( + PanelEvent.OPEN, + expect.objectContaining({ + widget: expect.objectContaining({ type: 'test-widget' }), + }) + ); + expect(eventHub.emit).not.toHaveBeenCalledWith( + CREATE_DASHBOARD, + expect.anything() + ); + }); +}); diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx index c8cf80777f..a8ab53d8a0 100644 --- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx @@ -15,7 +15,9 @@ import { import { type DashboardPanelProps, emitCloseDashboard, + emitCreateDashboard, emitPanelOpen, + emitPanelClose, LayoutManagerContext, LayoutUtils, PanelEvent, @@ -33,6 +35,7 @@ import { import { assertNotNull } from '@deephaven/utils'; import { getIconForPlugin, + getWidgetDashboardPlugin, pluginSupportsType, type PluginModuleMap, } from '@deephaven/plugin'; @@ -220,6 +223,19 @@ export class ConsolePanel extends PureComponent< return id; } + private deleteItemId(id: string): void { + this.setState(({ itemIds }) => { + log.debug('Delete item', id, itemIds); + const key = [...itemIds.keys()].find(k => itemIds.get(k) === id); + if (key === undefined) { + return null; + } + const newItemIds = new Map(itemIds); + newItemIds.delete(key); + return { itemIds: newItemIds }; + }); + } + handleTabFocus(): void { this.consoleRef.current?.focus(); } @@ -240,6 +256,22 @@ export class ConsolePanel extends PureComponent< } } + handlePanelClosed(panelId: string): void { + // When replacing a panel, LayoutUtils.openComponent adds the new panel before removing the old + // one, so the old panel's CLOSED event fires with the same panelId as the new panel. + // If a panel with panelId still exists, it was replaced and we should not delete the itemId. + const { glContainer } = this.props; + const root = LayoutUtils.getRootFromContainer(glContainer); + const stack = LayoutUtils.getStackForConfig(root, { id: panelId }); + if ( + stack !== null && + LayoutUtils.getContentItemInStack(stack, { id: panelId }) !== null + ) { + return; + } + this.deleteItemId(panelId); + } + handleFocusCommandHistory(): void { const { glEventHub } = this.props; glEventHub.emit(ConsoleEvent.FOCUS_HISTORY); @@ -282,7 +314,7 @@ export class ConsolePanel extends PureComponent< const id = this.getItemId(title, false); if (id != null) { const { glEventHub } = this.props; - glEventHub.emit(PanelEvent.CLOSE, id); + emitPanelClose(glEventHub, id); // Just emit for all panels since there shouldn't be dashboard and panel name conflicts emitCloseDashboard(glEventHub, title); } @@ -302,23 +334,45 @@ export class ConsolePanel extends PureComponent< * @param widget The widget to open */ openWidget(widget: dh.ide.VariableDescriptor & { title?: string }): void { + const { glEventHub, plugins } = this.props; + const { type } = widget; + const widgetDashboardPlugin = getWidgetDashboardPlugin(plugins, type); + if (widgetDashboardPlugin != null) { + // If it can be opened as a dashboard, open it as a dashboard. Otherwise, open it as a panel. + const dashboardPayload = + widgetDashboardPlugin.createDashboardPayload(widget); + log.debug('openDashboardWidget', dashboardPayload); + emitCreateDashboard(glEventHub, dashboardPayload); + } else { + this.openPanelWidget(widget); + } + } + + /** + * Open the given widget in a new panel. This is used for all widget types except "widget dashboard" types, which are widgets that should be opened in a new dashboard instead of a panel. + * @param widget The widget to open + */ + private openPanelWidget( + widget: dh.ide.VariableDescriptor & { title?: string } + ): void { const { glEventHub, sessionWrapper } = this.props; assertNotNull(sessionWrapper); const { config, session } = sessionWrapper; const { title = widget.name } = widget; assertNotNull(title); - const panelId = this.getItemId(title); + const itemId = this.getItemId(title); + assertNotNull(itemId); const openOptions = { fetch: () => session.getObject(widget), - panelId, + panelId: itemId, widget: { ...getVariableDescriptor(widget), sessionId: config.id, }, }; - log.debug('openWidget', openOptions); + log.debug('openPanelWidget', openOptions); emitPanelOpen(glEventHub, openOptions); } @@ -341,7 +395,7 @@ export class ConsolePanel extends PureComponent< const panelIdsToClose = [...itemIds.values()]; const { glEventHub } = this.props; panelIdsToClose.forEach(panelId => { - glEventHub.emit(PanelEvent.CLOSE, panelId); + emitPanelClose(glEventHub, panelId); }); this.setState({ itemIds: new Map() }); diff --git a/packages/dashboard/src/PanelEvent.ts b/packages/dashboard/src/PanelEvent.ts index 82e7354182..d5a6a3d4ed 100644 --- a/packages/dashboard/src/PanelEvent.ts +++ b/packages/dashboard/src/PanelEvent.ts @@ -78,6 +78,12 @@ export const { useListener: usePanelOpenListener, } = makeEventFunctions<[detail: PanelOpenEventDetail]>(PanelEvent.OPEN); +export const { + listen: listenForPanelClose, + emit: emitPanelClose, + useListener: usePanelCloseListener, +} = makeEventFunctions<[panelId: string]>(PanelEvent.CLOSE); + // TODO (#2147): Add the rest of the event functions here. Need to create the correct types for all of them. export default PanelEvent; diff --git a/packages/plugin/src/PluginTypes.test.ts b/packages/plugin/src/PluginTypes.test.ts index 1de2a2fd69..d2121c1568 100644 --- a/packages/plugin/src/PluginTypes.test.ts +++ b/packages/plugin/src/PluginTypes.test.ts @@ -11,9 +11,29 @@ import { type Plugin, type WidgetPlugin, type WidgetMiddlewarePlugin, + isWidgetDashboardPlugin, + type WidgetDashboardPlugin, isPlugin, } from './PluginTypes'; +function TestComponent() { + return null; +} + +const widgetPlugin: WidgetPlugin = { + name: 'test-widget-plugin', + type: PluginType.WIDGET_PLUGIN, + component: TestComponent, + supportedTypes: 'test-widget', +}; + +const widgetDashboardPlugin: WidgetDashboardPlugin = { + ...widgetPlugin, + name: 'test-widget-dashboard-plugin', + dashboardTypes: 'test-dashboard', + createDashboardPayload: jest.fn(), +}; + const pluginTypeToTypeGuardMap = [ [PluginType.AUTH_PLUGIN, isAuthPlugin], [PluginType.DASHBOARD_PLUGIN, isDashboardPlugin], @@ -88,3 +108,42 @@ describe('isWidgetMiddlewarePlugin', () => { ).toBe(false); }); }); + +describe('isWidgetDashboardPlugin', () => { + it('returns true for a widget plugin with dashboardTypes set', () => { + expect(isWidgetDashboardPlugin(widgetDashboardPlugin)).toBe(true); + }); + + it('returns true when dashboardTypes is an array', () => { + expect( + isWidgetDashboardPlugin({ + ...widgetDashboardPlugin, + dashboardTypes: ['test-dashboard', 'test-dashboard-two'], + }) + ).toBe(true); + }); + + it('returns false for a widget plugin without dashboardTypes', () => { + expect(isWidgetDashboardPlugin(widgetPlugin)).toBe(false); + }); + + it('returns false for non-widget plugins', () => { + expect( + isWidgetDashboardPlugin({ + name: 'test', + type: PluginType.DASHBOARD_PLUGIN, + component: TestComponent, + }) + ).toBe(false); + }); + + it('returns false when dashboardTypes is null', () => { + expect( + isWidgetDashboardPlugin({ + ...widgetPlugin, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dashboardTypes: null as any, + }) + ).toBe(false); + }); +}); diff --git a/packages/plugin/src/PluginTypes.ts b/packages/plugin/src/PluginTypes.ts index bc1972422f..b9819d58d7 100644 --- a/packages/plugin/src/PluginTypes.ts +++ b/packages/plugin/src/PluginTypes.ts @@ -1,5 +1,8 @@ import type { BaseThemeType } from '@deephaven/components'; -import type { WidgetDescriptor } from '@deephaven/dashboard'; +import type { + CreateDashboardPayload, + WidgetDescriptor, +} from '@deephaven/dashboard'; import { type EventEmitter, type ItemContainer, @@ -261,12 +264,43 @@ export interface WidgetPlugin extends Plugin { icon?: IconDefinition | React.ReactElement; } +/** + * Special type of WidgetPlugin that supports opening a widget as a dashboard instead of a panel. + */ +export interface WidgetDashboardPlugin extends WidgetPlugin { + /** + * The widget dashboard types that this plugin can handle. + * Widgets of these types can be opened by this plugin as a dashboard using the `createDashboardPayload` function. + * Can overlap with `supportedTypes` for widgets that can be opened as either a panel or a dashboard (e.g. nested dashboards). + */ + dashboardTypes: string | string[]; + + /** + * A function to generate the dashboard payload for creating a new dashboard when a widget of widget is opened. + * @param widget Widget to get the create dashboard payload for + * @returns The dashboard payload for creating a new dashboard + */ + createDashboardPayload( + widget: D + ): CreateDashboardPayload; +} + export function isWidgetPlugin( plugin: PluginModuleExport ): plugin is WidgetPlugin { return 'type' in plugin && plugin.type === PluginType.WIDGET_PLUGIN; } +export function isWidgetDashboardPlugin( + plugin: PluginModuleExport +): plugin is WidgetDashboardPlugin { + return ( + isWidgetPlugin(plugin) && + 'dashboardTypes' in plugin && + plugin.dashboardTypes != null + ); +} + export interface TablePlugin extends Plugin { type: typeof PluginType.TABLE_PLUGIN; component: TablePluginComponent; diff --git a/packages/plugin/src/PluginUtils.test.tsx b/packages/plugin/src/PluginUtils.test.tsx index 14fc57ccc3..bd8554803e 100644 --- a/packages/plugin/src/PluginUtils.test.tsx +++ b/packages/plugin/src/PluginUtils.test.tsx @@ -14,22 +14,31 @@ import { PluginType, type ThemePlugin, type WidgetPlugin, +<<<<<<< HEAD type WidgetMiddlewarePlugin, type WidgetComponentProps, type WidgetMiddlewareComponentProps, type WidgetPanelProps, type WidgetMiddlewarePanelProps, +======= + type WidgetDashboardPlugin, +>>>>>>> fdfefce4e1 (feat: DH-21757: Allow widget plugins to register types as dashboard types) } from './PluginTypes'; import { pluginSupportsType, + pluginSupportsTypeAsDashboard, getIconForPlugin, getThemeDataFromPlugins, getPluginsElementMap, +<<<<<<< HEAD getPluginModuleValue, processLoadedModule, registerPlugin, createChainedComponent, createChainedPanelComponent, +======= + getWidgetDashboardPlugin, +>>>>>>> fdfefce4e1 (feat: DH-21757: Allow widget plugins to register types as dashboard types) } from './PluginUtils'; function TestWidget() { @@ -74,6 +83,92 @@ test('pluginSupportsType', () => { expect(pluginSupportsType(undefined, 'test-widget')).toBe(false); }); +const widgetDashboardPlugin: WidgetDashboardPlugin = { + name: 'test-widget-dashboard-plugin', + type: PluginType.WIDGET_PLUGIN, + component: TestWidget, + supportedTypes: ['test-widget'], + dashboardTypes: ['test-dashboard', 'test-dashboard-two'], + createDashboardPayload: jest.fn(), +}; + +const widgetDashboardPluginSingleType: WidgetDashboardPlugin = { + ...widgetDashboardPlugin, + name: 'test-widget-dashboard-plugin-single', + dashboardTypes: 'test-dashboard-single', +}; + +describe('pluginSupportsTypeAsDashboard', () => { + it('returns true for matching dashboard types', () => { + expect( + pluginSupportsTypeAsDashboard(widgetDashboardPlugin, 'test-dashboard') + ).toBe(true); + expect( + pluginSupportsTypeAsDashboard(widgetDashboardPlugin, 'test-dashboard-two') + ).toBe(true); + expect( + pluginSupportsTypeAsDashboard( + widgetDashboardPluginSingleType, + 'test-dashboard-single' + ) + ).toBe(true); + }); + + it('returns false for non-matching dashboard types', () => { + expect( + pluginSupportsTypeAsDashboard(widgetDashboardPlugin, 'unknown') + ).toBe(false); + // supportedTypes should not satisfy dashboard types + expect( + pluginSupportsTypeAsDashboard(widgetDashboardPlugin, 'test-widget') + ).toBe(false); + }); + + it('returns false for non-widget-dashboard plugins', () => { + expect(pluginSupportsTypeAsDashboard(widgetPlugin, 'test-widget')).toBe( + false + ); + expect( + pluginSupportsTypeAsDashboard(dashboardPlugin, 'test-dashboard') + ).toBe(false); + expect(pluginSupportsTypeAsDashboard(undefined, 'test-dashboard')).toBe( + false + ); + }); +}); + +describe('getWidgetDashboardPlugin', () => { + const pluginMap = new Map([ + [widgetPlugin.name, widgetPlugin], + [dashboardPlugin.name, dashboardPlugin], + [widgetDashboardPlugin.name, widgetDashboardPlugin], + [widgetDashboardPluginSingleType.name, widgetDashboardPluginSingleType], + ]); + + it('returns the plugin matching the dashboard type', () => { + expect(getWidgetDashboardPlugin(pluginMap, 'test-dashboard')).toBe( + widgetDashboardPlugin + ); + expect(getWidgetDashboardPlugin(pluginMap, 'test-dashboard-two')).toBe( + widgetDashboardPlugin + ); + expect(getWidgetDashboardPlugin(pluginMap, 'test-dashboard-single')).toBe( + widgetDashboardPluginSingleType + ); + }); + + it('returns null when no plugin matches', () => { + expect(getWidgetDashboardPlugin(pluginMap, 'test-widget')).toBeNull(); + expect(getWidgetDashboardPlugin(pluginMap, 'unknown')).toBeNull(); + }); + + it('returns null for an empty plugin map', () => { + expect( + getWidgetDashboardPlugin(new Map(), 'test-dashboard') + ).toBeNull(); + }); +}); + const DEFAULT_ICON = ; describe('getIconForPlugin', () => { diff --git a/packages/plugin/src/PluginUtils.tsx b/packages/plugin/src/PluginUtils.tsx index abaa906778..682fd1d7fb 100644 --- a/packages/plugin/src/PluginUtils.tsx +++ b/packages/plugin/src/PluginUtils.tsx @@ -21,6 +21,8 @@ import { isPlugin, type LegacyPlugin, type Plugin, + type WidgetDashboardPlugin, + isWidgetDashboardPlugin, } from './PluginTypes'; const log = Log.module('@deephaven/plugin.PluginUtils'); @@ -48,6 +50,23 @@ export function pluginSupportsType( return [plugin.supportedTypes].flat().some(t => t === type); } +/** + * Check if the given plugin supports opening a widget of the given type as a dashboard. + * @param plugin Plugin to check + * @param type Widget type + * @returns True if plugin supports opening it as a dashboard + */ +export function pluginSupportsTypeAsDashboard( + plugin: PluginModule | undefined, + type: string +): boolean { + if (plugin == null || !isWidgetDashboardPlugin(plugin)) { + return false; + } + + return [plugin.dashboardTypes].flat().some(t => t === type); +} + export function getIconForPlugin(plugin: PluginModule): React.ReactElement { const defaultIcon = ; if (!isWidgetPlugin(plugin)) { @@ -490,3 +509,22 @@ export function sortPluginsByDependency< return sorted; } + +/** + * Get the WidgetPlugin that supports opening a widget of the given type as a dashboard. + * @param pluginMap Map of available plugins + * @param type Type of widget to check + * @returns The WidgetPlugin that supports opening this widget as a dashboard + */ +export function getWidgetDashboardPlugin( + pluginMap: PluginModuleMap, + type: string +): WidgetDashboardPlugin | null { + return ( + [...pluginMap.values()].find( + (entry): entry is WidgetDashboardPlugin => + isWidgetDashboardPlugin(entry) && + [entry.dashboardTypes].flat().some(t => t === type) + ) ?? null + ); +} From ff55996ce50e0f9c53f04201bf9b9681f0b0d5d7 Mon Sep 17 00:00:00 2001 From: mikebender Date: Thu, 14 May 2026 14:48:36 -0400 Subject: [PATCH 2/3] Cleanup based on review --- .../dashboard-core-plugins/src/panels/ConsolePanel.tsx | 10 ++++++++++ packages/plugin/src/PluginUtils.test.tsx | 10 +--------- packages/plugin/src/PluginUtils.tsx | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx index a8ab53d8a0..f3e3e5cdd5 100644 --- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx @@ -223,6 +223,10 @@ export class ConsolePanel extends PureComponent< return id; } + /** + * Removes the item ID mapping from state by its ID value. + * @param id The item ID to delete from the itemIds map. + */ private deleteItemId(id: string): void { this.setState(({ itemIds }) => { log.debug('Delete item', id, itemIds); @@ -256,6 +260,12 @@ export class ConsolePanel extends PureComponent< } } + /** + * Handles a panel closed event. Removes the panel's item ID mapping unless + * the panel was replaced (i.e. a panel with the same ID still exists in the layout), + * in which case the mapping is retained for the replacement panel. + * @param panelId The ID of the panel that was closed. + */ handlePanelClosed(panelId: string): void { // When replacing a panel, LayoutUtils.openComponent adds the new panel before removing the old // one, so the old panel's CLOSED event fires with the same panelId as the new panel. diff --git a/packages/plugin/src/PluginUtils.test.tsx b/packages/plugin/src/PluginUtils.test.tsx index bd8554803e..3bfc79b345 100644 --- a/packages/plugin/src/PluginUtils.test.tsx +++ b/packages/plugin/src/PluginUtils.test.tsx @@ -14,15 +14,12 @@ import { PluginType, type ThemePlugin, type WidgetPlugin, -<<<<<<< HEAD type WidgetMiddlewarePlugin, type WidgetComponentProps, type WidgetMiddlewareComponentProps, type WidgetPanelProps, type WidgetMiddlewarePanelProps, -======= type WidgetDashboardPlugin, ->>>>>>> fdfefce4e1 (feat: DH-21757: Allow widget plugins to register types as dashboard types) } from './PluginTypes'; import { pluginSupportsType, @@ -30,15 +27,12 @@ import { getIconForPlugin, getThemeDataFromPlugins, getPluginsElementMap, -<<<<<<< HEAD getPluginModuleValue, processLoadedModule, registerPlugin, createChainedComponent, createChainedPanelComponent, -======= getWidgetDashboardPlugin, ->>>>>>> fdfefce4e1 (feat: DH-21757: Allow widget plugins to register types as dashboard types) } from './PluginUtils'; function TestWidget() { @@ -163,9 +157,7 @@ describe('getWidgetDashboardPlugin', () => { }); it('returns null for an empty plugin map', () => { - expect( - getWidgetDashboardPlugin(new Map(), 'test-dashboard') - ).toBeNull(); + expect(getWidgetDashboardPlugin(new Map(), 'test-dashboard')).toBeNull(); }); }); diff --git a/packages/plugin/src/PluginUtils.tsx b/packages/plugin/src/PluginUtils.tsx index 682fd1d7fb..c0c79a8a20 100644 --- a/packages/plugin/src/PluginUtils.tsx +++ b/packages/plugin/src/PluginUtils.tsx @@ -514,7 +514,7 @@ export function sortPluginsByDependency< * Get the WidgetPlugin that supports opening a widget of the given type as a dashboard. * @param pluginMap Map of available plugins * @param type Type of widget to check - * @returns The WidgetPlugin that supports opening this widget as a dashboard + * @returns The WidgetPlugin that supports opening this widget as a dashboard, or null if no matching plugin is found */ export function getWidgetDashboardPlugin( pluginMap: PluginModuleMap, From 1e8a0646b70643b2329e08ac86b023285d3706b0 Mon Sep 17 00:00:00 2001 From: mikebender Date: Thu, 14 May 2026 16:46:49 -0400 Subject: [PATCH 3/3] Clean up failing tests --- .../dashboard-core-plugins/src/panels/ConsolePanel.test.tsx | 5 +---- packages/plugin/src/PluginTypes.ts | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx index 001181e66b..b91b2eba00 100644 --- a/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx +++ b/packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx @@ -1,10 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { type CommandHistoryStorage } from '@deephaven/console'; -import { - CREATE_DASHBOARD, - PanelEvent, -} from '@deephaven/dashboard'; +import { CREATE_DASHBOARD, PanelEvent } from '@deephaven/dashboard'; import type { Container, EventEmitter } from '@deephaven/golden-layout'; import type { IdeConnection, IdeSession } from '@deephaven/jsapi-types'; import { dh } from '@deephaven/jsapi-shim'; diff --git a/packages/plugin/src/PluginTypes.ts b/packages/plugin/src/PluginTypes.ts index b9819d58d7..5f31e43556 100644 --- a/packages/plugin/src/PluginTypes.ts +++ b/packages/plugin/src/PluginTypes.ts @@ -280,9 +280,7 @@ export interface WidgetDashboardPlugin extends WidgetPlugin { * @param widget Widget to get the create dashboard payload for * @returns The dashboard payload for creating a new dashboard */ - createDashboardPayload( - widget: D - ): CreateDashboardPayload; + createDashboardPayload: (widget: WidgetDescriptor) => CreateDashboardPayload; } export function isWidgetPlugin(