Manage Package Dialog Refactor (#8473)

* Refactoring Manage Packages dialog so that other extensions can contribute to it by registering package mange providers for different location and package type
This commit is contained in:
Leila Lali
2019-12-05 10:26:50 -08:00
committed by GitHub
parent a898c46e74
commit 0d9353d99e
15 changed files with 1406 additions and 136 deletions

View File

@@ -5,12 +5,10 @@
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as request from 'request';
import { JupyterServerInstallation, PipPackageOverview } from '../../jupyter/jupyterServerInstallation';
import * as utils from '../../common/utils';
import { ManagePackagesDialog } from './managePackagesDialog';
import { PythonPkgType } from '../../common/constants';
const localize = nls.loadMessageBundle();
@@ -28,7 +26,6 @@ export class AddNewPackageTab {
private packageInstallButton: azdata.ButtonComponent;
private readonly InvalidTextPlaceholder = localize('managePackages.invalidTextPlaceholder', "N/A");
private readonly PackageNotFoundError = localize('managePackages.packageNotFound', "Could not find the specified package");
private readonly SearchPlaceholder = (pkgType: string) => localize('managePackages.searchBarPlaceholder', "Search {0} packages", pkgType);
constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) {
@@ -42,8 +39,8 @@ export class AddNewPackageTab {
label: localize('managePackages.searchButtonLabel', "Search"),
width: '200px'
}).component();
this.packagesSearchButton.onDidClick(() => {
this.loadNewPackageInfo();
this.packagesSearchButton.onDidClick(async () => {
await this.loadNewPackageInfo();
});
this.newPackagesName = view.modelBuilder.text().withProperties({ width: '400px' }).component();
@@ -65,8 +62,8 @@ export class AddNewPackageTab {
label: localize('managePackages.installButtonText', "Install"),
width: '200px'
}).component();
this.packageInstallButton.onDidClick(() => {
this.doPackageInstall();
this.packageInstallButton.onDidClick(async () => {
await this.doPackageInstall();
});
let formModel = view.modelBuilder.formContainer()
@@ -107,7 +104,7 @@ export class AddNewPackageTab {
await this.newPackagesSearchBar.updateProperties({
value: '',
placeHolder: this.SearchPlaceholder(this.dialog.currentPkgType)
placeHolder: this.SearchPlaceholder(this.dialog.model.currentPackageType)
});
await this.setFieldsToEmpty();
} finally {
@@ -145,11 +142,7 @@ export class AddNewPackageTab {
}
let pipPackage: PipPackageOverview;
if (this.dialog.currentPkgType === PythonPkgType.Anaconda) {
pipPackage = await this.fetchCondaPackage(packageName);
} else {
pipPackage = await this.fetchPypiPackage(packageName);
}
pipPackage = await this.dialog.model.getPackageOverview(packageName);
if (!pipPackage.versions || pipPackage.versions.length === 0) {
this.dialog.showErrorMessage(
localize('managePackages.noVersionsFound',
@@ -179,86 +172,7 @@ export class AddNewPackageTab {
}
}
private async fetchPypiPackage(packageName: string): Promise<PipPackageOverview> {
return new Promise<PipPackageOverview>((resolve, reject) => {
request.get(`https://pypi.org/pypi/${packageName}/json`, { timeout: 10000 }, (error, response, body) => {
if (error) {
return reject(error);
}
if (response.statusCode === 404) {
return reject(this.PackageNotFoundError);
}
if (response.statusCode !== 200) {
return reject(
localize('managePackages.packageRequestError',
"Package info request failed with error: {0} {1}",
response.statusCode,
response.statusMessage));
}
let versionNums: string[] = [];
let packageSummary = '';
let packagesJson = JSON.parse(body);
if (packagesJson) {
if (packagesJson.releases) {
let versionKeys = Object.keys(packagesJson.releases);
versionKeys = versionKeys.filter(versionKey => {
let releaseInfo = packagesJson.releases[versionKey];
return Array.isArray(releaseInfo) && releaseInfo.length > 0;
});
versionNums = utils.sortPackageVersions(versionKeys, false);
}
if (packagesJson.info && packagesJson.info.summary) {
packageSummary = packagesJson.info.summary;
}
}
resolve({
name: packageName,
versions: versionNums,
summary: packageSummary
});
});
});
}
private async fetchCondaPackage(packageName: string): Promise<PipPackageOverview> {
let condaExe = this.jupyterInstallation.getCondaExePath();
let cmd = `"${condaExe}" search --json ${packageName}`;
let packageResult: string;
try {
packageResult = await this.jupyterInstallation.executeBufferedCommand(cmd);
} catch (err) {
throw new Error(this.PackageNotFoundError);
}
if (packageResult) {
let packageJson = JSON.parse(packageResult);
if (packageJson) {
if (packageJson.error) {
throw new Error(packageJson.error);
}
let packages = packageJson[packageName];
if (Array.isArray(packages)) {
let allVersions = packages.filter(pkg => pkg && pkg.version).map(pkg => pkg.version);
let singletonVersions = new Set<string>(allVersions);
let sortedVersions = utils.sortPackageVersions(Array.from(singletonVersions), false);
return {
name: packageName,
versions: sortedVersions,
summary: undefined
};
}
}
}
return undefined;
}
private async doPackageInstall(): Promise<void> {
let packageName = this.newPackagesName.value;
@@ -278,11 +192,7 @@ export class AddNewPackageTab {
isCancelable: false,
operation: op => {
let installPromise: Promise<void>;
if (this.dialog.currentPkgType === PythonPkgType.Anaconda) {
installPromise = this.jupyterInstallation.installCondaPackages([{ name: packageName, version: packageVersion }], false);
} else {
installPromise = this.jupyterInstallation.installPipPackages([{ name: packageName, version: packageVersion }], false);
}
installPromise = this.dialog.model.installPackages([{ name: packageName, version: packageVersion }]);
installPromise
.then(async () => {
let installMsg = localize('managePackages.backgroundInstallComplete',

View File

@@ -11,7 +11,6 @@ import * as utils from '../../common/utils';
import { ManagePackagesDialog } from './managePackagesDialog';
import CodeAdapter from '../../prompts/adapter';
import { QuestionTypes, IQuestion } from '../../prompts/question';
import { PythonPkgType } from '../../common/constants';
const localize = nls.loadMessageBundle();
@@ -21,6 +20,7 @@ export class InstalledPackagesTab {
private installedPkgTab: azdata.window.DialogTab;
private packageTypeDropdown: azdata.DropDownComponent;
private locationComponent: azdata.TextComponent;
private installedPackageCount: azdata.TextComponent;
private installedPackagesTable: azdata.TableComponent;
private installedPackagesLoader: azdata.LoadingComponent;
@@ -32,18 +32,28 @@ export class InstalledPackagesTab {
this.installedPkgTab = azdata.window.createTab(localize('managePackages.installedTabTitle', "Installed"));
this.installedPkgTab.registerContent(async view => {
let dropdownValues: string[];
if (this.dialog.currentPkgType === PythonPkgType.Anaconda) {
dropdownValues = [PythonPkgType.Anaconda, PythonPkgType.Pip];
} else {
dropdownValues = [PythonPkgType.Pip];
}
// TODO: only supporting single location for now. We should add a drop down for multi locations mode
//
let locationTitle = await this.dialog.model.getLocationTitle();
this.locationComponent = view.modelBuilder.text().withProperties({
value: locationTitle
}).component();
let dropdownValues = this.dialog.model.getPackageTypes().map(x => {
return {
name: x.providerId,
displayName: x.packageType
};
});
let defaultPackageType = this.dialog.model.getDefaultPackageType();
this.packageTypeDropdown = view.modelBuilder.dropDown().withProperties({
values: dropdownValues,
value: dropdownValues[0]
value: defaultPackageType
}).component();
this.dialog.changeProvider(defaultPackageType.providerId);
this.packageTypeDropdown.onValueChanged(() => {
this.dialog.resetPages(this.packageTypeDropdown.value as PythonPkgType)
this.dialog.resetPages((<azdata.CategoryValue>this.packageTypeDropdown.value).name)
.catch(err => {
this.dialog.showErrorMessage(utils.getErrorMessage(err));
});
@@ -73,6 +83,9 @@ export class InstalledPackagesTab {
let formModel = view.modelBuilder.formContainer()
.withFormItems([{
component: this.locationComponent,
title: localize('managePackages.location', "Location")
}, {
component: this.packageTypeDropdown,
title: localize('managePackages.packageType', "Package Type")
}, {
@@ -95,6 +108,7 @@ export class InstalledPackagesTab {
await view.initializeModel(this.installedPackagesLoader);
await this.loadInstalledPackagesInfo();
this.packageTypeDropdown.focus();
});
}
@@ -108,11 +122,7 @@ export class InstalledPackagesTab {
await this.installedPackagesLoader.updateProperties({ loading: true });
await this.uninstallPackageButton.updateProperties({ enabled: false });
try {
if (this.dialog.currentPkgType === PythonPkgType.Anaconda) {
pythonPackages = await this.jupyterInstallation.getInstalledCondaPackages();
} else {
pythonPackages = await this.jupyterInstallation.getInstalledPipPackages();
}
pythonPackages = await this.dialog.model.listPackages();
} catch (err) {
this.dialog.showErrorMessage(utils.getErrorMessage(err));
} finally {
@@ -131,7 +141,7 @@ export class InstalledPackagesTab {
await this.installedPackageCount.updateProperties({
value: localize('managePackages.packageCount', "{0} {1} packages found",
packageCount,
this.dialog.currentPkgType)
this.dialog.model.currentPackageType)
});
if (packageData && packageData.length > 0) {
@@ -178,12 +188,7 @@ export class InstalledPackagesTab {
description: taskName,
isCancelable: false,
operation: op => {
let uninstallPromise: Promise<void>;
if (this.dialog.currentPkgType === PythonPkgType.Anaconda) {
uninstallPromise = this.jupyterInstallation.uninstallCondaPackages(packages);
} else {
uninstallPromise = this.jupyterInstallation.uninstallPipPackages(packages);
}
let uninstallPromise: Promise<void> = this.dialog.model.uninstallPackages(packages);
uninstallPromise
.then(async () => {
let uninstallMsg = localize('managePackages.backgroundUninstallComplete',
@@ -213,4 +218,4 @@ export class InstalledPackagesTab {
this.uninstallPackageButton.updateProperties({ enabled: true });
}
}
}

View File

@@ -9,7 +9,7 @@ import * as azdata from 'azdata';
import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
import { InstalledPackagesTab } from './installedPackagesTab';
import { AddNewPackageTab } from './addNewPackageTab';
import { PythonPkgType } from '../../common/constants';
import { ManagePackagesDialogModel } from './managePackagesDialogModel';
const localize = nls.loadMessageBundle();
@@ -18,10 +18,8 @@ export class ManagePackagesDialog {
private installedPkgTab: InstalledPackagesTab;
private addNewPkgTab: AddNewPackageTab;
public currentPkgType: PythonPkgType;
constructor(private jupyterInstallation: JupyterServerInstallation) {
this.currentPkgType = this.jupyterInstallation.usingConda ? PythonPkgType.Anaconda : PythonPkgType.Pip;
constructor(
private _managePackageDialogModel: ManagePackagesDialogModel) {
}
/**
@@ -49,8 +47,37 @@ export class ManagePackagesDialog {
return this.installedPkgTab.loadInstalledPackagesInfo();
}
public async resetPages(newPkgType: PythonPkgType): Promise<void> {
this.currentPkgType = newPkgType;
public get jupyterInstallation(): JupyterServerInstallation {
return this._managePackageDialogModel.jupyterInstallation;
}
/**
* Dialog model instance
*/
public get model(): ManagePackagesDialogModel {
return this._managePackageDialogModel;
}
/**
* Changes the current provider id
* @param providerId Provider Id
*/
public changeProvider(providerId: string): void {
this.model.changeProvider(providerId);
}
/**
* Resets the tabs for given provider Id
* @param providerId Package Management Provider Id
*/
public async resetPages(providerId: string): Promise<void> {
// Change the provider in the model
//
this.changeProvider(providerId);
// Load packages for given provider
//
await this.installedPkgTab.loadInstalledPackagesInfo();
await this.addNewPkgTab.resetPageFields();
}

View File

@@ -0,0 +1,281 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
import { IPackageManageProvider, IPackageDetails, IPackageOverview } from '../../types';
export interface ManagePackageDialogOptions {
multiLocations: boolean;
defaultLocation?: string;
defaultProviderId?: string;
}
export interface ProviderPackageType {
packageType: string;
providerId: string;
}
/**
* Manage package dialog model
*/
export class ManagePackagesDialogModel {
private _currentProvider: string;
/**
* A set for locations
*/
private _locations: Set<string> = new Set<string>();
/**
* Map of locations to providers
*/
private _packageTypes: Map<string, IPackageManageProvider[]> = new Map<string, IPackageManageProvider[]>();
/**
* Creates new instance of the model
* @param _jupyterInstallation Jupyter installation
* @param _packageManageProviders package manage providers
* @param _options dialog options
*/
constructor(
private _jupyterInstallation: JupyterServerInstallation,
private _packageManageProviders: Map<string, IPackageManageProvider>,
private _options?: ManagePackageDialogOptions) {
if (!this._packageManageProviders || this._packageManageProviders.size === 0) {
throw Error('Invalid list of package manager providers');
}
}
/**
* Initialized the model
*/
public async init(): Promise<void> {
await this.loadCaches();
this.loadOptions();
this.changeProvider(this.defaultProviderId);
}
/**
* Loads the model options
*/
private loadOptions(): void {
// Set Default Options
//
if (!this._options) {
this._options = this.defaultOptions;
}
if (this._options.defaultLocation && !this._packageTypes.has(this._options.defaultLocation)) {
throw new Error(`Invalid default location '${this._options.defaultLocation}`);
}
if (this._options.defaultProviderId && !this._packageManageProviders.has(this._options.defaultProviderId)) {
throw new Error(`Invalid default provider id '${this._options.defaultProviderId}`);
}
if (!this._options.multiLocations && !this.defaultLocation) {
throw new Error('Default location not specified for single location mode');
}
}
private get defaultOptions(): ManagePackageDialogOptions {
return {
multiLocations: true,
defaultLocation: undefined,
defaultProviderId: undefined
};
}
/**
* Returns the providers map
*/
public get packageManageProviders(): Map<string, IPackageManageProvider> {
return this._packageManageProviders;
}
/**
* Returns the current provider
*/
public get currentPackageManageProvider(): IPackageManageProvider | undefined {
if (this._currentProvider) {
let provider = this._packageManageProviders.get(this._currentProvider);
return provider;
}
return undefined;
}
/**
* Returns the current provider
*/
public get currentPackageType(): string | undefined {
if (this._currentProvider) {
let provider = this._packageManageProviders.get(this._currentProvider);
return provider.packageTarget.packageType;
}
return undefined;
}
/**
* Returns true if multi locations mode is enabled
*/
public get multiLocationMode(): boolean {
return this.options.multiLocations;
}
/**
* Returns options
*/
public get options(): ManagePackageDialogOptions {
return this._options || this.defaultOptions;
}
/**
* returns the array of target locations
*/
public get targetLocations(): string[] {
return Array.from(this._locations.keys());
}
/**
* Returns the default location
*/
public get defaultLocation(): string {
return this.options.defaultLocation || this.targetLocations[0];
}
/**
* Returns the default location
*/
public get defaultProviderId(): string {
return this.options.defaultProviderId || Array.from(this.packageManageProviders.keys())[0];
}
/**
* Loads the provider cache
*/
private async loadCaches(): Promise<void> {
if (this.packageManageProviders) {
let keyArray = Array.from(this.packageManageProviders.keys());
for (let index = 0; index < keyArray.length; index++) {
const element = this.packageManageProviders.get(keyArray[index]);
if (await element.canUseProvider()) {
if (!this._locations.has(element.packageTarget.location)) {
this._locations.add(element.packageTarget.location);
}
if (!this._packageTypes.has(element.packageTarget.location)) {
this._packageTypes.set(element.packageTarget.location, []);
}
this._packageTypes.get(element.packageTarget.location).push(element);
}
}
}
}
/**
* Returns a map of providerId to package types for given location
*/
public getPackageTypes(targetLocation?: string): ProviderPackageType[] {
targetLocation = targetLocation || this.defaultLocation;
let providers = this._packageTypes.get(targetLocation);
return providers.map(x => {
return {
providerId: x.providerId,
packageType: x.packageTarget.packageType
};
});
}
/**
* Returns a map of providerId to package types for given location
*/
public getDefaultPackageType(): ProviderPackageType {
let defaultProviderId = this.defaultProviderId;
let packageTypes = this.getPackageTypes();
return packageTypes.find(x => x.providerId === defaultProviderId);
}
/**
* returns the list of packages for current provider
*/
public async listPackages(): Promise<IPackageDetails[]> {
let provider = this.currentPackageManageProvider;
if (provider) {
return await provider.listPackages();
} else {
throw new Error('Current Provider is not set');
}
}
/**
* Changes the current provider
*/
public changeProvider(providerId: string): void {
if (this._packageManageProviders.has(providerId)) {
this._currentProvider = providerId;
} else {
throw Error(`Invalid package type ${providerId}`);
}
}
/**
* Installs given packages using current provider
* @param packages Packages to install
*/
public async installPackages(packages: IPackageDetails[]): Promise<void> {
let provider = this.currentPackageManageProvider;
if (provider) {
await provider.installPackages(packages, false);
} else {
throw new Error('Current Provider is not set');
}
}
/**
* Returns the location title for current provider
*/
public async getLocationTitle(): Promise<string | undefined> {
let provider = this.currentPackageManageProvider;
if (provider) {
return await provider.getLocationTitle();
}
return Promise.resolve(undefined);
}
/**
* UnInstalls given packages using current provider
* @param packages Packages to install
*/
public async uninstallPackages(packages: IPackageDetails[]): Promise<void> {
let provider = this.currentPackageManageProvider;
if (provider) {
await provider.uninstallPackages(packages);
} else {
throw new Error('Current Provider is not set');
}
}
/**
* Returns package preview for given name
* @param packageName Package name
*/
public async getPackageOverview(packageName: string): Promise<IPackageOverview> {
let provider = this.currentPackageManageProvider;
if (provider) {
return await provider.getPackageOverview(packageName);
} else {
throw new Error('Current Provider is not set');
}
}
/**
* Returns the jupyterInstallation instance
*/
public get jupyterInstallation(): JupyterServerInstallation {
return this._jupyterInstallation;
}
}