Skip to content

Commit 7129036

Browse files
authored
Load widget scripts from CDN and/or local python interpreter (#10987)
* Address sonar issues * Fix linter * Fixes * Better way to pas array buffer * Added comments * Oops
1 parent 4eb8cc8 commit 7129036

File tree

59 files changed

+1926
-271
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1926
-271
lines changed

package.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,10 +1636,22 @@
16361636
"description": "Allows a user to import a jupyter notebook into a python file anytime one is opened.",
16371637
"scope": "resource"
16381638
},
1639-
"python.dataScience.loadWidgetScriptsFromThirdPartySource": {
1640-
"type": "boolean",
1641-
"default": false,
1642-
"description": "Enables loading of scripts files for Widgets (ipywidgest, bqplot, beakerx, ipyleaflet, etc) from https://unpkg.com.",
1639+
"python.dataScience.widgetScriptSources": {
1640+
"type": "array",
1641+
"default": [],
1642+
"items": {
1643+
"type": "string",
1644+
"enum": [
1645+
"jsdelivr.com",
1646+
"unpkg.com"
1647+
],
1648+
"enumDescriptions": [
1649+
"Loads widget (javascript) scripts from https://www.jsdelivr.com/",
1650+
"Loads widget (javascript) scripts from https://unpkg.com/"
1651+
]
1652+
},
1653+
"uniqueItems": true,
1654+
"markdownDescription": "Defines the location and order of the sources where scripts files for Widgets are downloaded from (e.g. ipywidgest, bqplot, beakerx, ipyleaflet, etc). Not selecting any of these could result in widgets not rendering or function correctly. See [here](https://aka.ms/PVSCIPyWidgets) for more information. Once updated you will need to restart the Kernel.",
16431655
"scope": "machine"
16441656
},
16451657
"python.dataScience.gatherRules": {

package.nls.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"Common.reload": "Reload",
146146
"Common.moreInfo": "More Info",
147147
"Common.and": "and",
148+
"Common.ok": "Ok",
148149
"Common.install": "Install",
149150
"Common.learnMore": "Learn more",
150151
"OutputChannelNames.languageServer": "Python Language Server",
@@ -464,6 +465,7 @@
464465
"DataScience.jupyterSelectURIRemoteDetail": "Specify the URI of an existing server",
465466
"DataScience.gatherQuality": "Did gather work as desired?",
466467
"DataScience.loadClassFailedWithNoInternet": "Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.",
467-
"DataScience.loadThirdPartyWidgetScriptsDisabled": "Loading of Widgets is disabled by default. Click <a href='https://command:python.datascience.loadWidgetScriptsFromThirdPartySource'>here</a> to enable the setting 'python.dataScience.loadWidgetScriptsFromThirdPartySource'. Once enabled you will need to restart the Kernel",
468-
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'loadWidgetScriptsFromThirdPartySource'."
468+
"DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.",
469+
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.",
470+
"DataScience.enableCDNForWidgetsSetting": "Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1})."
469471
}

src/client/common/application/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,18 @@ export type WebPanelMessage = {
10451045
// Wraps the VS Code webview panel
10461046
export const IWebPanel = Symbol('IWebPanel');
10471047
export interface IWebPanel {
1048+
/**
1049+
* Convert a uri for the local file system to one that can be used inside webviews.
1050+
*
1051+
* Webviews cannot directly load resources from the workspace or local file system using `file:` uris. The
1052+
* `asWebviewUri` function takes a local `file:` uri and converts it into a uri that can be used inside of
1053+
* a webview to load the same resource:
1054+
*
1055+
* ```ts
1056+
* webview.html = `<img src="${webview.asWebviewUri(vscode.Uri.file('/Users/codey/workspace/cat.gif'))}">`
1057+
* ```
1058+
*/
1059+
asWebviewUri(localResource: Uri): Uri;
10481060
setTitle(val: string): void;
10491061
/**
10501062
* Makes the webpanel show up.

src/client/common/application/webPanels/webPanel.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export class WebPanel implements IWebPanel {
7171
this.panel.dispose();
7272
}
7373
}
74+
public asWebviewUri(localResource: Uri) {
75+
if (!this.panel) {
76+
throw new Error('WebView not initialized, too early to get a Uri');
77+
}
78+
return this.panel?.webview.asWebviewUri(localResource);
79+
}
7480

7581
public isVisible(): boolean {
7682
return this.panel ? this.panel.visible : false;
@@ -161,7 +167,7 @@ export class WebPanel implements IWebPanel {
161167
<meta name="theme" content="${Identifiers.GeneratedThemeName}"/>
162168
<title>VS Code Python React UI</title>
163169
<base href="${uriBase}${uriBase.endsWith('/') ? '' : '/'}"/>
164-
<link rel="stylesheet" href="${rootPath}/../common/node_modules/font-awesome/css/font-awesome.min.css">
170+
<link rel="stylesheet" href="${rootPath}/../common/node_modules/font-awesome/css/font-awesome.min.css">
165171
</head>
166172
<body>
167173
<noscript>You need to enable JavaScript to run this app.</noscript>

src/client/common/net/httpClient.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { inject, injectable } from 'inversify';
77
import { parse, ParseError } from 'jsonc-parser';
8-
import * as requestTypes from 'request';
8+
import type * as requestTypes from 'request';
99
import { IHttpClient } from '../../common/types';
1010
import { IServiceContainer } from '../../ioc/types';
1111
import { IWorkspaceService } from '../application/types';
@@ -26,7 +26,7 @@ export class HttpClient implements IHttpClient {
2626
}
2727

2828
public async getJSON<T>(uri: string, strict: boolean = true): Promise<T> {
29-
const body = await this.getBody(uri);
29+
const body = await this.getContents(uri);
3030
return this.parseBodyToJSON(body, strict);
3131
}
3232

@@ -44,7 +44,21 @@ export class HttpClient implements IHttpClient {
4444
}
4545
}
4646

47-
public async getBody(uri: string): Promise<string> {
47+
public async exists(uri: string): Promise<boolean> {
48+
// tslint:disable-next-line:no-require-imports
49+
const request = require('request') as typeof requestTypes;
50+
return new Promise<boolean>((resolve) => {
51+
try {
52+
request
53+
.get(uri, this.requestOptions)
54+
.on('response', (response) => resolve(response.statusCode === 200))
55+
.on('error', () => resolve(false));
56+
} catch {
57+
resolve(false);
58+
}
59+
});
60+
}
61+
private async getContents(uri: string): Promise<string> {
4862
// tslint:disable-next-line:no-require-imports
4963
const request = require('request') as typeof requestTypes;
5064
return new Promise<string>((resolve, reject) => {

src/client/common/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,11 @@ export interface IDataScienceSettings {
393393
variableQueries: IVariableQuery[];
394394
disableJupyterAutoStart?: boolean;
395395
jupyterCommandLineArguments: string[];
396-
loadWidgetScriptsFromThirdPartySource?: boolean;
396+
widgetScriptSources: WidgetCDNs[];
397397
}
398398

399+
export type WidgetCDNs = 'unpkg.com' | 'jsdelivr.com';
400+
399401
export const IConfigurationService = Symbol('IConfigurationService');
400402
export interface IConfigurationService {
401403
getSettings(resource?: Uri): IPythonSettings;
@@ -466,6 +468,10 @@ export interface IHttpClient {
466468
* @param strict Set `false` to allow trailing comma and comments in the JSON, defaults to `true`
467469
*/
468470
getJSON<T>(uri: string, strict?: boolean): Promise<T>;
471+
/**
472+
* Returns the url is valid (i.e. return status code of 200).
473+
*/
474+
exists(uri: string): Promise<boolean>;
469475
}
470476

471477
export const IExtensionContext = Symbol('ExtensionContext');

src/client/common/utils/localize.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export namespace Common {
6363
export const bannerLabelNo = localize('Common.bannerLabelNo', 'No');
6464
export const canceled = localize('Common.canceled', 'Canceled');
6565
export const cancel = localize('Common.cancel', 'Cancel');
66+
export const ok = localize('Common.ok', 'Ok');
6667
export const gotIt = localize('Common.gotIt', 'Got it!');
6768
export const install = localize('Common.install', 'Install');
6869
export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...');
@@ -838,7 +839,15 @@ export namespace DataScience {
838839
);
839840
export const loadThirdPartyWidgetScriptsPostEnabled = localize(
840841
'DataScience.loadThirdPartyWidgetScriptsPostEnabled',
841-
"Once you have updated the setting 'loadWidgetScriptsFromThirdPartySource' you will need to restart the Kernel."
842+
"Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'."
843+
);
844+
export const useCDNForWidgets = localize(
845+
'DataScience.useCDNForWidgets',
846+
'Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.'
847+
);
848+
export const enableCDNForWidgetsSetting = localize(
849+
'DataScience.enableCDNForWidgetsSetting',
850+
"Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1})."
842851
);
843852
}
844853

src/client/common/utils/serializers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ export function serializeDataViews(buffers: undefined | (ArrayBuffer | ArrayBuff
3131
// tslint:disable-next-line: no-any
3232
} as any);
3333
} else {
34+
// Do not use `Array.apply`, it will not work for large arrays.
35+
// Nodejs will throw `stackoverflow` exceptions.
36+
// Else following ipynb fails https://github.com/K3D-tools/K3D-jupyter/blob/821a59ed88579afaafababd6291e8692d70eb088/examples/camera_manipulation.ipynb
37+
// Yet another case where 99% can work, but 1% can fail when testing.
3438
// tslint:disable-next-line: no-any
35-
newBufferView.push(Array.apply(null, new Uint8Array(item as any) as any) as any);
39+
newBufferView.push([...new Uint8Array(item as any)]);
3640
}
3741
}
3842

