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
96 changes: 95 additions & 1 deletion packages/dashboard-core-plugins/src/panels/ConsolePanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
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';
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';

Expand Down Expand Up @@ -61,16 +68,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<ConsolePanel>;
} = {}) {
return render(
<ConsolePanel
ref={ref}
glEventHub={eventHub}
glContainer={container}
commandHistoryStorage={commandHistoryStorage}
timeZone={timeZone}
sessionWrapper={sessionWrapper}
localDashboardId="mock-localDashboardId"
plugins={new Map()}
plugins={plugins}
/>
);
}
Expand All @@ -88,3 +106,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<EventEmitter>();
const ref = React.createRef<ConsolePanel>();
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<EventEmitter>();
const ref = React.createRef<ConsolePanel>();
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()
);
});
});
74 changes: 69 additions & 5 deletions packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
import {
type DashboardPanelProps,
emitCloseDashboard,
emitCreateDashboard,
emitPanelOpen,
emitPanelClose,
LayoutManagerContext,
LayoutUtils,
PanelEvent,
Expand All @@ -33,6 +35,7 @@ import {
import { assertNotNull } from '@deephaven/utils';
import {
getIconForPlugin,
getWidgetDashboardPlugin,
pluginSupportsType,
type PluginModuleMap,
} from '@deephaven/plugin';
Expand Down Expand Up @@ -220,6 +223,23 @@ 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 {
Comment thread
mofojed marked this conversation as resolved.
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();
}
Expand All @@ -240,6 +260,28 @@ 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.
// 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);
Expand Down Expand Up @@ -282,7 +324,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);
}
Expand All @@ -302,23 +344,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);
}
Expand All @@ -341,7 +405,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() });
Expand Down
6 changes: 6 additions & 0 deletions packages/dashboard/src/PanelEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
59 changes: 59 additions & 0 deletions packages/plugin/src/PluginTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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);
});
});
Loading
Loading