Skip to content

Commit

Permalink
Refactored panel management (DH-18347)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmingles committed Jan 17, 2025
1 parent b61e268 commit a48fd3d
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 92 deletions.
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const DEFAULT_CONSOLE_TYPE = 'python' as const;
export const DEFAULT_TEMPORARY_QUERY_AUTO_TIMEOUT_MS = 600000 as const;
export const DEFAULT_TEMPORARY_QUERY_TIMEOUT_MS = 60000 as const;

export const DH_PANEL_VIEW_TYPE = 'dhPanel';

export const INTERACTIVE_CONSOLE_QUERY_TYPE = 'InteractiveConsole';
export const INTERACTIVE_CONSOLE_TEMPORARY_QUEUE_NAME =
'InteractiveConsoleTemporaryQueue';
Expand Down
177 changes: 88 additions & 89 deletions src/controllers/PanelController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
createSessionDetailsResponsePostMessage,
getDHThemeKey,
getPanelHtml,
isDhPanelTab,
isNonEmptyArray,
Logger,
} from '../util';
import { DhcService } from '../services';
import {
DEEPHAVEN_POST_MSG,
DH_PANEL_VIEW_TYPE,
OPEN_VARIABLE_PANELS_CMD,
REFRESH_VARIABLE_PANELS_CMD,
} from '../common';
Expand All @@ -41,6 +43,10 @@ export class PanelController extends ControllerBase {
this._onRefreshPanelsContent
);

vscode.window.tabGroups.onDidChangeTabs(
this._debouncedRefreshVisiblePanelsPendingInitialLoad
);

