+
);
diff --git a/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts b/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts
index 814a01c2c550..954742cf86e9 100644
--- a/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts
+++ b/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts
@@ -18,7 +18,8 @@ import {
CommonReducerArg,
ILoadIPyWidgetClassFailureAction,
IOpenSettingsAction,
- LoadIPyWidgetClassLoadAction
+ LoadIPyWidgetClassLoadAction,
+ NotifyIPyWidgeWidgetVersionNotSupportedAction
} from './types';
export namespace CommonEffects {
@@ -242,9 +243,11 @@ export namespace CommonEffects {
"Widgets require us to download supporting files from a 3rd party website. Click
here to enable this or click
here for more information. (Error loading {0}:{1})."
).format(arg.payload.data.moduleName, arg.payload.data.moduleVersion);
}
+ // Preserve existing error messages.
+ const existingErrorMessage = current.uiSideError ? `${current.uiSideError}\n` : '';
newVMs[index] = Helpers.asCellViewModel({
...current,
- uiSideError: errorMessage
+ uiSideError: `${existingErrorMessage}${errorMessage}`
});
// Make sure to tell the extension so it can log telemetry.
@@ -258,6 +261,42 @@ export namespace CommonEffects {
return arg.prevState;
}
}
+ export function notifyAboutUnsupportedWidgetVersions(
+ arg: CommonReducerArg
+ ): IMainState {
+ // Find the first currently executing cell and add an error to its output
+ let index = arg.prevState.cellVMs.findIndex((c) => c.cell.state === CellState.executing);
+
+ // If there isn't one, then find the latest that matches the current execution count.
+ if (index < 0) {
+ index = arg.prevState.cellVMs.findIndex(
+ (c) => c.cell.data.execution_count === arg.prevState.currentExecutionCount
+ );
+ }
+ if (index >= 0 && arg.prevState.cellVMs[index].cell.data.cell_type === 'code') {
+ const newVMs = [...arg.prevState.cellVMs];
+ const current = arg.prevState.cellVMs[index];
+
+ const errorMessage = getLocString(
+ 'DataScience.qgridWidgetScriptVersionCompatibilityWarning',
+ "Unable to load a compatible version of the widget 'qgrid'. Consider downgrading to version 1.1.1."
+ );
+ newVMs[index] = Helpers.asCellViewModel({
+ ...current,
+ uiSideError: errorMessage
+ });
+
+ // Make sure to tell the extension so it can log telemetry.
+ postActionToExtension(arg, InteractiveWindowMessages.IPyWidgetWidgetVersionNotSupported, arg.payload.data);
+
+ return {
+ ...arg.prevState,
+ cellVMs: newVMs
+ };
+ } else {
+ return arg.prevState;
+ }
+ }
export function handleIPyWidgetRenderFailure(arg: CommonReducerArg): IMainState {
// Make sure to tell the extension so it can log telemetry.
postActionToExtension(arg, InteractiveWindowMessages.IPyWidgetRenderFailure, arg.payload.data);
diff --git a/src/datascience-ui/interactive-common/redux/reducers/types.ts b/src/datascience-ui/interactive-common/redux/reducers/types.ts
index 2a5f8d5e6910..9fddb76d7570 100644
--- a/src/datascience-ui/interactive-common/redux/reducers/types.ts
+++ b/src/datascience-ui/interactive-common/redux/reducers/types.ts
@@ -58,6 +58,7 @@ export enum CommonActionType {
IPYWIDGET_RENDER_FAILURE = 'action.ipywidget_render_failure',
LOAD_IPYWIDGET_CLASS_SUCCESS = 'action.load_ipywidget_class_success',
LOAD_IPYWIDGET_CLASS_FAILURE = 'action.load_ipywidget_class_failure',
+ IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED = 'action.ipywidget_widget_version_not_supported',
LOADED_ALL_CELLS = 'action.loaded_all_cells',
LINK_CLICK = 'action.link_click',
MOVE_CELL_DOWN = 'action.move_cell_down',
@@ -134,6 +135,7 @@ export type CommonActionTypeMapping = {
[CommonActionType.FOCUS_INPUT]: never | undefined;
[CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS]: LoadIPyWidgetClassLoadAction;
[CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE]: ILoadIPyWidgetClassFailureAction;
+ [CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED]: NotifyIPyWidgeWidgetVersionNotSupportedAction;
[CommonActionType.IPYWIDGET_RENDER_FAILURE]: Error;
};
@@ -232,5 +234,9 @@ export type LoadIPyWidgetClassLoadAction = {
moduleName: string;
moduleVersion: string;
};
+export type NotifyIPyWidgeWidgetVersionNotSupportedAction = {
+ moduleName: 'qgrid';
+ moduleVersion: string;
+};
export type CommonAction = ActionWithPayload;
diff --git a/src/datascience-ui/ipywidgets/container.tsx b/src/datascience-ui/ipywidgets/container.tsx
index 1072ccf33849..ef5fed6a19d5 100644
--- a/src/datascience-ui/ipywidgets/container.tsx
+++ b/src/datascience-ui/ipywidgets/container.tsx
@@ -19,10 +19,12 @@ import {
CommonAction,
CommonActionType,
ILoadIPyWidgetClassFailureAction,
- LoadIPyWidgetClassLoadAction
+ LoadIPyWidgetClassLoadAction,
+ NotifyIPyWidgeWidgetVersionNotSupportedAction
} from '../interactive-common/redux/reducers/types';
import { IStore } from '../interactive-common/redux/store';
import { PostOffice } from '../react-common/postOffice';
+import { warnAboutWidgetVersionsThatAreNotSupported } from './incompatibleWidgetHandler';
import { WidgetManager } from './manager';
import { registerScripts } from './requirejsRegistry';
@@ -38,6 +40,7 @@ export class WidgetManagerComponent extends React.Component {
string,
{ deferred: Deferred; timer: NodeJS.Timeout | number | undefined }
>();
+ private readonly registeredWidgetSources = new Map();
private timedoutWaitingForWidgetsToGetLoaded?: boolean;
private widgetsCanLoadFromCDN: boolean = false;
private readonly loaderSettings = {
@@ -78,6 +81,7 @@ export class WidgetManagerComponent extends React.Component {
// This happens when we have restarted a kernel.
// If user changed the kernel, then some widgets might exist now and some might now.
this.widgetSourceRequests.clear();
+ this.registeredWidgetSources.clear();
}
return true;
}
@@ -108,6 +112,7 @@ export class WidgetManagerComponent extends React.Component {
// Now resolve promises (anything that was waiting for modules to get registered can carry on).
sources.forEach((source) => {
+ this.registeredWidgetSources.set(source.moduleName, source);
// We have fetched the script sources for all of these modules.
// In some cases we might not have the source, meaning we don't have it or couldn't find it.
let request = this.widgetSourceRequests.get(source.moduleName);
@@ -167,6 +172,21 @@ export class WidgetManagerComponent extends React.Component {
}
};
}
+ private createWidgetVersionNotSupportedErrorAction(
+ moduleName: 'qgrid',
+ moduleVersion: string
+ ): CommonAction {
+ return {
+ type: CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED,
+ payload: {
+ messageDirection: 'incoming',
+ data: {
+ moduleName,
+ moduleVersion
+ }
+ }
+ };
+ }
private async handleLoadError(
className: string,
moduleName: string,
@@ -233,10 +253,25 @@ export class WidgetManagerComponent extends React.Component {
{ moduleName, moduleVersion }
);
- return request.deferred.promise.catch((ex) =>
- // tslint:disable-next-line: no-console
- console.error(`Failed to load Widget Script from Extension for for ${moduleName}, ${moduleVersion}`, ex)
- );
+ return request.deferred.promise
+ .then(() => {
+ const widgetSource = this.registeredWidgetSources.get(moduleName);
+ if (widgetSource) {
+ warnAboutWidgetVersionsThatAreNotSupported(
+ widgetSource,
+ moduleVersion,
+ this.widgetsCanLoadFromCDN,
+ (info) =>
+ this.props.store.dispatch(
+ this.createWidgetVersionNotSupportedErrorAction(info.moduleName, info.moduleVersion)
+ )
+ );
+ }
+ })
+ .catch((ex) =>
+ // tslint:disable-next-line: no-console
+ console.error(`Failed to load Widget Script from Extension for for ${moduleName}, ${moduleVersion}`, ex)
+ );
}
private handleLoadSuccess(className: string, moduleName: string, moduleVersion: string) {
diff --git a/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts b/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts
new file mode 100644
index 000000000000..a6d3ce4e1207
--- /dev/null
+++ b/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+'use strict';
+
+import * as semver from 'semver';
+import { WidgetScriptSource } from '../../client/datascience/ipywidgets/types';
+const supportedVersionOfQgrid = '1.1.1';
+const qgridModuleName = 'qgrid';
+
+/**
+ * For now only warns about qgrid.
+ * Warn user about qgrid versions > 1.1.1 (we know CDN isn't available for newer versions and local widget source will not work).
+ * Recommend to downgrade to 1.1.1.
+ * Returns `true` if a warning has been displayed.
+ */
+export function warnAboutWidgetVersionsThatAreNotSupported(
+ widgetSource: WidgetScriptSource,
+ moduleVersion: string,
+ cdnSupported: boolean,
+ errorDispatcher: (info: { moduleName: typeof qgridModuleName; moduleVersion: string }) => void
+) {
+ // if widget exists on CDN or CDN is disabled, get out.
+ if (widgetSource.source === 'cdn' || !cdnSupported) {
+ return false;
+ }
+ // Warn about qrid.
+ if (widgetSource.moduleName !== qgridModuleName) {
+ return false;
+ }
+ // We're only interested in versions > 1.1.1.
+ try {
+ // If we have an exact version, & if that is <= 1.1.1, then no warning needs to be displayed.
+ if (!moduleVersion.startsWith('^') && semver.compare(moduleVersion, supportedVersionOfQgrid) <= 0) {
+ return false;
+ }
+ // If we have a version range, then check the range.
+ // Basically if our minimum version 1.1.1 is met, then nothing to do.
+ // Eg. requesting script source for version `^1.3.0`.
+ if (moduleVersion.startsWith('^') && semver.satisfies(supportedVersionOfQgrid, moduleVersion)) {
+ return false;
+ }
+ } catch {
+ return false;
+ }
+ errorDispatcher({ moduleName: widgetSource.moduleName, moduleVersion });
+}
diff --git a/src/datascience-ui/native-editor/redux/reducers/index.ts b/src/datascience-ui/native-editor/redux/reducers/index.ts
index 049954ac03c8..d12ac9167a58 100644
--- a/src/datascience-ui/native-editor/redux/reducers/index.ts
+++ b/src/datascience-ui/native-editor/redux/reducers/index.ts
@@ -60,6 +60,7 @@ export const reducerMap: Partial = {
[CommonActionType.UNMOUNT]: Creation.unmount,
[CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS]: CommonEffects.handleLoadIPyWidgetClassSuccess,
[CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE]: CommonEffects.handleLoadIPyWidgetClassFailure,
+ [CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED]: CommonEffects.notifyAboutUnsupportedWidgetVersions,
// Messages from the webview (some are ignored)
[InteractiveWindowMessages.StartCell]: Creation.startCell,
diff --git a/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts b/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts
new file mode 100644
index 000000000000..48f8c2170fda
--- /dev/null
+++ b/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+import { assert } from 'chai';
+import { warnAboutWidgetVersionsThatAreNotSupported } from '../../../datascience-ui/ipywidgets/incompatibleWidgetHandler';
+
+// tslint:disable: max-func-body-length no-any
+suite('Data Science - Incompatible Widgets', () => {
+ suite('Using qgrid widget with CDN turned on', () => {
+ async function testLoadingQgrid(versionToLoad: string, warningExpectedToBeDisplayed: boolean) {
+ let warningDisplayed = false;
+ warnAboutWidgetVersionsThatAreNotSupported(
+ { moduleName: 'qgrid' },
+ versionToLoad,
+ true,
+ () => (warningDisplayed = true)
+ );
+
+ assert.equal(warningDisplayed, warningExpectedToBeDisplayed);
+ }
+ test('Widget script is not found for qgrid@1.1.0, then do not display a warning', async () => {
+ // This test just ensures we never display warnings for 1.1.0.
+ // This will never happen as the file exists on CDN.
+ // Hence gurantees that we'll not display when not required.
+ await testLoadingQgrid('1.1.0', false);
+ });
+ test('Widget script is not found for qgrid@1.1.1, then do not display a warning', async () => {
+ // This test just ensures we never display warnings for 1.1.0.
+ // This will never happen as the file exists on CDN.
+ // Hence gurantees that we'll not display when not required.
+ await testLoadingQgrid('1.1.1', false);
+ });
+ test('Widget script is not found for qgrid@1.1.2, then display a warning', async () => {
+ // We know there are no scripts on CDN for > 1.1.1
+ await testLoadingQgrid('1.1.2', true);
+ });
+ test('Widget script is not found for qgrid@^1.1.2, then display a warning', async () => {
+ // We know there are no scripts on CDN for > 1.1.1
+ await testLoadingQgrid('^1.1.2', true);
+ });
+ test('Widget script is not found for qgrid@1.3.0, then display a warning', async () => {
+ // We know there are no scripts on CDN for > 1.1.1
+ await testLoadingQgrid('1.3.0', true);
+ });
+ test('Widget script is not found for qgrid@^1.3.0, then display a warning', async () => {
+ // We know there are no scripts on CDN for > 1.1.1
+ await testLoadingQgrid('^1.3.0', true);
+ });
+ });
+});