mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 01:25:38 -05:00
Update Configure Python dialog to allow packages to be installed for specific kernels. (#10286)
This commit is contained in:
33
extensions/notebook/src/dialog/configurePython/basePage.ts
Normal file
33
extensions/notebook/src/dialog/configurePython/basePage.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { ConfigurePythonModel, ConfigurePythonWizard } from './configurePythonWizard';
|
||||
import { ApiWrapper } from '../../common/apiWrapper';
|
||||
|
||||
export abstract class BasePage {
|
||||
|
||||
constructor(protected readonly apiWrapper: ApiWrapper,
|
||||
protected readonly instance: ConfigurePythonWizard,
|
||||
protected readonly wizardPage: azdata.window.WizardPage,
|
||||
protected readonly model: ConfigurePythonModel,
|
||||
protected readonly view: azdata.ModelView) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method constructs all the elements of the page.
|
||||
*/
|
||||
public async abstract initialize(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* This method is called when the user is entering the page.
|
||||
*/
|
||||
public async abstract onPageEnter(): Promise<void>;
|
||||
|
||||
/**
|
||||
* This method is called when the user is leaving the page.
|
||||
*/
|
||||
public async abstract onPageLeave(): Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import { BasePage } from './basePage';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
|
||||
import { PythonPathInfo } from '../pythonPathLookup';
|
||||
import * as utils from '../../common/utils';
|
||||
|
||||
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 usingCustomPath: boolean = false;
|
||||
|
||||
public async initialize(): Promise<boolean> {
|
||||
this.pythonLocationDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProperties<azdata.DropDownProperties>({
|
||||
value: undefined,
|
||||
values: [],
|
||||
width: '100%'
|
||||
}).component();
|
||||
this.pythonDropdownLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withItem(this.pythonLocationDropdown)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: false
|
||||
})
|
||||
.component();
|
||||
|
||||
this.browseButton = this.view.modelBuilder.button()
|
||||
.withProperties<azdata.ButtonProperties>({
|
||||
label: this.BrowseButtonText,
|
||||
width: '70px'
|
||||
}).component();
|
||||
this.browseButton.onDidClick(() => this.handleBrowse());
|
||||
|
||||
this.createInstallRadioButtons(this.view.modelBuilder, this.model.useExistingPython);
|
||||
|
||||
let formModel = this.view.modelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
component: this.newInstallButton,
|
||||
title: localize('configurePython.installationType', "Installation Type")
|
||||
}, {
|
||||
component: this.existingInstallButton,
|
||||
title: ''
|
||||
}, {
|
||||
component: this.pythonDropdownLoader,
|
||||
title: this.LocationTextBoxTitle
|
||||
}, {
|
||||
component: this.browseButton,
|
||||
title: ''
|
||||
}]).component();
|
||||
|
||||
await this.view.initializeModel(formModel);
|
||||
|
||||
await this.updatePythonPathsDropdown(this.model.useExistingPython);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<boolean> {
|
||||
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;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async updatePythonPathsDropdown(useExistingPython: boolean): Promise<void> {
|
||||
this.pythonDropdownLoader.loading = true;
|
||||
try {
|
||||
let pythonPaths: PythonPathInfo[];
|
||||
let dropdownValues: azdata.CategoryValue[];
|
||||
if (useExistingPython) {
|
||||
pythonPaths = await this.model.pythonPathsPromise;
|
||||
if (pythonPaths && pythonPaths.length > 0) {
|
||||
dropdownValues = pythonPaths.map(path => {
|
||||
return {
|
||||
displayName: localize('configurePythyon.dropdownPathLabel', "{0} (Python {1})", path.installDir, path.version),
|
||||
name: path.installDir
|
||||
};
|
||||
});
|
||||
} else {
|
||||
dropdownValues = [{
|
||||
displayName: localize('configurePythyon.noVersionsFound', "No supported Python versions found."),
|
||||
name: ''
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
let defaultPath = JupyterServerInstallation.DefaultPythonLocation;
|
||||
dropdownValues = [{
|
||||
displayName: localize('configurePythyon.defaultPathLabel', "{0} (Default)", defaultPath),
|
||||
name: defaultPath
|
||||
}];
|
||||
}
|
||||
|
||||
this.usingCustomPath = false;
|
||||
await this.pythonLocationDropdown.updateProperties({
|
||||
value: dropdownValues[0],
|
||||
values: dropdownValues
|
||||
});
|
||||
} finally {
|
||||
this.pythonDropdownLoader.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private createInstallRadioButtons(modelBuilder: azdata.ModelBuilder, useExistingPython: boolean): void {
|
||||
let buttonGroup = 'installationType';
|
||||
this.newInstallButton = modelBuilder.radioButton()
|
||||
.withProperties<azdata.RadioButtonProperties>({
|
||||
name: buttonGroup,
|
||||
label: localize('configurePython.newInstall', "New Python installation"),
|
||||
checked: !useExistingPython
|
||||
}).component();
|
||||
this.newInstallButton.onDidClick(() => {
|
||||
this.updatePythonPathsDropdown(false)
|
||||
.catch(err => {
|
||||
this.instance.showErrorMessage(utils.getErrorMessage(err));
|
||||
});
|
||||
});
|
||||
|
||||
this.existingInstallButton = modelBuilder.radioButton()
|
||||
.withProperties<azdata.RadioButtonProperties>({
|
||||
name: buttonGroup,
|
||||
label: localize('configurePython.existingInstall', "Use existing Python installation"),
|
||||
checked: useExistingPython
|
||||
}).component();
|
||||
this.existingInstallButton.onDidClick(() => {
|
||||
this.updatePythonPathsDropdown(true)
|
||||
.catch(err => {
|
||||
this.instance.showErrorMessage(utils.getErrorMessage(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleBrowse(): Promise<void> {
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
defaultUri: vscode.Uri.file(utils.getUserHome()),
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: this.SelectFileLabel
|
||||
};
|
||||
|
||||
let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options);
|
||||
if (fileUris?.length > 0 && fileUris[0]) {
|
||||
let existingValues = <azdata.CategoryValue[]>this.pythonLocationDropdown.values;
|
||||
let filePath = fileUris[0].fsPath;
|
||||
let newValue = {
|
||||
displayName: localize('configurePythyon.customPathLabel', "{0} (Custom)", filePath),
|
||||
name: filePath
|
||||
};
|
||||
|
||||
if (this.usingCustomPath) {
|
||||
existingValues[0] = newValue;
|
||||
} else {
|
||||
existingValues.unshift(newValue);
|
||||
this.usingCustomPath = true;
|
||||
}
|
||||
|
||||
await this.pythonLocationDropdown.updateProperties({
|
||||
value: existingValues[0],
|
||||
values: existingValues
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as azdata from 'azdata';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as utils from '../../common/utils';
|
||||
|
||||
import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
|
||||
import { ApiWrapper } from '../../common/apiWrapper';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import { PythonPathLookup, PythonPathInfo } from '../pythonPathLookup';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class ConfigurePythonDialog {
|
||||
private dialog: azdata.window.Dialog;
|
||||
|
||||
private readonly DialogTitle = localize('configurePython.dialogName', "Configure Python for Notebooks");
|
||||
private readonly InstallButtonText = localize('configurePython.okButtonText', "Install");
|
||||
private readonly CancelButtonText = localize('configurePython.cancelButtonText', "Cancel");
|
||||
private readonly BrowseButtonText = localize('configurePython.browseButtonText', "Browse");
|
||||
private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', "Python Install Location");
|
||||
private readonly SelectFileLabel = localize('configurePython.selectFileLabel', "Select");
|
||||
private readonly InstallationNote = localize('configurePython.installNote', "This installation will take some time. It is recommended to not close the application until the installation is complete.");
|
||||
private readonly InvalidLocationMsg = localize('configurePython.invalidLocationMsg', "The specified install location is invalid.");
|
||||
private readonly PythonNotFoundMsg = localize('configurePython.pythonNotFoundMsg', "No python installation was found at the specified location.");
|
||||
|
||||
private pythonLocationDropdown: azdata.DropDownComponent;
|
||||
private pythonDropdownLoader: azdata.LoadingComponent;
|
||||
private browseButton: azdata.ButtonComponent;
|
||||
private newInstallButton: azdata.RadioButtonComponent;
|
||||
private existingInstallButton: azdata.RadioButtonComponent;
|
||||
|
||||
private setupComplete: Deferred<void>;
|
||||
private pythonPathsPromise: Promise<PythonPathInfo[]>;
|
||||
private usingCustomPath: boolean;
|
||||
|
||||
constructor(private apiWrapper: ApiWrapper, private jupyterInstallation: JupyterServerInstallation) {
|
||||
this.setupComplete = new Deferred<void>();
|
||||
this.pythonPathsPromise = (new PythonPathLookup()).getSuggestions();
|
||||
this.usingCustomPath = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog to configure python installation for notebooks.
|
||||
* @param rejectOnCancel Specifies whether an error should be thrown after clicking Cancel.
|
||||
* @returns A promise that is resolved when the python installation completes.
|
||||
*/
|
||||
public showDialog(rejectOnCancel: boolean = false): Promise<void> {
|
||||
this.dialog = azdata.window.createModelViewDialog(this.DialogTitle);
|
||||
|
||||
this.initializeContent();
|
||||
|
||||
this.dialog.okButton.label = this.InstallButtonText;
|
||||
this.dialog.cancelButton.label = this.CancelButtonText;
|
||||
this.dialog.cancelButton.onClick(() => {
|
||||
if (rejectOnCancel) {
|
||||
this.setupComplete.reject(localize('configurePython.pythonInstallDeclined', "Python installation was declined."));
|
||||
} else {
|
||||
this.setupComplete.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(() => this.handleInstall());
|
||||
|
||||
azdata.window.openDialog(this.dialog);
|
||||
|
||||
return this.setupComplete.promise;
|
||||
}
|
||||
|
||||
private initializeContent(): void {
|
||||
this.dialog.registerContent(async view => {
|
||||
this.pythonLocationDropdown = view.modelBuilder.dropDown()
|
||||
.withProperties<azdata.DropDownProperties>({
|
||||
value: undefined,
|
||||
values: [],
|
||||
width: '100%'
|
||||
}).component();
|
||||
this.pythonDropdownLoader = view.modelBuilder.loadingComponent()
|
||||
.withItem(this.pythonLocationDropdown)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: false
|
||||
})
|
||||
.component();
|
||||
|
||||
this.browseButton = view.modelBuilder.button()
|
||||
.withProperties<azdata.ButtonProperties>({
|
||||
label: this.BrowseButtonText,
|
||||
width: '70px'
|
||||
}).component();
|
||||
this.browseButton.onDidClick(() => this.handleBrowse());
|
||||
|
||||
let installationNoteText = view.modelBuilder.text().withProperties({
|
||||
value: this.InstallationNote
|
||||
}).component();
|
||||
let noteWrapper = view.modelBuilder.flexContainer().component();
|
||||
noteWrapper.addItem(installationNoteText, {
|
||||
flex: '1 1 auto',
|
||||
CSSStyles: {
|
||||
'margin-top': '60px',
|
||||
'padding-left': '15px',
|
||||
'padding-right': '15px',
|
||||
'border': '1px solid'
|
||||
}
|
||||
});
|
||||
|
||||
let useExistingPython = JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper);
|
||||
this.createInstallRadioButtons(view.modelBuilder, useExistingPython);
|
||||
|
||||
let formModel = view.modelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
component: this.newInstallButton,
|
||||
title: localize('configurePython.installationType', "Installation Type")
|
||||
}, {
|
||||
component: this.existingInstallButton,
|
||||
title: ''
|
||||
}, {
|
||||
component: this.pythonDropdownLoader,
|
||||
title: this.LocationTextBoxTitle
|
||||
}, {
|
||||
component: this.browseButton,
|
||||
title: ''
|
||||
}, {
|
||||
component: noteWrapper,
|
||||
title: ''
|
||||
}]).component();
|
||||
|
||||
await view.initializeModel(formModel);
|
||||
|
||||
await this.updatePythonPathsDropdown(useExistingPython);
|
||||
});
|
||||
}
|
||||
|
||||
private async updatePythonPathsDropdown(useExistingPython: boolean): Promise<void> {
|
||||
await this.pythonDropdownLoader.updateProperties({ loading: true });
|
||||
try {
|
||||
let pythonPaths: PythonPathInfo[];
|
||||
let dropdownValues: azdata.CategoryValue[];
|
||||
if (useExistingPython) {
|
||||
pythonPaths = await this.pythonPathsPromise;
|
||||
if (pythonPaths && pythonPaths.length > 0) {
|
||||
dropdownValues = pythonPaths.map(path => {
|
||||
return {
|
||||
displayName: `${path.installDir} (Python ${path.version})`,
|
||||
name: path.installDir
|
||||
};
|
||||
});
|
||||
} else {
|
||||
dropdownValues = [{
|
||||
displayName: 'No supported Python versions found.',
|
||||
name: ''
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
let defaultPath = JupyterServerInstallation.DefaultPythonLocation;
|
||||
dropdownValues = [{
|
||||
displayName: `${defaultPath} (Default)`,
|
||||
name: defaultPath
|
||||
}];
|
||||
}
|
||||
|
||||
this.usingCustomPath = false;
|
||||
await this.pythonLocationDropdown.updateProperties({
|
||||
value: dropdownValues[0],
|
||||
values: dropdownValues
|
||||
});
|
||||
} finally {
|
||||
await this.pythonDropdownLoader.updateProperties({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
private createInstallRadioButtons(modelBuilder: azdata.ModelBuilder, useExistingPython: boolean): void {
|
||||
let buttonGroup = 'installationType';
|
||||
this.newInstallButton = modelBuilder.radioButton()
|
||||
.withProperties<azdata.RadioButtonProperties>({
|
||||
name: buttonGroup,
|
||||
label: localize('configurePython.newInstall', "New Python installation"),
|
||||
checked: !useExistingPython
|
||||
}).component();
|
||||
this.newInstallButton.onDidClick(() => {
|
||||
this.existingInstallButton.checked = false;
|
||||
this.updatePythonPathsDropdown(false)
|
||||
.catch(err => {
|
||||
this.showErrorMessage(utils.getErrorMessage(err));
|
||||
});
|
||||
});
|
||||
|
||||
this.existingInstallButton = modelBuilder.radioButton()
|
||||
.withProperties<azdata.RadioButtonProperties>({
|
||||
name: buttonGroup,
|
||||
label: localize('configurePython.existingInstall', "Use existing Python installation"),
|
||||
checked: useExistingPython
|
||||
}).component();
|
||||
this.existingInstallButton.onDidClick(() => {
|
||||
this.newInstallButton.checked = false;
|
||||
this.updatePythonPathsDropdown(true)
|
||||
.catch(err => {
|
||||
this.showErrorMessage(utils.getErrorMessage(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInstall(): Promise<boolean> {
|
||||
let pythonLocation = (this.pythonLocationDropdown.value as azdata.CategoryValue).name;
|
||||
if (!pythonLocation || pythonLocation.length === 0) {
|
||||
this.showErrorMessage(this.InvalidLocationMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
let useExistingPython = !!this.existingInstallButton.checked;
|
||||
try {
|
||||
let isValid = await this.isFileValid(pythonLocation);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useExistingPython) {
|
||||
let exePath = JupyterServerInstallation.getPythonExePath(pythonLocation, true);
|
||||
let pythonExists = await utils.exists(exePath);
|
||||
if (!pythonExists) {
|
||||
this.showErrorMessage(this.PythonNotFoundMsg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.showErrorMessage(utils.getErrorMessage(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't wait on installation, since there's currently no Cancel functionality
|
||||
this.jupyterInstallation.startInstallProcess(false, { installPath: pythonLocation, existingPython: useExistingPython })
|
||||
.then(() => {
|
||||
this.setupComplete.resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
this.setupComplete.reject(utils.getErrorMessage(err));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async isFileValid(pythonLocation: string): Promise<boolean> {
|
||||
let self = this;
|
||||
try {
|
||||
const stats = await fs.stat(pythonLocation);
|
||||
if (stats.isFile()) {
|
||||
self.showErrorMessage(self.InvalidLocationMsg);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore error if folder doesn't exist, since it will be
|
||||
// created during installation
|
||||
if (err.code !== 'ENOENT') {
|
||||
self.showErrorMessage(err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async handleBrowse(): Promise<void> {
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
defaultUri: vscode.Uri.file(utils.getUserHome()),
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
openLabel: this.SelectFileLabel
|
||||
};
|
||||
|
||||
let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options);
|
||||
if (fileUris && fileUris[0]) {
|
||||
let existingValues = <azdata.CategoryValue[]>this.pythonLocationDropdown.values;
|
||||
let filePath = fileUris[0].fsPath;
|
||||
let newValue = {
|
||||
displayName: `${filePath} (Custom)`,
|
||||
name: filePath
|
||||
};
|
||||
|
||||
if (this.usingCustomPath) {
|
||||
existingValues[0] = newValue;
|
||||
} else {
|
||||
existingValues.unshift(newValue);
|
||||
this.usingCustomPath = true;
|
||||
}
|
||||
|
||||
await this.pythonLocationDropdown.updateProperties({
|
||||
value: existingValues[0],
|
||||
values: existingValues
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private showErrorMessage(message: string): void {
|
||||
this.dialog.message = {
|
||||
text: message,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as azdata from 'azdata';
|
||||
import { BasePage } from './basePage';
|
||||
import { ConfigurePathPage } from './configurePathPage';
|
||||
import { PickPackagesPage } from './pickPackagesPage';
|
||||
import { JupyterServerInstallation, PythonPkgDetails, PythonInstallSettings } from '../../jupyter/jupyterServerInstallation';
|
||||
import * as utils from '../../common/utils';
|
||||
import { promises as fs } from 'fs';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import { PythonPathInfo, PythonPathLookup } from '../pythonPathLookup';
|
||||
import { ApiWrapper } from '../../common/apiWrapper';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface ConfigurePythonModel {
|
||||
kernelName: string;
|
||||
pythonLocation: string;
|
||||
useExistingPython: boolean;
|
||||
pythonPathsPromise: Promise<PythonPathInfo[]>;
|
||||
packagesToInstall: PythonPkgDetails[];
|
||||
installation: JupyterServerInstallation;
|
||||
}
|
||||
|
||||
export class ConfigurePythonWizard {
|
||||
private readonly InstallButtonText = localize('configurePython.okButtonText', "Install");
|
||||
public readonly InvalidLocationMsg = localize('configurePython.invalidLocationMsg', "The specified install location is invalid.");
|
||||
private readonly PythonNotFoundMsg = localize('configurePython.pythonNotFoundMsg', "No Python installation was found at the specified location.");
|
||||
|
||||
private _wizard: azdata.window.Wizard;
|
||||
private model: ConfigurePythonModel;
|
||||
|
||||
private _setupComplete: Deferred<void>;
|
||||
private pythonPathsPromise: Promise<PythonPathInfo[]>;
|
||||
|
||||
constructor(private apiWrapper: ApiWrapper, private jupyterInstallation: JupyterServerInstallation) {
|
||||
this._setupComplete = new Deferred<void>();
|
||||
this.pythonPathsPromise = (new PythonPathLookup()).getSuggestions();
|
||||
}
|
||||
|
||||
public get wizard(): azdata.window.Wizard {
|
||||
return this._wizard;
|
||||
}
|
||||
|
||||
public get setupComplete(): Promise<void> {
|
||||
return this._setupComplete.promise;
|
||||
}
|
||||
|
||||
public async start(kernelName?: string, rejectOnCancel?: boolean, ...args: any[]): Promise<void> {
|
||||
this.model = <ConfigurePythonModel>{
|
||||
kernelName: kernelName,
|
||||
pythonPathsPromise: this.pythonPathsPromise,
|
||||
installation: this.jupyterInstallation,
|
||||
useExistingPython: JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper)
|
||||
};
|
||||
|
||||
let pages: Map<number, BasePage> = new Map<number, BasePage>();
|
||||
|
||||
let wizardTitle: string;
|
||||
if (kernelName) {
|
||||
wizardTitle = localize('configurePython.wizardNameWithKernel', 'Configure Python to run {0} kernel', kernelName);
|
||||
} else {
|
||||
wizardTitle = localize('configurePython.wizardNameWithoutKernel', 'Configure Python to run kernels');
|
||||
}
|
||||
this._wizard = azdata.window.createWizard(wizardTitle);
|
||||
let page0 = azdata.window.createWizardPage(localize('configurePython.page0Name', 'Configure Python Runtime'));
|
||||
let page1 = azdata.window.createWizardPage(localize('configurePython.page1Name', 'Install Dependencies'));
|
||||
|
||||
page0.registerContent(async (view) => {
|
||||
let configurePathPage = new ConfigurePathPage(this.apiWrapper, this, page0, this.model, view);
|
||||
pages.set(0, configurePathPage);
|
||||
await configurePathPage.initialize();
|
||||
await configurePathPage.onPageEnter();
|
||||
});
|
||||
|
||||
page1.registerContent(async (view) => {
|
||||
let pickPackagesPage = new PickPackagesPage(this.apiWrapper, this, page1, this.model, view);
|
||||
pages.set(1, pickPackagesPage);
|
||||
await pickPackagesPage.initialize();
|
||||
});
|
||||
|
||||
this._wizard.doneButton.label = this.InstallButtonText;
|
||||
this._wizard.cancelButton.onClick(() => {
|
||||
if (rejectOnCancel) {
|
||||
this._setupComplete.reject(localize('configurePython.pythonInstallDeclined', "Python installation was declined."));
|
||||
} else {
|
||||
this._setupComplete.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this._wizard.onPageChanged(async info => {
|
||||
let newPage = pages.get(info.newPage);
|
||||
if (newPage) {
|
||||
await newPage.onPageEnter();
|
||||
}
|
||||
});
|
||||
|
||||
this._wizard.registerNavigationValidator(async (info) => {
|
||||
let lastPage = pages.get(info.lastPage);
|
||||
let newPage = pages.get(info.newPage);
|
||||
|
||||
// Hit "next" on last page, so handle submit
|
||||
let nextOnLastPage = !newPage && lastPage instanceof PickPackagesPage;
|
||||
if (nextOnLastPage) {
|
||||
return await this.handlePackageInstall();
|
||||
}
|
||||
|
||||
if (lastPage) {
|
||||
let pageValid = await lastPage.onPageLeave();
|
||||
if (!pageValid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.clearStatusMessage();
|
||||
return true;
|
||||
});
|
||||
|
||||
this._wizard.generateScriptButton.hidden = true;
|
||||
this._wizard.pages = [page0, page1];
|
||||
this._wizard.open();
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
await this._wizard.close();
|
||||
}
|
||||
|
||||
public showErrorMessage(errorMsg: string) {
|
||||
this._wizard.message = <azdata.window.DialogMessage>{
|
||||
text: errorMsg,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
|
||||
public clearStatusMessage() {
|
||||
this._wizard.message = undefined;
|
||||
}
|
||||
|
||||
private async handlePackageInstall(): Promise<boolean> {
|
||||
let pythonLocation = this.model.pythonLocation;
|
||||
let useExistingPython = this.model.useExistingPython;
|
||||
try {
|
||||
let isValid = await this.isFileValid(pythonLocation);
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useExistingPython) {
|
||||
let exePath = JupyterServerInstallation.getPythonExePath(pythonLocation, true);
|
||||
let pythonExists = await utils.exists(exePath);
|
||||
if (!pythonExists) {
|
||||
this.showErrorMessage(this.PythonNotFoundMsg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.showErrorMessage(utils.getErrorMessage(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't wait on installation, since there's currently no Cancel functionality
|
||||
let installSettings: PythonInstallSettings = {
|
||||
installPath: pythonLocation,
|
||||
existingPython: useExistingPython,
|
||||
specificPackages: this.model.packagesToInstall
|
||||
};
|
||||
this.jupyterInstallation.startInstallProcess(false, installSettings)
|
||||
.then(() => {
|
||||
this._setupComplete.resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
this._setupComplete.reject(utils.getErrorMessage(err));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async isFileValid(pythonLocation: string): Promise<boolean> {
|
||||
try {
|
||||
const stats = await fs.stat(pythonLocation);
|
||||
if (stats.isFile()) {
|
||||
this.showErrorMessage(this.InvalidLocationMsg);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore error if folder doesn't exist, since it will be
|
||||
// created during installation
|
||||
if (err.code !== 'ENOENT') {
|
||||
this.showErrorMessage(err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { BasePage } from './basePage';
|
||||
import { JupyterServerInstallation, PythonPkgDetails } from '../../jupyter/jupyterServerInstallation';
|
||||
import { python3DisplayName, pysparkDisplayName, sparkScalaDisplayName, sparkRDisplayName, powershellDisplayName, allKernelsName } from '../../common/constants';
|
||||
import { getDropdownValue } from '../../common/utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
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<PythonPkgDetails[]>;
|
||||
private installedPackages: PythonPkgDetails[];
|
||||
|
||||
public async initialize(): Promise<boolean> {
|
||||
if (this.model.kernelName) {
|
||||
// Wizard was started for a specific kernel, so don't populate any other options
|
||||
this.kernelLabel = this.view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: this.model.kernelName
|
||||
}).component();
|
||||
} else {
|
||||
let dropdownValues = [python3DisplayName, pysparkDisplayName, sparkScalaDisplayName, sparkRDisplayName, powershellDisplayName, allKernelsName];
|
||||
this.kernelDropdown = this.view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
|
||||
value: dropdownValues[0],
|
||||
values: dropdownValues,
|
||||
width: '300px'
|
||||
}).component();
|
||||
this.kernelDropdown.onValueChanged(async value => {
|
||||
await this.updateRequiredPackages(value.selected);
|
||||
});
|
||||
}
|
||||
|
||||
this.requiredPackagesTable = this.view.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
columns: [{
|
||||
displayName: localize('configurePython.pkgNameColumn', "Name"),
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '200px'
|
||||
}, {
|
||||
displayName: localize('configurePython.existingVersionColumn', "Existing Version"),
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '200px'
|
||||
}, {
|
||||
displayName: localize('configurePython.requiredVersionColumn', "Required Version"),
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '200px'
|
||||
}],
|
||||
data: [[]]
|
||||
}).component();
|
||||
|
||||
this.packageTableSpinner = this.view.modelBuilder.loadingComponent().withItem(this.requiredPackagesTable).component();
|
||||
|
||||
let formModel = this.view.modelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
component: this.kernelDropdown ?? this.kernelLabel,
|
||||
title: localize('configurePython.kernelLabel', "Kernel")
|
||||
}, {
|
||||
component: this.packageTableSpinner,
|
||||
title: localize('configurePython.requiredDependencies', "Install required kernel dependencies")
|
||||
}]).component();
|
||||
await this.view.initializeModel(formModel);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
let pythonExe = JupyterServerInstallation.getPythonExePath(this.model.pythonLocation, this.model.useExistingPython);
|
||||
this.installedPackagesPromise = this.model.installation.getInstalledPipPackages(pythonExe);
|
||||
this.installedPackages = undefined;
|
||||
|
||||
if (this.kernelDropdown) {
|
||||
if (this.model.kernelName) {
|
||||
this.kernelDropdown.value = this.model.kernelName;
|
||||
} else {
|
||||
this.model.kernelName = getDropdownValue(this.kernelDropdown);
|
||||
}
|
||||
}
|
||||
await this.updateRequiredPackages(this.model.kernelName);
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async updateRequiredPackages(kernelName: string): Promise<void> {
|
||||
this.packageTableSpinner.loading = true;
|
||||
try {
|
||||
let pkgVersionMap = new Map<string, { currentVersion: string, newVersion: string }>();
|
||||
|
||||
// Fetch list of required packages for the specified kernel
|
||||
let requiredPackages = JupyterServerInstallation.getRequiredPackagesForKernel(kernelName);
|
||||
requiredPackages.forEach(pkg => {
|
||||
pkgVersionMap.set(pkg.name, { currentVersion: undefined, newVersion: 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);
|
||||
}
|
||||
});
|
||||
|
||||
if (pkgVersionMap.size > 0) {
|
||||
let packageData = [];
|
||||
for (let [key, value] of pkgVersionMap.entries()) {
|
||||
packageData.push([key, value.currentVersion ?? '-', value.newVersion]);
|
||||
}
|
||||
this.requiredPackagesTable.data = packageData;
|
||||
this.model.packagesToInstall = requiredPackages;
|
||||
} else {
|
||||
this.instance.showErrorMessage(localize('msgUnsupportedKernel', "Could not retrieve packages for unsupported kernel {0}", kernelName));
|
||||
this.requiredPackagesTable.data = [['-', '-', '-']];
|
||||
this.model.packagesToInstall = undefined;
|
||||
}
|
||||
} finally {
|
||||
this.packageTableSpinner.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user