Skip to content

Add message when widget script is not found on CDN #11249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"Common.ok": "Ok",
"Common.install": "Install",
"Common.learnMore": "Learn more",
"Common.reportThisIssue": "Report this issue",
"OutputChannelNames.languageServer": "Python Language Server",
"OutputChannelNames.python": "Python",
"OutputChannelNames.pythonTest": "Python Test Log",
Expand Down Expand Up @@ -471,5 +472,6 @@
"DataScience.loadClassFailedWithNoInternet": "Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.",
"DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.",
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.",
"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})."
"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}).",
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."
}
5 changes: 5 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export namespace Common {
export const moreInfo = localize('Common.moreInfo', 'More Info');
export const learnMore = localize('Common.learnMore', 'Learn more');
export const and = localize('Common.and', 'and');
export const reportThisIssue = localize('Common.reportThisIssue', 'Report this issue');
}

export namespace AttachProcess {
Expand Down Expand Up @@ -865,6 +866,10 @@ export namespace DataScience {
'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})."
);
export const widgetScriptNotFoundOnCDNWidgetMightNotWork = localize(
'DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork',
"Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."
);
}

export namespace DebugConfigStrings {
Expand Down
43 changes: 41 additions & 2 deletions src/client/datascience/ipywidgets/ipyWidgetScriptSourceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { sha256 } from 'hash.js';
import { ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
import { IApplicationShell, IWorkspaceService } from '../../common/application/types';
import '../../common/extensions';
import { traceError } from '../../common/logger';
import { IFileSystem } from '../../common/platform/types';
import {
Expand All @@ -17,6 +18,7 @@ import {
} from '../../common/types';
import { createDeferred, Deferred } from '../../common/utils/async';
import { Common, DataScience } from '../../common/utils/localize';
import { noop } from '../../common/utils/misc';
import { IInterpreterService } from '../../interpreter/contracts';
import { sendTelemetryEvent } from '../../telemetry';
import { Telemetry } from '../constants';
Expand All @@ -27,6 +29,7 @@ import { RemoteWidgetScriptSourceProvider } from './remoteWidgetScriptSourceProv
import { IWidgetScriptSourceProvider, WidgetScriptSource } from './types';

const GlobalStateKeyToTrackIfUserConfiguredCDNAtLeastOnce = 'IPYWidgetCDNConfigured';
const GlobalStateKeyToNeverWarnAboutScriptsNotFoundOnCDN = 'IPYWidgetNotFoundOnCDN';

/**
* This class decides where to get widget scripts from.
Expand All @@ -35,13 +38,15 @@ const GlobalStateKeyToTrackIfUserConfiguredCDNAtLeastOnce = 'IPYWidgetCDNConfigu
* If user has not configured antying, user will be presented with a prompt.
*/
export class IPyWidgetScriptSourceProvider implements IWidgetScriptSourceProvider {
private readonly notifiedUserAboutWidgetScriptNotFound = new Set<string>();
private scriptProviders?: IWidgetScriptSourceProvider[];
private configurationPromise?: Deferred<void>;
private get configuredScriptSources(): readonly WidgetCDNs[] {
const settings = this.configurationSettings.getSettings(undefined);
return settings.datascience.widgetScriptSources;
}
private readonly userConfiguredCDNAtLeastOnce: IPersistentState<boolean>;
private readonly neverWarnAboutScriptsNotFoundOnCDN: IPersistentState<boolean>;
constructor(
private readonly notebook: INotebook,
private readonly localResourceUriConverter: ILocalResourceUriConverter,
Expand All @@ -57,6 +62,10 @@ export class IPyWidgetScriptSourceProvider implements IWidgetScriptSourceProvide
GlobalStateKeyToTrackIfUserConfiguredCDNAtLeastOnce,
false
);
this.neverWarnAboutScriptsNotFoundOnCDN = this.stateFactory.createGlobalPersistentState<boolean>(
GlobalStateKeyToNeverWarnAboutScriptsNotFoundOnCDN,
false
);
}
public initialize() {
this.workspaceService.onDidChangeConfiguration(this.onSettingsChagned.bind(this));
Expand Down Expand Up @@ -94,16 +103,46 @@ export class IPyWidgetScriptSourceProvider implements IWidgetScriptSourceProvide

sendTelemetryEvent(Telemetry.HashedIPyWidgetNameUsed, undefined, {
hashedName: sha256().update(found.moduleName).digest('hex'),
source: found.source
source: found.source,
cdnSearched: this.configuredScriptSources.length > 0
});

if (!found.scriptUri) {
traceError(`Script source for Widget ${moduleName}@${moduleVersion} not found`);
}
this.handleWidgetSourceNotFoundOnCDN(found).ignoreErrors();
return found;
}
private async handleWidgetSourceNotFoundOnCDN(widgetSource: WidgetScriptSource) {
// if widget exists nothing to do.
if (widgetSource.source === 'cdn' || this.neverWarnAboutScriptsNotFoundOnCDN.value === true) {
return;
}
if (
this.notifiedUserAboutWidgetScriptNotFound.has(widgetSource.moduleName) ||
this.configuredScriptSources.length === 0
) {
return;
}
this.notifiedUserAboutWidgetScriptNotFound.add(widgetSource.moduleName);
const selection = await this.appShell.showWarningMessage(
DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork().format(widgetSource.moduleName),
Common.ok(),
Common.doNotShowAgain(),
Common.reportThisIssue()
);
switch (selection) {
case Common.doNotShowAgain():
return this.neverWarnAboutScriptsNotFoundOnCDN.updateValue(true);
case Common.reportThisIssue():
return this.appShell.openUrl('https://aka.ms/CreatePVSCDataScienceIssue');
default:
noop();
}
}

private onSettingsChagned(e: ConfigurationChangeEvent) {
if (e.affectsConfiguration('dataScience.widgetScriptSources')) {
if (e.affectsConfiguration('python.dataScience.widgetScriptSources')) {
this.rebuildProviders();
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,10 @@ export interface IEventNamePropertyMapping {
* Where did we find the hashed name (CDN or user environment or remote jupyter).
*/
source?: 'cdn' | 'local' | 'remote';
/**
* Whether we searched CDN or not.
*/
cdnSearched: boolean;
};
/**
* Telemetry event sent with name of a Widget found.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,47 @@ suite('Data Science - ipywidget - Widget Script Source Provider', () => {
assert.deepEqual(values, { moduleName: 'module1', scriptUri: '1', source: 'cdn' });
assert.isFalse(localOrRemoteSource.calledOnce);
assert.isTrue(cdnSource.calledOnce);
verify(appShell.showWarningMessage(anything(), anything(), anything(), anything())).never();
});
test('When CDN is turned on and widget script is not found, then display a warning about script not found on CDN', async () => {
settings.datascience.widgetScriptSources = ['jsdelivr.com', 'unpkg.com'];
const localOrRemoteSource = localLaunch
? sinon.stub(LocalWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource')
: sinon.stub(RemoteWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource');
const cdnSource = sinon.stub(CDNWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource');

localOrRemoteSource.resolves({ moduleName: 'module1' });
cdnSource.resolves({ moduleName: 'module1' });

scriptSourceProvider.initialize();
let values = await scriptSourceProvider.getWidgetScriptSource('module1', '1');

assert.deepEqual(values, { moduleName: 'module1' });
assert.isTrue(localOrRemoteSource.calledOnce);
assert.isTrue(cdnSource.calledOnce);
verify(
appShell.showWarningMessage(
DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork().format('module1'),
anything(),
anything(),
anything()
)
).once();

// Ensure message is not displayed more than once.
values = await scriptSourceProvider.getWidgetScriptSource('module1', '1');

assert.deepEqual(values, { moduleName: 'module1' });
assert.isTrue(localOrRemoteSource.calledTwice);
assert.isTrue(cdnSource.calledTwice);
verify(
appShell.showWarningMessage(
DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork().format('module1'),
anything(),
anything(),
anything()
)
).once();
});
});
});
Expand Down