Skip to content

Commit d2c6eab

Browse files
authored
fix: Reuse dashboard tabs when reassigning the variable (#2243)
Fixes #1971 Tested with this code run 1 line at a time ```python import deephaven.ui as ui a = ui.dashboard(ui.text("Hello")) a = ui.dashboard(ui.text("World")) a = ui.text("Hello") a = ui.dashboard(ui.text("Hello")) del a ``` Also tested dashboards still load in embed-widget. There was a race condition since the `makeUseListenerFunction` hook was using `useEffect`. This caused the event to emit between the event hub being set and the listener being added.
1 parent 91bd8fe commit d2c6eab

5 files changed

Lines changed: 95 additions & 76 deletions

File tree

packages/code-studio/src/main/AppMainContainer.tsx

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ import {
3434
getAllDashboardsData,
3535
getDashboardData,
3636
listenForCreateDashboard,
37+
listenForCloseDashboard,
3738
PanelEvent,
3839
setDashboardData as setDashboardDataAction,
3940
setDashboardPluginData as setDashboardPluginDataAction,
40-
stopListenForCreateDashboard,
4141
updateDashboardData as updateDashboardDataAction,
4242
} from '@deephaven/dashboard';
4343
import {
@@ -189,6 +189,8 @@ export class AppMainContainer extends Component<
189189
const { allDashboardData } = this.props;
190190

191191
this.dashboardLayouts = new Map();
192+
this.createDashboardListenerRemovers = new Map();
193+
this.closeDashboardListenerRemovers = new Map();
192194

193195
this.state = {
194196
contextActions: [
@@ -275,9 +277,8 @@ export class AppMainContainer extends Component<
275277
componentWillUnmount(): void {
276278
this.deinitWidgets();
277279

278-
this.dashboardLayouts.forEach(layout => {
279-
stopListenForCreateDashboard(layout.eventHub, this.handleCreateDashboard);
280-
});
280+
this.createDashboardListenerRemovers.forEach(rm => rm());
281+
this.closeDashboardListenerRemovers.forEach(rm => rm());
281282

282283
window.removeEventListener(
283284
'beforeunload',
@@ -288,6 +289,12 @@ export class AppMainContainer extends Component<
288289
/** Map from the dashboard ID to the GoldenLayout instance for that dashboard */
289290
dashboardLayouts: Map<string, GoldenLayout>;
290291

292+
/** Map from dashboard ID to remover functions for create dashboard listener */
293+
createDashboardListenerRemovers: Map<string, () => void>;
294+
295+
/** Map from dashboard ID to remover functions for close dashboard listener */
296+
closeDashboardListenerRemovers: Map<string, () => void>;
297+
291298
importElement: RefObject<HTMLInputElement>;
292299

293300
widgetListenerRemover?: () => void;
@@ -504,16 +511,21 @@ export class AppMainContainer extends Component<
504511
const oldLayout = this.dashboardLayouts.get(activeTabKey);
505512
if (oldLayout === newLayout) return;
506513

514+
this.dashboardLayouts.set(activeTabKey, newLayout);
515+
507516
if (oldLayout != null) {
508-
stopListenForCreateDashboard(
509-
oldLayout.eventHub,
510-
this.handleCreateDashboard
511-
);
517+
this.createDashboardListenerRemovers.get(activeTabKey)?.();
518+
this.closeDashboardListenerRemovers.get(activeTabKey)?.();
512519
}
513520

514-
this.dashboardLayouts.set(activeTabKey, newLayout);
515-
516-
listenForCreateDashboard(newLayout.eventHub, this.handleCreateDashboard);
521+
this.createDashboardListenerRemovers.set(
522+
activeTabKey,
523+
listenForCreateDashboard(newLayout.eventHub, this.handleCreateDashboard)
524+
);
525+
this.closeDashboardListenerRemovers.set(
526+
activeTabKey,
527+
listenForCloseDashboard(newLayout.eventHub, this.handleCloseDashboard)
528+
);
517529
}
518530

519531
handleCreateDashboard({
@@ -524,16 +536,38 @@ export class AppMainContainer extends Component<
524536
const newId = nanoid();
525537
const { setDashboardPluginData } = this.props;
526538
setDashboardPluginData(newId, pluginId, data);
527-
this.setState(({ tabs }) => ({
528-
tabs: [
529-
...tabs,
530-
{
531-
key: newId,
532-
title,
533-
},
534-
],
535-
activeTabKey: newId,
536-
}));
539+
this.setState(({ tabs }) => {
540+
const existingTab = tabs.findIndex(
541+
({ title: tabTitle }) => tabTitle === title
542+
);
543+
if (existingTab !== -1) {
544+
// Replace the existing tab
545+
return {
546+
tabs: tabs.map((tab, index) =>
547+
index === existingTab ? { key: newId, title } : tab
548+
),
549+
activeTabKey: newId,
550+
};
551+
}
552+
return {
553+
tabs: [
554+
...tabs,
555+
{
556+
key: newId,
557+
title,
558+
},
559+
],
560+
activeTabKey: newId,
561+
};
562+
});
563+
}
564+
565+
handleCloseDashboard(title: string): void {
566+
const { tabs } = this.state;
567+
const tab = tabs.find(t => t.title === title);
568+
if (tab != null) {
569+
this.handleTabClose(tab.key);
570+
}
537571
}
538572

539573
handleWidgetMenuClick(): void {

packages/dashboard-core-plugins/src/panels/ConsolePanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@deephaven/console';
1515
import {
1616
type DashboardPanelProps,
17+
emitCloseDashboard,
1718
emitPanelOpen,
1819
LayoutManagerContext,
1920
LayoutUtils,
@@ -278,6 +279,8 @@ export class ConsolePanel extends PureComponent<
278279
if (id != null) {
279280
const { glEventHub } = this.props;
280281
glEventHub.emit(PanelEvent.CLOSE, id);
282+
// Just emit for all panels since there shouldn't be dashboard and panel name conflicts
283+
emitCloseDashboard(glEventHub, title);
281284
}
282285
}
283286
}
Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,23 @@
1-
import type { EventHub } from '@deephaven/golden-layout';
1+
import { makeEventFunctions } from '@deephaven/golden-layout';
22

33
export const CREATE_DASHBOARD = 'CREATE_DASHBOARD';
44

5+
export const CLOSE_DASHBOARD = 'CLOSE_DASHBOARD';
6+
57
export interface CreateDashboardPayload<T = unknown> {
68
pluginId: string;
79
title: string;
810
data: T;
911
}
1012

11-
export function stopListenForCreateDashboard<T = unknown>(
12-
eventHub: EventHub,
13-
handler: (p: CreateDashboardPayload<T>) => void
14-
): void {
15-
try {
16-
eventHub.off(CREATE_DASHBOARD, handler);
17-
} catch {
18-
// golden-layout throws if the handler is not found. Instead catch it and no-op
19-
}
20-
}
13+
export const {
14+
listen: listenForCreateDashboard,
15+
emit: emitCreateDashboard,
16+
useListener: useCreateDashboardListener,
17+
} = makeEventFunctions<[detail: CreateDashboardPayload]>(CREATE_DASHBOARD);
2118

22-
export function listenForCreateDashboard<T = unknown>(
23-
eventHub: EventHub,
24-
handler: (p: CreateDashboardPayload<T>) => void
25-
): () => void {
26-
eventHub.on(CREATE_DASHBOARD, handler);
27-
return () => stopListenForCreateDashboard(eventHub, handler);
28-
}
29-
30-
export function emitCreateDashboard<T = unknown>(
31-
eventHub: EventHub,
32-
payload: CreateDashboardPayload<T>
33-
): void {
34-
eventHub.emit(CREATE_DASHBOARD, payload);
35-
}
19+
export const {
20+
listen: listenForCloseDashboard,
21+
emit: emitCloseDashboard,
22+
useListener: useCloseDashboardListener,
23+
} = makeEventFunctions<[title: string]>(CLOSE_DASHBOARD);

packages/embed-widget/src/App.tsx

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ import Log from '@deephaven/log';
2424
import { useDashboardPlugins } from '@deephaven/plugin';
2525
import {
2626
getAllDashboardsData,
27-
listenForCreateDashboard,
2827
type CreateDashboardPayload,
2928
setDashboardPluginData,
30-
stopListenForCreateDashboard,
3129
emitPanelOpen,
30+
useCreateDashboardListener,
3231
} from '@deephaven/dashboard';
3332
import {
3433
getVariableDescriptor,
@@ -147,31 +146,17 @@ function App(): JSX.Element {
147146
const [goldenLayout, setGoldenLayout] = useState<GoldenLayout | null>(null);
148147
const [dashboardId, setDashboardId] = useState('default-embed-widget'); // Can't be DEFAULT_DASHBOARD_ID because its dashboard layout is not stored in dashboardData
149148

150-
const handleGoldenLayoutChange = useCallback(
151-
(newLayout: GoldenLayout) => {
152-
function handleCreateDashboard({
153-
pluginId,
154-
data,
155-
}: CreateDashboardPayload) {
156-
const id = nanoid();
157-
dispatch(setDashboardPluginData(id, pluginId, data));
158-
setDashboardId(id);
159-
}
160-
161-
setGoldenLayout(oldLayout => {
162-
if (oldLayout != null) {
163-
stopListenForCreateDashboard(
164-
oldLayout.eventHub,
165-
handleCreateDashboard
166-
);
167-
}
168-
listenForCreateDashboard(newLayout.eventHub, handleCreateDashboard);
169-
return newLayout;
170-
});
149+
const handleCreateDashboard = useCallback(
150+
({ pluginId, data }: CreateDashboardPayload) => {
151+
const id = nanoid();
152+
dispatch(setDashboardPluginData(id, pluginId, data));
153+
setDashboardId(id);
171154
},
172155
[dispatch]
173156
);
174157

158+
useCreateDashboardListener(goldenLayout?.eventHub, handleCreateDashboard);
159+
175160
const [hasEmittedWidget, setHasEmittedWidget] = useState(false);
176161

177162
const handleDashboardInitialized = useCallback(() => {
@@ -243,7 +228,7 @@ function App(): JSX.Element {
243228
]}
244229
activeDashboard={dashboardId}
245230
onLayoutInitialized={handleDashboardInitialized}
246-
onGoldenLayoutChange={handleGoldenLayoutChange}
231+
onGoldenLayoutChange={setGoldenLayout}
247232
plugins={dashboardPlugins}
248233
/>
249234
</ErrorBoundary>

packages/golden-layout/src/utils/EventUtils.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useLayoutEffect, useRef } from 'react';
22
import EventEmitter from './EventEmitter';
33

44
type AsArray<P> = P extends unknown[] ? P : [P];
@@ -16,7 +16,7 @@ export type EventEmitFunction<TParameters = []> = (
1616
) => void;
1717

1818
export type EventListenerHook<TParameters = []> = (
19-
eventEmitter: EventEmitter,
19+
eventEmitter: EventEmitter | null | undefined,
2020
handler: EventHandlerFunction<TParameters>
2121
) => void;
2222

@@ -57,10 +57,19 @@ export function makeUseListenerFunction<TParameters = []>(
5757
event: string
5858
): EventListenerHook<TParameters> {
5959
return (eventEmitter, handler) => {
60-
useEffect(
61-
() => listenForEvent(eventEmitter, event, handler),
62-
[eventEmitter, handler]
63-
);
60+
const eventEmitterRef = useRef<typeof eventEmitter>(null);
61+
const handlerRef = useRef<typeof handler>(() => false);
62+
63+
if (
64+
eventEmitterRef.current != eventEmitter ||
65+
handlerRef.current != handler
66+
) {
67+
eventEmitterRef.current?.off(event, handlerRef.current);
68+
eventEmitter?.on(event, handler);
69+
}
70+
71+
eventEmitterRef.current = eventEmitter;
72+
handlerRef.current = handler;
6473
};
6574
}
6675

0 commit comments

Comments
 (0)