diff --git a/package.nls.json b/package.nls.json
index 01c38a8b7111..c3aa71c3d06c 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -233,6 +233,7 @@
"StartPage.folderDesc": "- Open a
Folder
- Open a Workspace
",
"StartPage.badWebPanelFormatString": "{0} is not a valid file name
",
"Jupyter.extensionRequired": "The Jupyter extension is required to perform that task. Click Yes to open the Jupyter extension installation page.",
+ "Jupyter.extensionNotInstalled": "This feature is available in the Jupyter extension, which isn't currently installed.",
"TensorBoard.missingSourceFile": "We could not locate the requested source file on disk. Please manually specify the file.",
"TensorBoard.selectMissingSourceFile": "Choose File",
"TensorBoard.selectMissingSourceFileDescription": "The source file's contents may not match the original contents in the trace.",
diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts
index 8c8dc9aff5e9..bd6e00bd6083 100644
--- a/src/client/common/serviceRegistry.ts
+++ b/src/client/common/serviceRegistry.ts
@@ -120,6 +120,8 @@ import {
} from './types';
import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput';
import { Random } from './utils/random';
+import { JupyterNotInstalledNotificationHelper } from '../jupyter/jupyterNotInstalledNotificationHelper';
+import { IJupyterNotInstalledNotificationHelper } from '../jupyter/types';
export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS);
@@ -141,6 +143,10 @@ export function registerTypes(serviceManager: IServiceManager) {
IJupyterExtensionDependencyManager,
JupyterExtensionDependencyManager,
);
+ serviceManager.addSingleton(
+ IJupyterNotInstalledNotificationHelper,
+ JupyterNotInstalledNotificationHelper,
+ );
serviceManager.addSingleton(ICommandManager, CommandManager);
serviceManager.addSingleton(IConfigurationService, ConfigurationService);
serviceManager.addSingleton(IWorkspaceService, WorkspaceService);
diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts
index d266f9805eef..80fd801ba783 100644
--- a/src/client/common/utils/localize.ts
+++ b/src/client/common/utils/localize.ts
@@ -141,6 +141,11 @@ export namespace Jupyter {
'Jupyter.extensionRequired',
'The Jupyter extension is required to perform that task. Click Yes to open the Jupyter extension installation page.',
);
+
+ export const jupyterExtensionNotInstalled = localize(
+ 'Jupyter.extensionNotInstalled',
+ "This feature is available in the Jupyter extension, which isn't currently installed.",
+ );
}
export namespace TensorBoard {
diff --git a/src/client/jupyter/jupyterNotInstalledNotificationHelper.ts b/src/client/jupyter/jupyterNotInstalledNotificationHelper.ts
new file mode 100644
index 000000000000..328fa23ca955
--- /dev/null
+++ b/src/client/jupyter/jupyterNotInstalledNotificationHelper.ts
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { injectable, inject } from 'inversify';
+import { IApplicationShell, IJupyterExtensionDependencyManager } from '../common/application/types';
+import { IPersistentStateFactory } from '../common/types';
+import { Common, Jupyter } from '../common/utils/localize';
+import { sendTelemetryEvent } from '../telemetry';
+import { EventName } from '../telemetry/constants';
+import { IJupyterNotInstalledNotificationHelper, JupyterNotInstalledOrigin } from './types';
+
+export const jupyterExtensionNotInstalledKey = 'jupyterExtensionNotInstalledKey';
+
+@injectable()
+export class JupyterNotInstalledNotificationHelper implements IJupyterNotInstalledNotificationHelper {
+ constructor(
+ @inject(IApplicationShell) private appShell: IApplicationShell,
+ @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
+ @inject(IJupyterExtensionDependencyManager) private depsManager: IJupyterExtensionDependencyManager,
+ ) {}
+
+ public shouldShowJupypterExtensionNotInstalledPrompt(): boolean {
+ const doNotShowAgain = this.persistentState.createGlobalPersistentState(jupyterExtensionNotInstalledKey, false);
+
+ if (doNotShowAgain.value) {
+ return false;
+ }
+
+ const isInstalled = this.depsManager.isJupyterExtensionInstalled;
+
+ return !isInstalled;
+ }
+
+ public async jupyterNotInstalledPrompt(entrypoint: JupyterNotInstalledOrigin): Promise {
+ sendTelemetryEvent(EventName.JUPYTER_NOT_INSTALLED_NOTIFICATION_DISPLAYED, undefined, { entrypoint });
+
+ const prompts = [Common.doNotShowAgain()];
+ const telemetrySelections: ['Do not show again'] = ['Do not show again'];
+
+ const selection = await this.appShell.showInformationMessage(
+ Jupyter.jupyterExtensionNotInstalled(),
+ ...prompts,
+ );
+
+ sendTelemetryEvent(EventName.JUPYTER_NOT_INSTALLED_NOTIFICATION_ACTION, undefined, {
+ selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined,
+ });
+
+ if (!selection) {
+ return;
+ }
+
+ // Never show this prompt again
+ await this.persistentState
+ .createGlobalPersistentState(jupyterExtensionNotInstalledKey, false)
+ .updateValue(true);
+ }
+}
diff --git a/src/client/jupyter/types.ts b/src/client/jupyter/types.ts
index 5eb58c7cf2b2..14d0c868adbf 100644
--- a/src/client/jupyter/types.ts
+++ b/src/client/jupyter/types.ts
@@ -45,3 +45,17 @@ enum ColumnType {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IRowsResponse = any[];
+
+// Note: While #16102 is being worked on, this enum will be updated as we add ways to display this notification.
+export enum JupyterNotInstalledOrigin {
+ StartPageCreateBlankNotebook = 'startpage_create_blank_notebook',
+ StartPageCreateJupyterNotebook = 'startpage_create_jupyter_notebook',
+ StartPageCreateSampleNotebook = 'startpage_sample_notebook',
+ StartPageUseInteractiveWindow = 'startpage_use_interactive_window',
+}
+
+export const IJupyterNotInstalledNotificationHelper = Symbol('IJupyterNotInstalledNotificationHelper');
+export interface IJupyterNotInstalledNotificationHelper {
+ shouldShowJupypterExtensionNotInstalledPrompt(): boolean;
+ jupyterNotInstalledPrompt(entrypoint: JupyterNotInstalledOrigin): Promise;
+}
diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts
index c14cbed6d3b3..244a5a42b031 100644
--- a/src/client/telemetry/constants.ts
+++ b/src/client/telemetry/constants.ts
@@ -128,6 +128,9 @@ export enum EventName {
JEDI_LANGUAGE_SERVER_TELEMETRY = 'JEDI_LANGUAGE_SERVER.EVENT',
JEDI_LANGUAGE_SERVER_REQUEST = 'JEDI_LANGUAGE_SERVER.REQUEST',
+ JUPYTER_NOT_INSTALLED_NOTIFICATION_DISPLAYED = 'JUPYTER_NOT_INSTALLED_NOTIFICATION_DISPLAYED',
+ JUPYTER_NOT_INSTALLED_NOTIFICATION_ACTION = 'JUPYTER_NOT_INSTALLED_NOTIFICATION_ACTION',
+
TENSORBOARD_SESSION_LAUNCH = 'TENSORBOARD.SESSION_LAUNCH',
TENSORBOARD_SESSION_DURATION = 'TENSORBOARD.SESSION_DURATION',
TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION = 'TENSORBOARD.SESSION_DAEMON_STARTUP_DURATION',
diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts
index a6aa451997bf..d5b8c714b127 100644
--- a/src/client/telemetry/index.ts
+++ b/src/client/telemetry/index.ts
@@ -27,6 +27,7 @@ import {
import { TestProvider } from '../testing/types';
import { EventName, PlatformErrors } from './constants';
import type { LinterTrigger, TestTool } from './types';
+import { JupyterNotInstalledOrigin } from '../jupyter/types';
/**
* Checks whether telemetry is supported.
@@ -1750,6 +1751,31 @@ export interface IEventNamePropertyMapping {
terminal: TerminalShellType;
};
+ /**
+ * Telemetry event sent when the notification about the Jupyter extension not being installed is displayed.
+ * Since this notification will only be displayed after an action that requires the Jupyter extension,
+ * the telemetry event will include the action the user took, under the `entrypoint` property.
+ */
+ [EventName.JUPYTER_NOT_INSTALLED_NOTIFICATION_DISPLAYED]: {
+ /**
+ * Action that the user took to trigger the notification.
+ */
+ entrypoint: JupyterNotInstalledOrigin;
+ };
+
+ /**
+ * Telemetry event sent when the notification about the Jupyter extension not being installed is closed.
+ */
+ [EventName.JUPYTER_NOT_INSTALLED_NOTIFICATION_ACTION]: {
+ /**
+ * Action selected by the user in response to the notification:
+ * close the notification using the close button, or "Do not show again".
+ *
+ * @type {('Do not show again' | undefined)}
+ */
+ selection: 'Do not show again' | undefined;
+ };
+
[Telemetry.WebviewStyleUpdate]: never | undefined;
[Telemetry.WebviewMonacoStyleUpdate]: never | undefined;
[Telemetry.WebviewStartup]: { type: string };
diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts
index 1190cfca61c0..6d1b4b504b11 100644
--- a/src/test/common/moduleInstaller.test.ts
+++ b/src/test/common/moduleInstaller.test.ts
@@ -134,6 +134,8 @@ import { MockModuleInstaller } from '../mocks/moduleInstaller';
import { MockProcessService } from '../mocks/proc';
import { UnitTestIocContainer } from '../testing/serviceRegistry';
import { closeActiveWindows, initializeTest } from '../initialize';
+import { JupyterNotInstalledNotificationHelper } from '../../client/jupyter/jupyterNotInstalledNotificationHelper';
+import { IJupyterNotInstalledNotificationHelper } from '../../client/jupyter/types';
chaiUse(chaiAsPromised);
@@ -245,6 +247,10 @@ suite('Module Installer', () => {
IJupyterExtensionDependencyManager,
JupyterExtensionDependencyManager,
);
+ ioc.serviceManager.addSingleton(
+ IJupyterNotInstalledNotificationHelper,
+ JupyterNotInstalledNotificationHelper,
+ );
ioc.serviceManager.addSingleton(IBrowserService, BrowserService);
ioc.serviceManager.addSingleton(IHttpClient, HttpClient);
ioc.serviceManager.addSingleton(IFileDownloader, FileDownloader);
diff --git a/src/test/jupyter/jupyterNotInstalledNotificationHelper.unit.test.ts b/src/test/jupyter/jupyterNotInstalledNotificationHelper.unit.test.ts
new file mode 100644
index 000000000000..f82b9cb0c8a6
--- /dev/null
+++ b/src/test/jupyter/jupyterNotInstalledNotificationHelper.unit.test.ts
@@ -0,0 +1,151 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import * as assert from 'assert';
+import * as sinon from 'sinon';
+import { IApplicationShell, IJupyterExtensionDependencyManager } from '../../client/common/application/types';
+import { IPersistentStateFactory } from '../../client/common/types';
+import { Jupyter, Common } from '../../client/common/utils/localize';
+import {
+ jupyterExtensionNotInstalledKey,
+ JupyterNotInstalledNotificationHelper,
+} from '../../client/jupyter/jupyterNotInstalledNotificationHelper';
+import { JupyterNotInstalledOrigin } from '../../client/jupyter/types';
+
+suite('Jupyter not installed notification helper', () => {
+ teardown(() => {
+ sinon.restore();
+ });
+
+ test('Notification check should return false if the Jupyter extension is installed', () => {
+ const createGlobalPersistentStateStub = sinon
+ .stub()
+ .withArgs(jupyterExtensionNotInstalledKey, sinon.match.bool)
+ .returns({ value: undefined });
+
+ // Need to define 'isJupyterExtensionInstalled' for it to be stubbed.
+ const jupyterExtDependencyManager = {
+ isJupyterExtensionInstalled: false,
+ } as IJupyterExtensionDependencyManager;
+ const isJupyterExtensionInstalledStub = sinon.stub().returns(true);
+ sinon.stub(jupyterExtDependencyManager, 'isJupyterExtensionInstalled').get(isJupyterExtensionInstalledStub);
+
+ const notificationHelper = new JupyterNotInstalledNotificationHelper(
+ {} as IApplicationShell,
+ ({ createGlobalPersistentState: createGlobalPersistentStateStub } as unknown) as IPersistentStateFactory,
+ jupyterExtDependencyManager,
+ );
+
+ const result = notificationHelper.shouldShowJupypterExtensionNotInstalledPrompt();
+
+ assert.strictEqual(result, false);
+ sinon.assert.calledOnce(createGlobalPersistentStateStub);
+ sinon.assert.calledWith(createGlobalPersistentStateStub, jupyterExtensionNotInstalledKey, sinon.match.bool);
+ sinon.assert.calledOnce(isJupyterExtensionInstalledStub);
+ });
+
+ test('Notification check should return false if the doNotShowAgain persistent value is set', () => {
+ const createGlobalPersistentStateStub = sinon
+ .stub()
+ .withArgs(jupyterExtensionNotInstalledKey, sinon.match.bool)
+ .returns({ value: true });
+
+ const jupyterExtDependencyManager = {
+ isJupyterExtensionInstalled: false,
+ } as IJupyterExtensionDependencyManager;
+ const isJupyterExtensionInstalledStub = sinon.stub().returns(false);
+ sinon.stub(jupyterExtDependencyManager, 'isJupyterExtensionInstalled').get(isJupyterExtensionInstalledStub);
+
+ const notificationHelper = new JupyterNotInstalledNotificationHelper(
+ {} as IApplicationShell,
+ ({ createGlobalPersistentState: createGlobalPersistentStateStub } as unknown) as IPersistentStateFactory,
+ jupyterExtDependencyManager,
+ );
+
+ const result = notificationHelper.shouldShowJupypterExtensionNotInstalledPrompt();
+
+ assert.strictEqual(result, false);
+ sinon.assert.calledOnce(createGlobalPersistentStateStub);
+ sinon.assert.calledWith(createGlobalPersistentStateStub, jupyterExtensionNotInstalledKey, sinon.match.bool);
+ sinon.assert.notCalled(isJupyterExtensionInstalledStub);
+ });
+
+ test('Notification check should return true if the doNotShowAgain persistent value is not set and the Jupyter extension is not installed', () => {
+ const createGlobalPersistentStateStub = sinon
+ .stub()
+ .withArgs(jupyterExtensionNotInstalledKey, sinon.match.bool)
+ .returns({ value: undefined });
+
+ const jupyterExtDependencyManager = {
+ isJupyterExtensionInstalled: false,
+ } as IJupyterExtensionDependencyManager;
+ const isJupyterExtensionInstalledStub = sinon.stub().returns(false);
+ sinon.stub(jupyterExtDependencyManager, 'isJupyterExtensionInstalled').get(isJupyterExtensionInstalledStub);
+
+ const notificationHelper = new JupyterNotInstalledNotificationHelper(
+ {} as IApplicationShell,
+ ({ createGlobalPersistentState: createGlobalPersistentStateStub } as unknown) as IPersistentStateFactory,
+ (jupyterExtDependencyManager as unknown) as IJupyterExtensionDependencyManager,
+ );
+
+ const result = notificationHelper.shouldShowJupypterExtensionNotInstalledPrompt();
+
+ assert.strictEqual(result, true);
+ sinon.assert.calledOnce(createGlobalPersistentStateStub);
+ sinon.assert.calledWith(createGlobalPersistentStateStub, jupyterExtensionNotInstalledKey, sinon.match.bool);
+ sinon.assert.calledOnce(isJupyterExtensionInstalledStub);
+ });
+
+ test('Selecting "Do not show again" should set the doNotShowAgain persistent value', async () => {
+ const updateValueStub = sinon.stub();
+ const createGlobalPersistentStateStub = sinon
+ .stub()
+ .withArgs(jupyterExtensionNotInstalledKey, sinon.match.bool)
+ .returns({ updateValue: updateValueStub });
+
+ const showInformationMessageStub = sinon.stub().returns(Promise.resolve(Common.doNotShowAgain));
+
+ const notificationHelper = new JupyterNotInstalledNotificationHelper(
+ ({ showInformationMessage: showInformationMessageStub } as unknown) as IApplicationShell,
+ ({ createGlobalPersistentState: createGlobalPersistentStateStub } as unknown) as IPersistentStateFactory,
+ {} as IJupyterExtensionDependencyManager,
+ );
+ await notificationHelper.jupyterNotInstalledPrompt(JupyterNotInstalledOrigin.StartPageCreateBlankNotebook);
+
+ sinon.assert.calledOnce(createGlobalPersistentStateStub);
+ sinon.assert.calledOnce(showInformationMessageStub);
+ sinon.assert.calledWith(
+ showInformationMessageStub,
+ Jupyter.jupyterExtensionNotInstalled(),
+ Common.doNotShowAgain(),
+ );
+ sinon.assert.calledOnce(updateValueStub);
+ sinon.assert.calledWith(updateValueStub, true);
+ });
+
+ test('Selecting "Do not show again" should make the prompt check return false', async () => {
+ const persistentState: { value: boolean | undefined; updateValue: (v: boolean) => void } = {
+ value: undefined,
+ updateValue(v: boolean) {
+ this.value = v;
+ },
+ };
+ const createGlobalPersistentStateStub = sinon
+ .stub()
+ .withArgs(jupyterExtensionNotInstalledKey, sinon.match.bool)
+ .returns(persistentState);
+
+ const showInformationMessageStub = sinon.stub().returns(Promise.resolve(Common.doNotShowAgain));
+
+ const notificationHelper = new JupyterNotInstalledNotificationHelper(
+ ({ showInformationMessage: showInformationMessageStub } as unknown) as IApplicationShell,
+ ({ createGlobalPersistentState: createGlobalPersistentStateStub } as unknown) as IPersistentStateFactory,
+ {} as IJupyterExtensionDependencyManager,
+ );
+ await notificationHelper.jupyterNotInstalledPrompt(JupyterNotInstalledOrigin.StartPageCreateBlankNotebook);
+
+ const result = notificationHelper.shouldShowJupypterExtensionNotInstalledPrompt();
+
+ assert.strictEqual(result, false);
+ });
+});