src/client/datascience/commands/commandRegistry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@ export class CommandRegistry implements IDisposable {
106106
}
107107

108108
private enableLoadingWidgetScriptsFromThirdParty(): void {
109-
if (this.configService.getSettings(undefined).datascience.loadWidgetScriptsFromThirdPartySource) {
109+
if (this.configService.getSettings(undefined).datascience.widgetScriptSources.length > 0) {
110110
return;
111111
}
112112
// Update the setting and once updated, notify user to restart kernel.
113113
this.configService
114114
.updateSetting(
115-
'dataScience.loadWidgetScriptsFromThirdPartySource',
116-
true,
115+
'dataScience.widgetScriptSources',
116+
['jsdelivr.com', 'unpkg.com'],
117117
undefined,
118118
ConfigurationTarget.Global
119119
)

src/client/datascience/constants.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export namespace Commands {
8282
export const SaveAsNotebookNonCustomEditor = 'python.datascience.notebookeditor.saveAs';
8383
export const OpenNotebookNonCustomEditor = 'python.datascience.notebookeditor.open';
8484
export const GatherQuality = 'python.datascience.gatherquality';
85-
export const EnableLoadingWidgetsFrom3rdPartySource = 'python.datascience.loadWidgetScriptsFromThirdPartySource';
85+
export const EnableLoadingWidgetsFrom3rdPartySource =
86+
'python.datascience.enableLoadingWidgetScriptsFromThirdPartySource';
8687
}
8788

8889
export namespace CodeLensCommands {
@@ -292,6 +293,13 @@ export enum Telemetry {
292293
IPyWidgetLoadSuccess = 'DS_INTERNAL.IPYWIDGET_LOAD_SUCCESS',
293294
IPyWidgetLoadFailure = 'DS_INTERNAL.IPYWIDGET_LOAD_FAILURE',
294295
IPyWidgetLoadDisabled = 'DS_INTERNAL.IPYWIDGET_LOAD_DISABLED',
296+
HashedIPyWidgetNameUsed = 'DS_INTERNAL.IPYWIDGET_USED_BY_USER',
297+
HashedIPyWidgetNameDiscovered = 'DS_INTERNAL.IPYWIDGET_DISCOVERED',
298+
HashedIPyWidgetScriptDiscoveryError = 'DS_INTERNAL.IPYWIDGET_DISCOVERY_ERRORED',
299+
DiscoverIPyWidgetNamesLocalPerf = 'DS_INTERNAL.IPYWIDGET_TEST_AVAILABILITY_ON_LOCAL',
300+
DiscoverIPyWidgetNamesCDNPerf = 'DS_INTERNAL.IPYWIDGET_TEST_AVAILABILITY_ON_CDN',
301+
IPyWidgetPromptToUseCDN = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN',
302+
IPyWidgetPromptToUseCDNSelection = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION',
295303
IPyWidgetOverhead = 'DS_INTERNAL.IPYWIDGET_OVERHEAD',
296304
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE'
297305
}

src/client/datascience/interactive-common/interactiveBase.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
177177

178178
// For each listener sign up for their post events
179179
this.listeners.forEach((l) => l.postMessage((e) => this.postMessageInternal(e.message, e.payload)));
180+
// Channel for listeners to send messages to the interactive base.
181+
this.listeners.forEach((l) => {
182+
if (l.postInternalMessage) {
183+
l.postInternalMessage((e) => this.onMessage(e.message, e.payload));
184+
}
185+
});
180186

181187
// Tell each listener our identity. Can't do it here though as were in the constructor for the base class
182188
setTimeout(() => {
@@ -204,6 +210,12 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
204210
// tslint:disable-next-line: no-any no-empty cyclomatic-complexity max-func-body-length
205211
public onMessage(message: string, payload: any) {
206212
switch (message) {
213+
case InteractiveWindowMessages.ConvertUriForUseInWebViewRequest:
214+
const request = payload as Uri;
215+
const response = { request, response: this.asWebviewUri(request) };
216+
this.postMessageToListeners(InteractiveWindowMessages.ConvertUriForUseInWebViewResponse, response);
217+
break;
218+
207219
case InteractiveWindowMessages.Started:
208220
// Send the first settings message
209221
this.onDataScienceSettingsChanged().ignoreErrors();

src/client/datascience/interactive-common/interactiveWindowTypes.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {
1010
CommonActionType,
1111
IAddCellAction,
1212
ILoadIPyWidgetClassFailureAction,
13-
LoadIPyWidgetClassDisabledAction,
1413
LoadIPyWidgetClassLoadAction
1514
} from '../../../datascience-ui/interactive-common/redux/reducers/types';
1615
import { PythonInterpreter } from '../../interpreter/contracts';
16+
import { WidgetScriptSource } from '../ipywidgets/types';
1717
import { LiveKernelModel } from '../jupyter/kernels/types';
1818
import { CssMessages, IGetCssRequest, IGetCssResponse, IGetMonacoThemeRequest, SharedMessages } from '../messages';
1919
import { IGetMonacoThemeResponse } from '../monacoMessages';
@@ -53,6 +53,8 @@ export enum InteractiveWindowMessages {
5353
DoSave = 'DoSave',
5454
SendInfo = 'send_info',
5555
Started = 'started',
56+
ConvertUriForUseInWebViewRequest = 'ConvertUriForUseInWebViewRequest',
57+
ConvertUriForUseInWebViewResponse = 'ConvertUriForUseInWebViewResponse',
5658
AddedSysInfo = 'added_sys_info',
5759
RemoteAddCode = 'remote_add_code',
5860
RemoteReexecuteCode = 'remote_reexecute_code',
@@ -111,13 +113,22 @@ export enum InteractiveWindowMessages {
111113
UpdateDisplayData = 'update_display_data',
112114
IPyWidgetLoadSuccess = 'ipywidget_load_success',
113115
IPyWidgetLoadFailure = 'ipywidget_load_failure',
114-
IPyWidgetLoadDisabled = 'ipywidget_load_disabled',
115116
IPyWidgetRenderFailure = 'ipywidget_render_failure'
116117
}
117118

118119
export enum IPyWidgetMessages {
119120
IPyWidgets_Ready = 'IPyWidgets_Ready',
120121
IPyWidgets_onRestartKernel = 'IPyWidgets_onRestartKernel',
122+
IPyWidgets_onKernelChanged = 'IPyWidgets_onKernelChanged',
123+
IPyWidgets_updateRequireConfig = 'IPyWidgets_updateRequireConfig',
124+
/**
125+
* UI sends a request to extension to determine whether we have the source for any of the widgets.
126+
*/
127+
IPyWidgets_WidgetScriptSourceRequest = 'IPyWidgets_WidgetScriptSourceRequest',
128+
/**
129+
* Extension sends response to the request with yes/no.
130+
*/
131+
IPyWidgets_WidgetScriptSourceResponse = 'IPyWidgets_WidgetScriptSourceResponse',
121132
IPyWidgets_msg = 'IPyWidgets_msg',
122133
IPyWidgets_binary_msg = 'IPyWidgets_binary_msg',
123134
IPyWidgets_msg_handled = 'IPyWidgets_msg_handled',
@@ -477,8 +488,11 @@ export type NotebookModelChange =
477488
// Map all messages to specific payloads
478489
export class IInteractiveWindowMapping {
479490
public [IPyWidgetMessages.IPyWidgets_kernelOptions]: KernelSocketOptions;
491+
public [IPyWidgetMessages.IPyWidgets_WidgetScriptSourceRequest]: { moduleName: string; moduleVersion: string };
492+
public [IPyWidgetMessages.IPyWidgets_WidgetScriptSourceResponse]: WidgetScriptSource;
480493
public [IPyWidgetMessages.IPyWidgets_Ready]: never | undefined;
481494
public [IPyWidgetMessages.IPyWidgets_onRestartKernel]: never | undefined;
495+
public [IPyWidgetMessages.IPyWidgets_onKernelChanged]: never | undefined;
482496
public [IPyWidgetMessages.IPyWidgets_registerCommTarget]: string;
483497
// tslint:disable-next-line: no-any
484498
public [IPyWidgetMessages.IPyWidgets_binary_msg]: { id: string; data: any };
@@ -589,6 +603,7 @@ export class IInteractiveWindowMapping {
589603
public [InteractiveWindowMessages.UpdateDisplayData]: KernelMessage.IUpdateDisplayDataMsg;
590604
public [InteractiveWindowMessages.IPyWidgetLoadSuccess]: LoadIPyWidgetClassLoadAction;
591605
public [InteractiveWindowMessages.IPyWidgetLoadFailure]: ILoadIPyWidgetClassFailureAction;
592-
public [InteractiveWindowMessages.IPyWidgetLoadDisabled]: LoadIPyWidgetClassDisabledAction;
606+
public [InteractiveWindowMessages.ConvertUriForUseInWebViewRequest]: Uri;
607+
public [InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: { request: Uri; response: Uri };
593608
public [InteractiveWindowMessages.IPyWidgetRenderFailure]: Error;
594609
}

src/client/datascience/interactive-common/linkProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const LineQueryRegex = /line=(\d+)/;
1919
// in a markdown cell using the syntax: https://command:[my.vscode.command].
2020
const linkCommandWhitelist = [
2121
'python.datascience.gatherquality',
22-
'python.datascience.loadWidgetScriptsFromThirdPartySource'
22+
'python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'
2323
];
2424

2525
// tslint:disable: no-any

0 commit comments

Comments
 (0)