Skip to content

Commit 7129036

Browse files
authored
Load widget scripts from CDN and/or local python interpreter (microsoft#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
}

0 commit comments

Comments
 (0)