Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions build/esbuild/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ async function buildAll() {
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'selectInputSettings', 'index.tsx'),
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'selectInputSettings', 'index.js'),
{ target: 'web', watch: watchAll }
),
build(
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'bigNumberComparisonSettings', 'index.tsx'),
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'bigNumberComparisonSettings', 'index.js'),
{ target: 'web', watch: watchAll }
)
);

Expand Down
14 changes: 14 additions & 0 deletions src/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,20 @@ export type LocalizedMessages = {
saveButton: string;
cancelButton: string;
failedToSave: string;
// Big number comparison settings strings
bigNumberComparisonTitle: string;
enableComparison: string;
comparisonTypeLabel: string;
percentageChange: string;
absoluteValue: string;
comparisonValueLabel: string;
comparisonValuePlaceholder: string;
comparisonTitleLabel: string;
comparisonTitlePlaceholder: string;
comparisonTitleHelp: string;
comparisonValueHelp: string;
comparisonFormatLabel: string;
comparisonFormatHelp: string;
};
// Map all messages to specific payloads
export class IInteractiveWindowMapping {
Expand Down
314 changes: 314 additions & 0 deletions src/notebooks/deepnote/bigNumberComparisonSettingsWebview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import {
CancellationToken,
Disposable,
NotebookCell,
NotebookEdit,
Uri,
ViewColumn,
WebviewPanel,
window,
workspace,
WorkspaceEdit
} from 'vscode';
import { inject, injectable } from 'inversify';

import { IExtensionContext } from '../../platform/common/types';
import { LocalizedMessages } from '../../messageTypes';
import * as localize from '../../platform/common/utils/localize';
import {
BigNumberComparisonSettings,
BigNumberComparisonWebviewMessage
} from '../../platform/notebooks/deepnote/types';
import { WrappedError } from '../../platform/errors/types';
import { logger } from '../../platform/logging';

/**
* Manages the webview panel for big number comparison settings
*/
@injectable()
export class BigNumberComparisonSettingsWebviewProvider {
private currentPanel: WebviewPanel | undefined;
private currentPanelId: number = 0;
private readonly disposables: Disposable[] = [];
private currentCell: NotebookCell | undefined;
private resolvePromise: ((settings: BigNumberComparisonSettings | null) => void) | undefined;

constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {}

/**
* Show the big number comparison settings webview
*/
public async show(cell: NotebookCell, token?: CancellationToken): Promise<BigNumberComparisonSettings | null> {
this.currentCell = cell;

const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : ViewColumn.One;

// If we already have a panel, cancel any outstanding operation before disposing
if (this.currentPanel) {
// Cancel the previous operation by resolving with null
if (this.resolvePromise) {
this.resolvePromise(null);
this.resolvePromise = undefined;
}
// Now dispose the old panel
this.currentPanel.dispose();
}

// Increment panel ID to track this specific panel instance
this.currentPanelId++;
const panelId = this.currentPanelId;

// Create a new panel
this.currentPanel = window.createWebviewPanel(
'deepnoteBigNumberComparisonSettings',
localize.BigNumberComparison.title,
column || ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [this.extensionContext.extensionUri]
}
);

// Set the webview's initial html content
this.currentPanel.webview.html = this.getWebviewContent();

// Handle messages from the webview
this.currentPanel.webview.onDidReceiveMessage(
async (message: BigNumberComparisonWebviewMessage) => {
await this.handleMessage(message);
},
null,
this.disposables
);

// Handle cancellation token if provided
let cancellationDisposable: Disposable | undefined;
if (token) {
cancellationDisposable = token.onCancellationRequested(() => {
// Only handle cancellation if this is still the current panel
if (this.currentPanelId === panelId) {
if (this.resolvePromise) {
this.resolvePromise(null);
this.resolvePromise = undefined;
}
this.currentPanel?.dispose();
}
});
}

// Reset when the current panel is closed
this.currentPanel.onDidDispose(
() => {
// Only handle disposal if this is still the current panel
if (this.currentPanelId === panelId) {
this.currentPanel = undefined;
this.currentCell = undefined;
if (this.resolvePromise) {
this.resolvePromise(null);
this.resolvePromise = undefined;
}
// Clean up cancellation listener
cancellationDisposable?.dispose();
this.disposables.forEach((d) => d.dispose());
this.disposables.length = 0;
}
},
null,
this.disposables
);

// Send initial data after a small delay to ensure webview is ready
// This is necessary because postMessage can fail if sent before the webview is fully loaded
setTimeout(async () => {
await this.sendLocStrings();
await this.sendInitialData();
}, 100);

// Return a promise that resolves when the user saves or cancels
return new Promise((resolve) => {
this.resolvePromise = resolve;
});
}

private async sendInitialData(): Promise<void> {
if (!this.currentPanel || !this.currentCell) {
return;
}

const metadata = this.currentCell.metadata as Record<string, unknown> | undefined;

const settings: BigNumberComparisonSettings = {
enabled: (metadata?.deepnote_big_number_comparison_enabled as boolean) ?? false,
comparisonType:
(metadata?.deepnote_big_number_comparison_type as 'percentage-change' | 'absolute-value' | '') ?? '',
comparisonValue: (metadata?.deepnote_big_number_comparison_value as string) ?? '',
comparisonTitle: (metadata?.deepnote_big_number_comparison_title as string) ?? '',
comparisonFormat: (metadata?.deepnote_big_number_comparison_format as string) ?? ''
};

await this.currentPanel.webview.postMessage({
type: 'init',
settings
});
}

private async sendLocStrings(): Promise<void> {
if (!this.currentPanel) {
return;
}

const locStrings: Partial<LocalizedMessages> = {
bigNumberComparisonTitle: localize.BigNumberComparison.title,
enableComparison: localize.BigNumberComparison.enableComparison,
comparisonTypeLabel: localize.BigNumberComparison.comparisonTypeLabel,
percentageChange: localize.BigNumberComparison.percentageChange,
absoluteValue: localize.BigNumberComparison.absoluteValue,
comparisonValueLabel: localize.BigNumberComparison.comparisonValueLabel,
comparisonValuePlaceholder: localize.BigNumberComparison.comparisonValuePlaceholder,
comparisonTitleLabel: localize.BigNumberComparison.comparisonTitleLabel,
comparisonTitlePlaceholder: localize.BigNumberComparison.comparisonTitlePlaceholder,
comparisonTitleHelp: localize.BigNumberComparison.comparisonTitleHelp,
comparisonFormatLabel: localize.BigNumberComparison.comparisonFormatLabel,
comparisonFormatHelp: localize.BigNumberComparison.comparisonFormatHelp,
saveButton: localize.BigNumberComparison.saveButton,
cancelButton: localize.BigNumberComparison.cancelButton
};

await this.currentPanel.webview.postMessage({
type: 'locInit',
locStrings
});
}

private async handleMessage(message: BigNumberComparisonWebviewMessage): Promise<void> {
switch (message.type) {
case 'save':
if (this.currentCell) {
try {
await this.saveSettings(message.settings);
if (this.resolvePromise) {
this.resolvePromise(message.settings);
this.resolvePromise = undefined;
}
this.currentPanel?.dispose();
} catch (error) {
// Error is already shown to user in saveSettings
logger.error('BigNumberComparisonSettingsWebview: Failed to save settings', error);
}
}
break;

case 'cancel':
if (this.resolvePromise) {
this.resolvePromise(null);
this.resolvePromise = undefined;
}
this.currentPanel?.dispose();
break;

case 'init':
case 'locInit':
// These messages are sent from extension to webview, not handled here
break;
}
}

private async saveSettings(settings: BigNumberComparisonSettings): Promise<void> {
if (!this.currentCell) {
return;
}

const edit = new WorkspaceEdit();
const metadata = { ...(this.currentCell.metadata as Record<string, unknown>) };

metadata.deepnote_big_number_comparison_enabled = settings.enabled;
metadata.deepnote_big_number_comparison_type = settings.comparisonType;
metadata.deepnote_big_number_comparison_value = settings.comparisonValue;
metadata.deepnote_big_number_comparison_title = settings.comparisonTitle;
metadata.deepnote_big_number_comparison_format = settings.comparisonFormat;

// Update cell metadata
edit.set(this.currentCell.notebook.uri, [NotebookEdit.updateCellMetadata(this.currentCell.index, metadata)]);

try {
const success = await workspace.applyEdit(edit);
if (!success) {
const errorMessage = localize.BigNumberComparison.failedToSave;
logger.error(errorMessage);
void window.showErrorMessage(errorMessage);
throw new WrappedError(errorMessage, undefined);
}
} catch (error) {
const errorMessage = localize.BigNumberComparison.failedToSave;
const cause = error instanceof Error ? error : undefined;
const causeMessage = cause?.message || String(error);
logger.error(`${errorMessage}: ${causeMessage}`, error);
void window.showErrorMessage(errorMessage);
throw new WrappedError(errorMessage, cause);
}
}

private getWebviewContent(): string {
if (!this.currentPanel) {
return '';
}

const webview = this.currentPanel.webview;
const nonce = this.getNonce();

// Get URIs for the React app
const scriptUri = webview.asWebviewUri(
Uri.joinPath(
this.extensionContext.extensionUri,
'dist',
'webviews',
'webview-side',
'bigNumberComparisonSettings',
'index.js'
)
);
const codiconUri = webview.asWebviewUri(
Uri.joinPath(
this.extensionContext.extensionUri,
'dist',
'webviews',
'webview-side',
'react-common',
'codicon',
'codicon.css'
)
);

const title = localize.BigNumberComparison.title;

return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${webview.cspSource};">
<link rel="stylesheet" href="${codiconUri}">
<title>${title}</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
</body>
</html>`;
}

private getNonce(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

public dispose(): void {
this.currentPanel?.dispose();
this.disposables.forEach((d) => d.dispose());
}
}
Loading
Loading