diff --git a/extensions/notebook/src/dialog/configurePython/configurePathPage.ts b/extensions/notebook/src/dialog/configurePython/configurePathPage.ts index 02a27ddc9c..00ffce2e12 100644 --- a/extensions/notebook/src/dialog/configurePython/configurePathPage.ts +++ b/extensions/notebook/src/dialog/configurePython/configurePathPage.ts @@ -15,23 +15,37 @@ const localize = nls.loadMessageBundle(); export class ConfigurePathPage extends BasePage { private readonly BrowseButtonText = localize('configurePython.browseButtonText', "Browse"); - private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', "Python Install Location"); private readonly SelectFileLabel = localize('configurePython.selectFileLabel', "Select"); private pythonLocationDropdown: azdata.DropDownComponent; private pythonDropdownLoader: azdata.LoadingComponent; - private browseButton: azdata.ButtonComponent; private newInstallButton: azdata.RadioButtonComponent; private existingInstallButton: azdata.RadioButtonComponent; + private selectInstallEnabled: boolean; private usingCustomPath: boolean = false; public async initialize(): Promise { + let wizardDescription: string; + if (this.model.kernelName) { + wizardDescription = localize('configurePython.descriptionWithKernel', "The {0} kernel requires a Python runtime to be configured and dependencies to be installed.", this.model.kernelName); + } else { + wizardDescription = localize('configurePython.descriptionWithoutKernel', "Notebook kernels require a Python runtime to be configured and dependencies to be installed."); + } + let wizardDescriptionLabel = this.view.modelBuilder.text() + .withProperties({ + value: wizardDescription, + CSSStyles: { + 'padding': '0px', + 'margin': '0px' + } + }).component(); + this.pythonLocationDropdown = this.view.modelBuilder.dropDown() .withProperties({ value: undefined, values: [], - width: '100%' + width: '400px' }).component(); this.pythonDropdownLoader = this.view.modelBuilder.loadingComponent() .withItem(this.pythonLocationDropdown) @@ -39,17 +53,16 @@ export class ConfigurePathPage extends BasePage { loading: false }) .component(); - - this.browseButton = this.view.modelBuilder.button() + let browseButton = this.view.modelBuilder.button() .withProperties({ label: this.BrowseButtonText, width: '70px' }).component(); - this.browseButton.onDidClick(() => this.handleBrowse()); + browseButton.onDidClick(() => this.handleBrowse()); this.createInstallRadioButtons(this.view.modelBuilder, this.model.useExistingPython); - let formModel = this.view.modelBuilder.formContainer() + let selectInstallForm = this.view.modelBuilder.formContainer() .withFormItems([{ component: this.newInstallButton, title: localize('configurePython.installationType', "Installation Type") @@ -58,14 +71,66 @@ export class ConfigurePathPage extends BasePage { title: '' }, { component: this.pythonDropdownLoader, - title: this.LocationTextBoxTitle + title: localize('configurePython.locationTextBoxText', "Python Install Location") }, { - component: this.browseButton, + component: browseButton, title: '' }]).component(); + let selectInstallContainer = this.view.modelBuilder.divContainer() + .withItems([selectInstallForm]) + .withProperties({ + clickable: false + }).component(); - await this.view.initializeModel(formModel); + let allParentItems = [selectInstallContainer]; + if (this.model.pythonLocation) { + let installedPathTextBox = this.view.modelBuilder.inputBox().withProperties({ + value: this.model.pythonLocation, + enabled: false, + width: '400px' + }).component(); + let editPathButton = this.view.modelBuilder.button().withProperties({ + label: 'Edit', + width: '70px' + }).component(); + let editPathForm = this.view.modelBuilder.formContainer() + .withFormItems([{ + title: localize('configurePython.pythonConfigured', "Python runtime configured!"), + component: installedPathTextBox + }, { + title: '', + component: editPathButton + }]).component(); + let editPathContainer = this.view.modelBuilder.divContainer() + .withItems([editPathForm]) + .withProperties({ + clickable: false + }).component(); + allParentItems.push(editPathContainer); + + editPathButton.onDidClick(async () => { + editPathContainer.display = 'none'; + selectInstallContainer.display = 'block'; + this.selectInstallEnabled = true; + }); + selectInstallContainer.display = 'none'; + + this.selectInstallEnabled = false; + } else { + this.selectInstallEnabled = true; + } + + let parentContainer = this.view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }).component(); + parentContainer.addItem(wizardDescriptionLabel, { + CSSStyles: { + 'padding': '0px 30px 0px 30px' + } + }); + parentContainer.addItems(allParentItems); + + await this.view.initializeModel(parentContainer); await this.updatePythonPathsDropdown(this.model.useExistingPython); return true; @@ -75,19 +140,24 @@ export class ConfigurePathPage extends BasePage { } public async onPageLeave(): Promise { - let pythonLocation = utils.getDropdownValue(this.pythonLocationDropdown); - if (!pythonLocation || pythonLocation.length === 0) { - this.instance.showErrorMessage(this.instance.InvalidLocationMsg); + if (this.pythonDropdownLoader.loading) { return false; } + if (this.selectInstallEnabled) { + let pythonLocation = utils.getDropdownValue(this.pythonLocationDropdown); + if (!pythonLocation || pythonLocation.length === 0) { + this.instance.showErrorMessage(this.instance.InvalidLocationMsg); + return false; + } - this.model.pythonLocation = pythonLocation; - this.model.useExistingPython = !!this.existingInstallButton.checked; - + this.model.pythonLocation = pythonLocation; + this.model.useExistingPython = !!this.existingInstallButton.checked; + } return true; } private async updatePythonPathsDropdown(useExistingPython: boolean): Promise { + this.instance.wizard.nextButton.enabled = false; this.pythonDropdownLoader.loading = true; try { let pythonPaths: PythonPathInfo[]; @@ -121,6 +191,7 @@ export class ConfigurePathPage extends BasePage { values: dropdownValues }); } finally { + this.instance.wizard.nextButton.enabled = true; this.pythonDropdownLoader.loading = false; } } diff --git a/extensions/notebook/src/dialog/configurePython/configurePythonWizard.ts b/extensions/notebook/src/dialog/configurePython/configurePythonWizard.ts index 19a510b612..f310ff962b 100644 --- a/extensions/notebook/src/dialog/configurePython/configurePythonWizard.ts +++ b/extensions/notebook/src/dialog/configurePython/configurePythonWizard.ts @@ -55,6 +55,7 @@ export class ConfigurePythonWizard { kernelName: kernelName, pythonPathsPromise: this.pythonPathsPromise, installation: this.jupyterInstallation, + pythonLocation: JupyterServerInstallation.getPythonPathSetting(this.apiWrapper), useExistingPython: JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper) }; diff --git a/extensions/notebook/src/dialog/configurePython/pickPackagesPage.ts b/extensions/notebook/src/dialog/configurePython/pickPackagesPage.ts index 4b2ebbb450..383e13693b 100644 --- a/extensions/notebook/src/dialog/configurePython/pickPackagesPage.ts +++ b/extensions/notebook/src/dialog/configurePython/pickPackagesPage.ts @@ -6,20 +6,31 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import { BasePage } from './basePage'; -import { JupyterServerInstallation, PythonPkgDetails } from '../../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; import { python3DisplayName, pysparkDisplayName, sparkScalaDisplayName, sparkRDisplayName, powershellDisplayName, allKernelsName } from '../../common/constants'; import { getDropdownValue } from '../../common/utils'; const localize = nls.loadMessageBundle(); +interface RequiredPackageInfo { + name: string; + existingVersion: string; + requiredVersion: string; +} + +namespace cssStyles { + export const tableHeader = { 'text-align': 'left', 'font-weight': 'lighter', 'font-size': '10px', 'user-select': 'text', 'border': 'none' }; + export const tableRow = { 'border-top': 'solid 1px #ccc', 'border-bottom': 'solid 1px #ccc', 'border-left': 'none', 'border-right': 'none' }; +} + export class PickPackagesPage extends BasePage { private kernelLabel: azdata.TextComponent | undefined; private kernelDropdown: azdata.DropDownComponent | undefined; private requiredPackagesTable: azdata.DeclarativeTableComponent; private packageTableSpinner: azdata.LoadingComponent; - private installedPackagesPromise: Promise; - private installedPackages: PythonPkgDetails[]; + private packageVersionRetrieval: Promise; + private packageVersionMap = new Map(); public async initialize(): Promise { if (this.model.kernelName) { @@ -39,22 +50,46 @@ export class PickPackagesPage extends BasePage { }); } + let nameColumn = localize('configurePython.pkgNameColumn', "Name"); + let existingVersionColumn = localize('configurePython.existingVersionColumn', "Existing Version"); + let requiredVersionColumn = localize('configurePython.requiredVersionColumn', "Required Version"); this.requiredPackagesTable = this.view.modelBuilder.declarativeTable().withProperties({ columns: [{ - displayName: localize('configurePython.pkgNameColumn', "Name"), + displayName: nameColumn, + ariaLabel: nameColumn, valueType: azdata.DeclarativeDataType.string, isReadOnly: true, - width: '200px' + width: '200px', + headerCssStyles: { + ...cssStyles.tableHeader + }, + rowCssStyles: { + ...cssStyles.tableRow + } }, { - displayName: localize('configurePython.existingVersionColumn', "Existing Version"), + displayName: existingVersionColumn, + ariaLabel: existingVersionColumn, valueType: azdata.DeclarativeDataType.string, isReadOnly: true, - width: '200px' + width: '200px', + headerCssStyles: { + ...cssStyles.tableHeader + }, + rowCssStyles: { + ...cssStyles.tableRow + } }, { - displayName: localize('configurePython.requiredVersionColumn', "Required Version"), + displayName: requiredVersionColumn, + ariaLabel: requiredVersionColumn, valueType: azdata.DeclarativeDataType.string, isReadOnly: true, - width: '200px' + width: '200px', + headerCssStyles: { + ...cssStyles.tableHeader + }, + rowCssStyles: { + ...cssStyles.tableRow + } }], data: [[]] }).component(); @@ -74,9 +109,16 @@ export class PickPackagesPage extends BasePage { } public async onPageEnter(): Promise { + this.packageVersionMap.clear(); let pythonExe = JupyterServerInstallation.getPythonExePath(this.model.pythonLocation, this.model.useExistingPython); - this.installedPackagesPromise = this.model.installation.getInstalledPipPackages(pythonExe); - this.installedPackages = undefined; + this.packageVersionRetrieval = this.model.installation.getInstalledPipPackages(pythonExe) + .then(installedPackages => { + if (installedPackages) { + installedPackages.forEach(pkg => { + this.packageVersionMap.set(pkg.name, pkg.version); + }); + } + }); if (this.kernelDropdown) { if (this.model.kernelName) { @@ -89,45 +131,39 @@ export class PickPackagesPage extends BasePage { } public async onPageLeave(): Promise { - return true; + return !this.packageTableSpinner.loading; } private async updateRequiredPackages(kernelName: string): Promise { + this.instance.wizard.doneButton.enabled = false; this.packageTableSpinner.loading = true; try { - let pkgVersionMap = new Map(); - // Fetch list of required packages for the specified kernel - let requiredPackages = JupyterServerInstallation.getRequiredPackagesForKernel(kernelName); + let requiredPkgVersions: RequiredPackageInfo[] = []; + let requiredPackages = this.model.installation.getRequiredPackagesForKernel(kernelName); requiredPackages.forEach(pkg => { - pkgVersionMap.set(pkg.name, { currentVersion: undefined, newVersion: pkg.version }); + requiredPkgVersions.push({ name: pkg.name, existingVersion: undefined, requiredVersion: pkg.version }); }); // For each required package, check if there is another version of that package already installed - if (!this.installedPackages) { - this.installedPackages = await this.installedPackagesPromise; - } - this.installedPackages.forEach(pkg => { - let info = pkgVersionMap.get(pkg.name); - if (info) { - info.currentVersion = pkg.version; - pkgVersionMap.set(pkg.name, info); + await this.packageVersionRetrieval; + requiredPkgVersions.forEach(pkgVersion => { + let installedPackageVersion = this.packageVersionMap.get(pkgVersion.name); + if (installedPackageVersion) { + pkgVersion.existingVersion = installedPackageVersion; } }); - if (pkgVersionMap.size > 0) { - let packageData = []; - for (let [key, value] of pkgVersionMap.entries()) { - packageData.push([key, value.currentVersion ?? '-', value.newVersion]); - } - this.requiredPackagesTable.data = packageData; + if (requiredPkgVersions.length > 0) { + this.requiredPackagesTable.data = requiredPkgVersions.map(pkg => [pkg.name, pkg.existingVersion ?? '-', pkg.requiredVersion]); this.model.packagesToInstall = requiredPackages; } else { - this.instance.showErrorMessage(localize('msgUnsupportedKernel', "Could not retrieve packages for unsupported kernel {0}", kernelName)); + this.instance.showErrorMessage(localize('msgUnsupportedKernel', "Could not retrieve packages for kernel {0}", kernelName)); this.requiredPackagesTable.data = [['-', '-', '-']]; this.model.packagesToInstall = undefined; } } finally { + this.instance.wizard.doneButton.enabled = true; this.packageTableSpinner.loading = false; } } diff --git a/extensions/notebook/src/jupyter/jupyterController.ts b/extensions/notebook/src/jupyter/jupyterController.ts index 4625100492..45f604f81b 100644 --- a/extensions/notebook/src/jupyter/jupyterController.ts +++ b/extensions/notebook/src/jupyter/jupyterController.ts @@ -251,8 +251,7 @@ export class JupyterController implements vscode.Disposable { } public doConfigurePython(jupyterInstaller: JupyterServerInstallation): void { - let enablePreviewFeatures = this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); - if (enablePreviewFeatures) { + if (jupyterInstaller.previewFeaturesEnabled) { let pythonWizard = new ConfigurePythonWizard(this.apiWrapper, jupyterInstaller); pythonWizard.start().catch((err: any) => { this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index 3af23decb7..134cd7ae54 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -34,7 +34,6 @@ const msgTaskName = localize('msgTaskName', "Installing Notebook dependencies"); const msgInstallPkgStart = localize('msgInstallPkgStart', "Installing Notebook dependencies, see Tasks view for more information"); const msgInstallPkgFinish = localize('msgInstallPkgFinish', "Notebook dependencies installation is complete"); const msgPythonRunningError = localize('msgPythonRunningError', "Cannot overwrite an existing Python installation while python is running. Please close any active notebooks before proceeding."); -const msgSkipPythonInstall = localize('msgSkipPythonInstall', "Python already exists at the specific location. Skipping install."); const msgWaitingForInstall = localize('msgWaitingForInstall', "Another Python installation is currently in progress. Waiting for it to complete."); function msgDependenciesInstallationFailed(errorMessage: string): string { return localize('msgDependenciesInstallationFailed', "Installing Notebook dependencies failed with error: {0}", errorMessage); } function msgDownloadPython(platform: string, pythonDownloadUrl: string): string { return localize('msgDownloadPython', "Downloading local python for platform: {0} to {1}", platform, pythonDownloadUrl); } @@ -110,6 +109,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { private readonly _expectedCondaPackages: PythonPkgDetails[]; private _kernelSetupCache: Map; + private readonly _requiredKernelPackages: Map; + private readonly _requiredPackagesSet: Set; constructor(extensionPath: string, outputChannel: OutputChannel, apiWrapper: ApiWrapper, pythonInstallationPath?: string) { this.extensionPath = extensionPath; @@ -129,28 +130,66 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } this._kernelSetupCache = new Map(); + this._requiredKernelPackages = new Map(); + + let jupyterPkg = { + name: 'jupyter', + version: '1.0.0' + }; + this._requiredKernelPackages.set(constants.python3DisplayName, [jupyterPkg]); + + let powershellPkg = { + name: 'powershell-kernel', + version: '0.1.3' + }; + this._requiredKernelPackages.set(constants.powershellDisplayName, [jupyterPkg, powershellPkg]); + + let sparkPackages = [ + jupyterPkg, + { + name: 'sparkmagic', + version: '0.12.9' + }, { + name: 'pandas', + version: '0.24.2' + }, { + name: 'prose-codeaccelerator', + version: '1.3.0' + }]; + this._requiredKernelPackages.set(constants.pysparkDisplayName, sparkPackages); + this._requiredKernelPackages.set(constants.sparkScalaDisplayName, sparkPackages); + this._requiredKernelPackages.set(constants.sparkRDisplayName, sparkPackages); + + let allPackages = sparkPackages.concat(powershellPkg); + this._requiredKernelPackages.set(constants.allKernelsName, allPackages); + + this._requiredPackagesSet = new Set(); + allPackages.forEach(pkg => { + this._requiredPackagesSet.add(pkg.name); + }); } private async installDependencies(backgroundOperation: azdata.BackgroundOperation, forceInstall: boolean, specificPackages?: PythonPkgDetails[]): Promise { - if (!(await utils.exists(this._pythonExecutable)) || forceInstall || this._usingExistingPython) { - window.showInformationMessage(msgInstallPkgStart); + window.showInformationMessage(msgInstallPkgStart); - this.outputChannel.show(true); - this.outputChannel.appendLine(msgInstallPkgProgress); - backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgInstallPkgProgress); + this.outputChannel.show(true); + this.outputChannel.appendLine(msgInstallPkgProgress); + backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgInstallPkgProgress); - try { + try { + let pythonExists = await utils.exists(this._pythonExecutable); + if (!pythonExists || forceInstall) { await this.installPythonPackage(backgroundOperation, this._usingExistingPython, this._pythonInstallationPath, this.outputChannel); - await this.upgradePythonPackages(false, forceInstall, specificPackages); - } catch (err) { - this.outputChannel.appendLine(msgDependenciesInstallationFailed(utils.getErrorMessage(err))); - throw err; } - - this.outputChannel.appendLine(msgInstallPkgFinish); - backgroundOperation.updateStatus(azdata.TaskStatus.Succeeded, msgInstallPkgFinish); - window.showInformationMessage(msgInstallPkgFinish); + await this.upgradePythonPackages(false, forceInstall, specificPackages); + } catch (err) { + this.outputChannel.appendLine(msgDependenciesInstallationFailed(utils.getErrorMessage(err))); + throw err; } + + this.outputChannel.appendLine(msgInstallPkgFinish); + backgroundOperation.updateStatus(azdata.TaskStatus.Succeeded, msgInstallPkgFinish); + window.showInformationMessage(msgInstallPkgFinish); } public installPythonPackage(backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: OutputChannel): Promise { @@ -388,41 +427,29 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { this._usingExistingPython = installSettings.existingPython; await this.configurePackagePaths(); - let updateConfig = async () => { - let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey); - await notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global); - await notebookConfig.update(constants.existingPythonConfigKey, this._usingExistingPython, ConfigurationTarget.Global); - await this.configurePackagePaths(); - }; + this.apiWrapper.startBackgroundOperation({ + displayName: msgTaskName, + description: msgTaskName, + isCancelable: false, + operation: op => { + this.installDependencies(op, forceInstall, installSettings.specificPackages) + .then(async () => { + let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey); + await notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global); + await notebookConfig.update(constants.existingPythonConfigKey, this._usingExistingPython, ConfigurationTarget.Global); + await this.configurePackagePaths(); - if (!(await utils.exists(this._pythonExecutable)) || forceInstall || this._usingExistingPython) { - this.apiWrapper.startBackgroundOperation({ - displayName: msgTaskName, - description: msgTaskName, - isCancelable: false, - operation: op => { - this.installDependencies(op, forceInstall, installSettings.specificPackages) - .then(async () => { - await updateConfig(); - this._installCompletion.resolve(); - this._installInProgress = false; - }) - .catch(err => { - let errorMsg = msgDependenciesInstallationFailed(utils.getErrorMessage(err)); - op.updateStatus(azdata.TaskStatus.Failed, errorMsg); - this._installCompletion.reject(errorMsg); - this._installInProgress = false; - }); - } - }); - } else { - // Python executable already exists, but the path setting wasn't defined, - // so update it here - await updateConfig(); - this._installCompletion.resolve(); - this._installInProgress = false; - this.apiWrapper.showInfoMessage(msgSkipPythonInstall); - } + this._installCompletion.resolve(); + this._installInProgress = false; + }) + .catch(err => { + let errorMsg = msgDependenciesInstallationFailed(utils.getErrorMessage(err)); + op.updateStatus(azdata.TaskStatus.Failed, errorMsg); + this._installCompletion.reject(errorMsg); + this._installInProgress = false; + }); + } + }); return this._installCompletion.promise; } @@ -430,12 +457,20 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { * Opens a dialog for configuring the installation path for the Notebook Python dependencies. */ public async promptForPythonInstall(kernelDisplayName: string): Promise { - if (!JupyterServerInstallation.isPythonInstalled(this.apiWrapper)) { - let enablePreviewFeatures = this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); - if (enablePreviewFeatures) { + if (this._installInProgress) { + this.apiWrapper.showInfoMessage(msgWaitingForInstall); + return this._installCompletion.promise; + } + + let isPythonInstalled = JupyterServerInstallation.isPythonInstalled(this.apiWrapper); + let areRequiredPackagesInstalled = await this.areRequiredPackagesInstalled(kernelDisplayName); + if (!isPythonInstalled || !areRequiredPackagesInstalled) { + if (this.previewFeaturesEnabled) { let pythonWizard = new ConfigurePythonWizard(this.apiWrapper, this); await pythonWizard.start(kernelDisplayName, true); - return pythonWizard.setupComplete; + return pythonWizard.setupComplete.then(() => { + this._kernelSetupCache.set(kernelDisplayName, true); + }); } else { let pythonDialog = new ConfigurePythonDialog(this.apiWrapper, this); return pythonDialog.showDialog(true); @@ -453,12 +488,11 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } let requiredPackages: PythonPkgDetails[]; - let enablePreviewFeatures = this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); - if (enablePreviewFeatures) { + if (this.previewFeaturesEnabled) { if (this._kernelSetupCache.get(kernelName)) { return; } - requiredPackages = JupyterServerInstallation.getRequiredPackagesForKernel(kernelName); + requiredPackages = this.getRequiredPackagesForKernel(kernelName); } this._installInProgress = true; @@ -477,6 +511,27 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { return this._installCompletion.promise; } + private async areRequiredPackagesInstalled(kernelDisplayName: string): Promise { + if (this._kernelSetupCache.get(kernelDisplayName)) { + return true; + } + + let installedPackages = await this.getInstalledPipPackages(); + let installedPackageMap = new Map(); + installedPackages.forEach(pkg => { + installedPackageMap.set(pkg.name, pkg.version); + }); + let requiredPackages = this.getRequiredPackagesForKernel(kernelDisplayName); + for (let pkg of requiredPackages) { + let installedVersion = installedPackageMap.get(pkg.name); + if (!installedVersion || utils.comparePackageVersions(installedVersion, pkg.version) < 0) { + return false; + } + } + this._kernelSetupCache.set(kernelDisplayName, true); + return true; + } + private async upgradePythonPackages(promptForUpgrade: boolean, forceInstall: boolean, specificPackages?: PythonPkgDetails[]): Promise { let expectedCondaPackages: PythonPkgDetails[]; let expectedPipPackages: PythonPkgDetails[]; @@ -625,6 +680,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } public uninstallPipPackages(packages: PythonPkgDetails[]): Promise { + for (let pkg of packages) { + if (this._requiredPackagesSet.has(pkg.name)) { + this._kernelSetupCache.clear(); + break; + } + } + let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' '); let cmd = `"${this.pythonExecutable}" -m pip uninstall -y ${packagesStr}`; return this.executeStreamedCommand(cmd); @@ -669,6 +731,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } public uninstallCondaPackages(packages: PythonPkgDetails[]): Promise { + for (let pkg of packages) { + if (this._requiredPackagesSet.has(pkg.name)) { + this._kernelSetupCache.clear(); + break; + } + } + let condaExe = this.getCondaExePath(); let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' '); let cmd = `"${condaExe}" uninstall -y ${packagesStr}`; @@ -751,7 +820,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { return useExistingPython; } - private static getPythonPathSetting(apiWrapper: ApiWrapper): string { + public static getPythonPathSetting(apiWrapper: ApiWrapper): string { let path = undefined; if (apiWrapper) { let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey); @@ -813,53 +882,12 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { return undefined; } - public static getRequiredPackagesForKernel(kernelName: string): PythonPkgDetails[] { - let packages = [{ - name: 'jupyter', - version: '1.0.0' - }]; - switch (kernelName) { - case constants.python3DisplayName: - break; - case constants.pysparkDisplayName: - case constants.sparkScalaDisplayName: - case constants.sparkRDisplayName: - packages.push({ - name: 'sparkmagic', - version: '0.12.9' - }, { - name: 'pandas', - version: '0.24.2' - }, { - name: 'prose-codeaccelerator', - version: '1.3.0' - }); - break; - case constants.powershellDisplayName: - packages.push({ - name: 'powershell-kernel', - version: '0.1.3' - }); - break; - case constants.allKernelsName: - packages.push({ - name: 'sparkmagic', - version: '0.12.9' - }, { - name: 'pandas', - version: '0.24.2' - }, { - name: 'prose-codeaccelerator', - version: '1.3.0' - }, { - name: 'powershell-kernel', - version: '0.1.3' - }); - break; - default: - return undefined; - } - return packages; + public getRequiredPackagesForKernel(kernelName: string): PythonPkgDetails[] { + return this._requiredKernelPackages.get(kernelName) ?? []; + } + + public get previewFeaturesEnabled(): boolean { + return this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); } } diff --git a/extensions/notebook/src/jupyter/jupyterServerManager.ts b/extensions/notebook/src/jupyter/jupyterServerManager.ts index bb21573a15..83aed59f47 100644 --- a/extensions/notebook/src/jupyter/jupyterServerManager.ts +++ b/extensions/notebook/src/jupyter/jupyterServerManager.ts @@ -109,7 +109,9 @@ export class LocalJupyterServerManager implements nb.ServerManager, vscode.Dispo private async doStartServer(kernelSpec: nb.IKernelSpec): Promise { // We can't find or create servers until the installation is complete let installation = this.options.jupyterInstallation; await installation.promptForPythonInstall(kernelSpec.display_name); - await installation.promptForPackageUpgrade(kernelSpec.display_name); + if (!installation.previewFeaturesEnabled) { + await installation.promptForPackageUpgrade(kernelSpec.display_name); + } this._apiWrapper.setCommandContext(CommandContext.NotebookPythonInstalled, true); // Calculate the path to use as the notebook-dir for Jupyter based on the path of the uri of the diff --git a/extensions/notebook/src/jupyter/jupyterSessionManager.ts b/extensions/notebook/src/jupyter/jupyterSessionManager.ts index 79775701e9..07e809485c 100644 --- a/extensions/notebook/src/jupyter/jupyterSessionManager.ts +++ b/extensions/notebook/src/jupyter/jupyterSessionManager.ts @@ -237,7 +237,11 @@ export class JupyterSession implements nb.ISession { public async changeKernel(kernelInfo: nb.IKernelSpec): Promise { if (this._installation) { try { - await this._installation.promptForPackageUpgrade(kernelInfo.display_name); + if (this._installation.previewFeaturesEnabled) { + await this._installation.promptForPythonInstall(kernelInfo.display_name); + } else { + await this._installation.promptForPackageUpgrade(kernelInfo.display_name); + } } catch (err) { // Have to swallow the error here to prevent hangs when changing back to the old kernel. console.error(err.toString()); diff --git a/extensions/notebook/src/test/common.ts b/extensions/notebook/src/test/common.ts index b1190f4d9e..cc15e2958f 100644 --- a/extensions/notebook/src/test/common.ts +++ b/extensions/notebook/src/test/common.ts @@ -337,6 +337,45 @@ class TestFormContainer extends TestComponentBase implements azdata.FormContaine } } +class TestDivContainer extends TestComponentBase implements azdata.DivContainer { + onDidClick: vscode.Event; + items: azdata.Component[] = []; + clearItems(): void { + } + addItems(itemConfigs: azdata.Component[], itemLayout?: azdata.DivItemLayout): void { + } + addItem(component: azdata.Component, itemLayout?: azdata.DivItemLayout): void { + } + insertItem(component: azdata.Component, index: number, itemLayout?: azdata.DivItemLayout): void { + } + removeItem(component: azdata.Component): boolean { + return true; + } + setLayout(layout: azdata.DivLayout): void { + } + setItemLayout(component: azdata.Component, layout: azdata.DivItemLayout): void { + } +} + +class TestFlexContainer extends TestComponentBase implements azdata.FlexContainer { + items: azdata.Component[] = []; + clearItems(): void { + } + addItems(itemConfigs: azdata.Component[], itemLayout?: azdata.FlexItemLayout): void { + } + addItem(component: azdata.Component, itemLayout?: azdata.FlexItemLayout): void { + } + insertItem(component: azdata.Component, index: number, itemLayout?: azdata.FlexItemLayout): void { + } + removeItem(component: azdata.Component): boolean { + return true; + } + setLayout(layout: azdata.FlexLayout): void { + } + setItemLayout(component: azdata.Component, layout: azdata.FlexItemLayout): void { + } +} + class TestComponentBuilder implements azdata.ComponentBuilder { constructor(private _component: T) { } @@ -383,6 +422,34 @@ export function createViewContext(): TestContext { withLayout: () => formBuilder }); + let div: azdata.DivContainer = new TestDivContainer(); + let divBuilder: azdata.DivBuilder = Object.assign({}, { + component: () => div, + addFormItem: () => { }, + insertFormItem: () => { }, + removeFormItem: () => true, + addFormItems: () => { }, + withFormItems: () => divBuilder, + withProperties: () => divBuilder, + withValidation: () => divBuilder, + withItems: () => divBuilder, + withLayout: () => divBuilder + }); + + let flex: azdata.FlexContainer = new TestFlexContainer(); + let flexBuilder: azdata.FlexBuilder = Object.assign({}, { + component: () => flex, + addFormItem: () => { }, + insertFormItem: () => { }, + removeFormItem: () => true, + addFormItems: () => { }, + withFormItems: () => flexBuilder, + withProperties: () => flexBuilder, + withValidation: () => flexBuilder, + withItems: () => flexBuilder, + withLayout: () => flexBuilder + }); + let view: azdata.ModelView = { onClosed: undefined!, connection: undefined!, @@ -398,7 +465,9 @@ export function createViewContext(): TestContext { dropDown: () => dropdownBuilder, declarativeTable: () => declarativeTableBuilder, formContainer: () => formBuilder, - loadingComponent: () => loadingBuilder + loadingComponent: () => loadingBuilder, + divContainer: () => divBuilder, + flexContainer: () => flexBuilder } }; @@ -412,4 +481,13 @@ export interface TestContext { view: azdata.ModelView; onClick: vscode.EventEmitter; } + +export class TestButton implements azdata.window.Button { + label: string; + enabled: boolean; + hidden: boolean; + constructor(private onClickEmitter: vscode.EventEmitter) { + } + onClick: vscode.Event = this.onClickEmitter.event; +} //#endregion diff --git a/extensions/notebook/src/test/configurePython.test.ts b/extensions/notebook/src/test/configurePython.test.ts index 4cded30d7f..8c6c5e7187 100644 --- a/extensions/notebook/src/test/configurePython.test.ts +++ b/extensions/notebook/src/test/configurePython.test.ts @@ -12,7 +12,8 @@ import { ConfigurePathPage } from '../dialog/configurePython/configurePathPage'; import * as should from 'should'; import { PickPackagesPage } from '../dialog/configurePython/pickPackagesPage'; import { python3DisplayName, allKernelsName } from '../common/constants'; -import { TestContext, createViewContext } from './common'; +import { TestContext, createViewContext, TestButton } from './common'; +import { EventEmitter } from 'vscode'; describe('Configure Python Wizard', function () { let apiWrapper: ApiWrapper = new ApiWrapper(); @@ -23,11 +24,20 @@ describe('Configure Python Wizard', function () { beforeEach(() => { let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation); mockInstall.setup(i => i.getInstalledPipPackages(TypeMoq.It.isAnyString())).returns(() => Promise.resolve([])); + mockInstall.setup(i => i.getRequiredPackagesForKernel(TypeMoq.It.isAnyString())).returns(() => [{ name: 'TestPkg', version: '1.0.0'}]); testInstallation = mockInstall.object; - let mockWizard = TypeMoq.Mock.ofType(ConfigurePythonWizard); - mockWizard.setup(w => w.showErrorMessage(TypeMoq.It.isAnyString())); - testWizard = mockWizard.object; + let mockDoneButton = new TestButton(new EventEmitter()); + let mockNextButton = new TestButton(new EventEmitter()); + + let mockWizard = TypeMoq.Mock.ofType(); + mockWizard.setup(w => w.doneButton).returns(() => mockDoneButton); + mockWizard.setup(w => w.nextButton).returns(() => mockNextButton); + + let mockPythonWizard = TypeMoq.Mock.ofType(ConfigurePythonWizard); + mockPythonWizard.setup(w => w.showErrorMessage(TypeMoq.It.isAnyString())); + mockPythonWizard.setup(w => w.wizard).returns(() => mockWizard.object); + testWizard = mockPythonWizard.object; viewContext = createViewContext(); }); @@ -82,6 +92,7 @@ describe('Configure Python Wizard', function () { // First page, so onPageEnter should do nothing await should(configurePathPage.onPageEnter()).be.resolved(); + should(testWizard.wizard.nextButton.enabled).be.true(); should(await configurePathPage.onPageLeave()).be.true(); should(model.useExistingPython).be.true(); @@ -108,7 +119,8 @@ describe('Configure Python Wizard', function () { should(await pickPackagesPage.onPageLeave()).be.true(); await should(pickPackagesPage.onPageEnter()).be.resolved(); - should(model.packagesToInstall).be.deepEqual(JupyterServerInstallation.getRequiredPackagesForKernel(allKernelsName)); + should(testWizard.wizard.doneButton.enabled).be.true(); + should(model.packagesToInstall).be.deepEqual(testInstallation.getRequiredPackagesForKernel(allKernelsName)); }); it('Undefined kernel test', async () => { @@ -128,6 +140,6 @@ describe('Configure Python Wizard', function () { should((pickPackagesPage).kernelDropdown).not.be.undefined(); await should(pickPackagesPage.onPageEnter()).be.resolved(); - should(model.packagesToInstall).be.deepEqual(JupyterServerInstallation.getRequiredPackagesForKernel(python3DisplayName)); + should(model.packagesToInstall).be.deepEqual(testInstallation.getRequiredPackagesForKernel(python3DisplayName)); }); });