this.disposables.push(
vscode.window.onDidChangeActiveColorTheme(
this._onDidChangeActiveColorTheme
Expand All @@ -50,13 +56,70 @@ export class PanelController extends ControllerBase {

private readonly _panelService: IPanelService;
private readonly _serverManager: IServerManager;
private _lastPanelDetails: {
panel: vscode.WebviewPanel;
variable: VariableDefintion;
isNew: boolean;
hasChangedToVisible: boolean;
} | null = null;
private _panelsPendingInitialLoad = new Set<vscode.WebviewPanel>();

private readonly _lastPanelInViewColumn = new Map<
vscode.ViewColumn | undefined,
vscode.WebviewPanel
>();
private readonly _panelsPendingInitialLoad = new Map<
vscode.WebviewPanel,
VariableDefintion
>();

private _debounceRefreshPanels?: NodeJS.Timeout;

/**
* Load any visible panels that are marked for pending initial load. Calls
* to this method are debounced in case this is called multiple times before
* the active tab state actually settles. e.g. tab change events may fire
* multiple times as tabs are removed, added, etc.
*/
private _debouncedRefreshVisiblePanelsPendingInitialLoad = (): void => {
clearTimeout(this._debounceRefreshPanels);

this._debounceRefreshPanels = setTimeout(() => {
const visiblePanels: {
url: URL;
panel: vscode.WebviewPanel;
variable: VariableDefintion;
}[] = [];

// Get details for visible panels that are pending initial load
for (const url of this._panelService.getPanelUrls()) {
for (const panel of this._panelService.getPanels(url)) {
if (panel.visible && this._panelsPendingInitialLoad.has(panel)) {
const variable = this._panelsPendingInitialLoad.get(panel)!;
visiblePanels.push({ url, panel, variable });
}
}
}

vscode.window.tabGroups.all.forEach(tabGroup => {
if (!isDhPanelTab(tabGroup.activeTab)) {
return;
}

// Panel names are not guaranteed to be unique across multiple servers,
// so there is an edge case where a panel could get unnecessarily refreshed
// if it is the active panel, and a variable with the same name gets
// updated on another vscode connection. This shouldn't hurt anything
// and seems likely to be rare. There doesn't seem to be a way to know
// which vscode panel is associated with a tab, so best we can do is
// match the tab label to the panel title.
const matchingPanels = visiblePanels.filter(
({ panel }) =>
panel.viewColumn === tabGroup.viewColumn &&
panel.title === tabGroup.activeTab?.label
);

for (const { url, panel, variable } of matchingPanels) {
logger.debug2('Loading initial panel content:', panel.title);
this._panelsPendingInitialLoad.delete(panel);
this._onRefreshPanelsContent(url, [variable]);
}
});
}, 100);
};

/**
* Handle `postMessage` messages from the panel.
Expand Down Expand Up @@ -120,6 +183,12 @@ export class PanelController extends ControllerBase {
logger.debug('Unknown message type', message);
}

/**
* Ensure panels for given variables are open and queued for loading initial
* content.
* @param serverUrl
* @param variables
*/
private _onOpenPanels = async (
serverUrl: URL,
variables: NonEmptyArray<VariableDefintion>
Expand All @@ -134,7 +203,7 @@ export class PanelController extends ControllerBase {
// where the `editor/title/run` menu gets stuck on a previous selection.
await waitFor(0);

this._lastPanelDetails = null;
this._lastPanelInViewColumn.clear();

// Target ViewColumn is either the first existing panel's viewColumn or a
// new tab group if none exist.
Expand All @@ -149,7 +218,7 @@ export class PanelController extends ControllerBase {

const panel: vscode.WebviewPanel = isNewPanel
? vscode.window.createWebviewPanel(
'dhPanel', // Identifies the type of the webview. Used internally
DH_PANEL_VIEW_TYPE, // Identifies the type of the webview. Used internally
title,
{ viewColumn: targetViewColumn, preserveFocus: true },
{
Expand All @@ -159,21 +228,10 @@ export class PanelController extends ControllerBase {
)
: this._panelService.getPanelOrThrow(serverUrl, id);

this._lastPanelDetails = {
panel,
variable,
isNew: isNewPanel,
hasChangedToVisible: false,
};
this._panelsPendingInitialLoad.add(panel);
this._lastPanelInViewColumn.set(panel.viewColumn, panel);
this._panelsPendingInitialLoad.set(panel, variable);

if (isNewPanel) {
const onDidChangeViewStateSubscription = panel.onDidChangeViewState(
({ webviewPanel }) => {
this._onPanelViewStateChange(serverUrl, webviewPanel, variable);
}
);

const onDidReceiveMessageSubscription =
panel.webview.onDidReceiveMessage(({ data }) => {
const postMessage = panel.webview.postMessage.bind(panel.webview);
Expand All @@ -191,78 +249,18 @@ export class PanelController extends ControllerBase {
this._panelService.deletePanel(serverUrl, id);
this._panelsPendingInitialLoad.delete(panel);

onDidChangeViewStateSubscription.dispose();
// onDidChangeViewStateSubscription.dispose();
onDidReceiveMessageSubscription.dispose();
});
}
}

assertDefined(this._lastPanelDetails, '_lastPanelDetails');

this._lastPanelDetails.panel.reveal();

// If the last panel already exists, it may not fire a `onDidChangeViewState`
// event, so eagerly refresh it since we know it will be visible.
if (!this._lastPanelDetails.isNew) {
logger.debug2(
'Refreshing last panel:',
this._lastPanelDetails.panel.title
);
this._panelsPendingInitialLoad.delete(this._lastPanelDetails.panel);
this._onRefreshPanelsContent(serverUrl, [
this._lastPanelDetails.variable,
]);
}
};

/**
* Subscribe to visibility changes on new panels so that we can load data
* the first time it becomes visible. Initial data will be loaded if
* 1. The panel is visible.
* 2. The last opened panel has changed to visible. When multiple panels
* are being created, they will start visible and then be hidden as the
* next panel is created. We want to wait until all panels have been
* created to avoid eager loading every panel along the way.
* 3. The panel is marked as pending initial load.
* @param serverUrl The server url.
* @param panel The panel to subscribe to.
* @param variable The variable associated with the panel.
*/
private _onPanelViewStateChange = (
serverUrl: URL,
panel: vscode.WebviewPanel,
variable: VariableDefintion
): void => {
logger.debug2(
`[_onPanelViewStateChange]: ${panel.title}`,
`active:${panel.active},visible:${panel.visible}`
);

if (!panel.visible || this._lastPanelDetails == null) {
return;
}

if (
this._lastPanelDetails.panel === panel &&
!this._lastPanelDetails.hasChangedToVisible
) {
logger.debug2(panel.title, 'Last panel has changed to visible');
this._lastPanelDetails.hasChangedToVisible = true;
}

if (!this._lastPanelDetails.hasChangedToVisible) {
logger.debug2(panel.title, 'Waiting for last panel');
return;
// Reveal last panel added to each tab group
for (const panel of this._lastPanelInViewColumn.values()) {
panel.reveal();
}

if (!this._panelsPendingInitialLoad.has(panel)) {
logger.debug2(panel.title, 'Panel already loaded');
return;
}

logger.debug2(panel.title, 'Loading initial panel content');
this._panelsPendingInitialLoad.delete(panel);
this._onRefreshPanelsContent(serverUrl, [variable]);
this._debouncedRefreshVisiblePanelsPendingInitialLoad();
};

/**
Expand All @@ -287,7 +285,8 @@ export class PanelController extends ControllerBase {
await this._serverManager.getWorkerInfo(serverUrl as WorkerURL)
);

for (const { id, title } of variables) {
for (const variable of variables) {
const { id, title } = variable;
const panel = this._panelService.getPanelOrThrow(serverUrl, id);

// For any panels that are not visible at time of refresh, flag them as
Expand All @@ -298,7 +297,7 @@ export class PanelController extends ControllerBase {
// everything in vscode.
if (!panel.visible) {
logger.debug2('Panel not visible:', panel.title);
this._panelsPendingInitialLoad.add(panel);
this._panelsPendingInitialLoad.set(panel, variable);
continue;
}

Expand Down
15 changes: 13 additions & 2 deletions src/services/DhcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,20 @@ export class DhcService implements IDhcService {
);

if (isNonEmptyArray(panelVariablesToUpdate)) {
logger.debug2('[subscribeToFieldUpdates] Updating variables:');
logger.debug2(
'[subscribeToFieldUpdates] Updating variables',
panelVariablesToUpdate.map(v => v.title)
);

vscode.commands.executeCommand(
REFRESH_VARIABLE_PANELS_CMD,
this.serverUrl,
panelVariablesToUpdate
);
} else {
logger.debug2(
'[subscribeToFieldUpdates] No existing panels to update:'
);
}
});
this.subscriptions.push(fieldUpdateSubscription);
Expand Down Expand Up @@ -436,7 +443,11 @@ export class DhcService implements IDhcService {
const showVariables = changed.filter(v => !v.title.startsWith('_'));

if (isNonEmptyArray(showVariables)) {
logger.debug('[runEditorCode] Showing variables:', showVariables);
logger.debug(
'[runEditorCode] Showing variables:',
showVariables.map(v => v.title).join(', ')
);

vscode.commands.executeCommand(
OPEN_VARIABLE_PANELS_CMD,
this.serverUrl,
Expand Down
28 changes: 27 additions & 1 deletion src/util/panelUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as vscode from 'vscode';
import type { dh as DhcType } from '@deephaven/jsapi-types';
import { DEEPHAVEN_POST_MSG, VSCODE_POST_MSG } from '../common';
import {
DEEPHAVEN_POST_MSG,
DH_PANEL_VIEW_TYPE,
VSCODE_POST_MSG,
} from '../common';
import type {
LoginOptionsResponsePostMessage,
SessionDetailsResponsePostMessage,
Expand Down Expand Up @@ -141,3 +146,24 @@ export function getPanelHtml(iframeUrl: URL, title: string): string {
</body>
</html>`;
}

/**
* Returns whether a given tab contains a dhPanel.
* @param tab The tab to check
* @returns True if the given tab contains a dhPanel.
*/
export function isDhPanelTab(tab?: vscode.Tab): boolean {
if (tab == null) {
return false;
}

const { input } = tab;

return (
input != null &&
typeof input === 'object' &&
'viewType' in input &&
typeof input.viewType === 'string' &&
input.viewType.endsWith(`-${DH_PANEL_VIEW_TYPE}`)
);
}

0 comments on commit a48fd3d

Please sign in to comment.