diff --git a/extensions/machine-learning-services/config.json b/extensions/machine-learning-services/config.json index a9e3d622ec..d4e5ee7c17 100644 --- a/extensions/machine-learning-services/config.json +++ b/extensions/machine-learning-services/config.json @@ -9,5 +9,11 @@ { "name": "sqlmlutils", "fileName": "sqlmlutils_0.7.1.zip", "downloadUrl": "https://github.com/microsoft/sqlmlutils/blob/master/R/dist/sqlmlutils_0.7.1.zip?raw=true"} ], - "rPackagesRepository": "https://cran.r-project.org" + "rPackagesRepository": "https://cran.r-project.org", + + "registeredModelsDatabaseName": "MlFlowDB", + "registeredModelsTableName": "dbo.artifacts", + "amlModelManagementUrl": "modelmanagement.azureml.net", + "amlExperienceUrl": "experiments.azureml.net", + "amlApiVersion": "2018-11-19" } diff --git a/extensions/machine-learning-services/package.json b/extensions/machine-learning-services/package.json index 8946bfe73d..55453f3920 100644 --- a/extensions/machine-learning-services/package.json +++ b/extensions/machine-learning-services/package.json @@ -57,6 +57,14 @@ "command": "mls.command.managePackages", "title": "%mls.command.managePackages%" }, + { + "command": "mls.command.manageModels", + "title": "%mls.command.manageModels%" + }, + { + "command": "mls.command.registerModel", + "title": "%mls.command.registerModel%" + }, { "command": "mls.command.manageLanguages", "title": "%mls.command.manageLanguages%" @@ -101,6 +109,17 @@ "tasks-widget": [ "mls.command.managePackages", "mls.command.manageLanguages", + "mls.command.manageModels", + "mls.command.registerModel" + ] + } + }, + { + "name": "%title.documents%", + "row": 1, + "col": 0, + "widget": { + "tasks-widget": [ "mls.command.odbcdriver", "mls.command.mlsdocs" ] @@ -114,7 +133,9 @@ "dependencies": { "request": "^2.88.0", "vscode-nls": "^4.0.0", - "vscode-languageclient": "^5.3.0-next.1" + "vscode-languageclient": "^5.3.0-next.1", + "@azure/arm-machinelearningservices" : "^3.0.0", + "polly-js": "^1.6.3" }, "devDependencies": { "@types/mocha": "^5.2.5", diff --git a/extensions/machine-learning-services/package.nls.json b/extensions/machine-learning-services/package.nls.json index 559633faa3..2a3ea86ca5 100644 --- a/extensions/machine-learning-services/package.nls.json +++ b/extensions/machine-learning-services/package.nls.json @@ -2,10 +2,13 @@ "displayName": "SQL Server Machine Learning Services", "description": "SQL Server Machine Learning Services", "title.tasks": "Tasks", + "title.documents": "Documents", "title.configurations": "Configurations", "title.endpoints": "Endpoints", "mls.command.managePackages": "Manage Packages in SQL Server", "mls.command.manageLanguages": "Manage External Languages", + "mls.command.manageModels": "Manage Models", + "mls.command.registerModel": "Register Model", "mls.command.odbcdriver": "Install ODBC Driver for SQL Server", "mls.command.mlsdocs": "Machine Learning Services Documentation", "mls.configuration.title": "Machine Learning Services configurations", diff --git a/extensions/machine-learning-services/src/common/apiWrapper.ts b/extensions/machine-learning-services/src/common/apiWrapper.ts index 468df64837..543a34be72 100644 --- a/extensions/machine-learning-services/src/common/apiWrapper.ts +++ b/extensions/machine-learning-services/src/common/apiWrapper.ts @@ -82,7 +82,23 @@ export class ApiWrapper { return azdata.window.createModelViewDialog(title, dialogName, isWide); } + public createWizard(title: string): azdata.window.Wizard { + return azdata.window.createWizard(title); + } + + public createWizardPage(title: string): azdata.window.WizardPage { + return azdata.window.createWizardPage(title); + } + public openDialog(dialog: azdata.window.Dialog): void { return azdata.window.openDialog(dialog); } + + public getAllAccounts(): Thenable { + return azdata.accounts.getAllAccounts(); + } + + public getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: any }> { + return azdata.accounts.getSecurityToken(account, resource); + } } diff --git a/extensions/machine-learning-services/src/common/constants.ts b/extensions/machine-learning-services/src/common/constants.ts index ec0c0518ba..c6d11b7554 100644 --- a/extensions/machine-learning-services/src/common/constants.ts +++ b/extensions/machine-learning-services/src/common/constants.ts @@ -18,10 +18,14 @@ export const mlEnableMlsCommand = 'mls.command.enableMls'; export const mlDisableMlsCommand = 'mls.command.disableMls'; export const extensionOutputChannel = 'Machine Learning Services'; export const notebookExtensionName = 'Microsoft.notebook'; +export const azureSubscriptionsCommand = 'azure.accounts.getSubscriptions'; +export const azureResourceGroupsCommand = 'azure.accounts.getResourceGroups'; // Tasks, commands // export const mlManageLanguagesCommand = 'mls.command.manageLanguages'; +export const mlManageModelsCommand = 'mls.command.manageModels'; +export const mlRegisterModelCommand = 'mls.command.registerModel'; export const mlManagePackagesCommand = 'mls.command.managePackages'; export const mlOdbcDriverCommand = 'mls.command.odbcdriver'; export const mlsDocumentsCommand = 'mls.command.mlsdocs'; @@ -33,6 +37,7 @@ export const mlsConfigKey = 'machineLearningServices'; export const pythonPathConfigKey = 'pythonPath'; export const pythonEnabledConfigKey = 'enablePython'; export const rEnabledConfigKey = 'enableR'; +export const registeredModelsTableName = 'registeredModelsTableName'; export const rPathConfigKey = 'rPath'; // Localized texts @@ -70,7 +75,8 @@ export function httpGetRequestError(code: number, message: string): string { code, message); } -export function getErrorMessage(error: Error): string { return localize('azure.resource.error', "Error: {0}", error?.message); } +export function getErrorMessage(error: Error): string { return localize('azure.resource.error', "Error: {0}", error?.message || error?.toString()); } +export const notSupportedEventArg = localize('notSupportedEventArg', "Not supported event args"); export const extLangInstallTabTitle = localize('extLang.installTabTitle', "Installed"); export const extLangLanguageCreatedDate = localize('extLang.languageCreatedDate', "Installed"); export const extLangLanguagePlatform = localize('extLang.languagePlatform', "Platform"); @@ -95,6 +101,33 @@ export const extLangSelectedPath = localize('extLang.selectedPath', "Selected Pa export const extLangInstallFailedError = localize('extLang.installFailedError', "Failed to install language"); export const extLangUpdateFailedError = localize('extLang.updateFailedError', "Failed to update language"); +export const modeIld = localize('models.id', "Id"); +export const modelName = localize('models.name', "Name"); +export const modelSize = localize('models.size', "Size"); +export const browseModels = localize('models.browseButton', "..."); +export const azureAccount = localize('models.azureAccount', "Account"); +export const azureSubscription = localize('models.azureSubscription', "Subscription"); +export const azureGroup = localize('models.azureGroup', "Resource Group"); +export const azureModelWorkspace = localize('models.azureModelWorkspace', "Workspace"); +export const azureModelFilter = localize('models.azureModelFilter', "Filter"); +export const azureModels = localize('models.azureModels', "Models"); +export const azureModelsTitle = localize('models.azureModelsTitle', "Azure models"); +export const localModelsTitle = localize('models.localModelsTitle', "Local models"); +export const modelSourcesTitle = localize('models.modelSourcesTitle', "Source location"); +export const currentModelsTitle = localize('models.currentModelsTitle', "Models"); +export const azureRegisterModel = localize('models.azureRegisterModel', "Register"); +export const registerModelWizardTitle = localize('models.RegisterWizard', "Register"); +export const registerModelButton = localize('models.RegisterModelButton', "Register model"); +export const modelRegisteredSuccessfully = localize('models.modelRegisteredSuccessfully', "Model registered successfully"); +export const modelFailedToRegister = localize('models.modelFailedToRegistered', "Model failed to register"); +export const localModelSource = localize('models.localModelSource', "Upload file"); +export const azureModelSource = localize('models.azureModelSource', "Import from AzureML registry"); +export const downloadModelMsgTaskName = localize('models.downloadModelMsgTaskName', "Downloading Model from Azure"); +export const invalidAzureResourceError = localize('models.invalidAzureResourceError', "Invalid Azure resource"); +export const invalidModelToRegisterError = localize('models.invalidModelToRegisterError', "Invalid model to register"); + + + // Links // export const mlsDocuments = 'https://docs.microsoft.com/sql/advanced-analytics/?view=sql-server-ver15'; diff --git a/extensions/machine-learning-services/src/common/eventEmitter.ts b/extensions/machine-learning-services/src/common/eventEmitter.ts new file mode 100644 index 0000000000..1fbcda73d5 --- /dev/null +++ b/extensions/machine-learning-services/src/common/eventEmitter.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export class EventEmitterCollection extends vscode.Disposable { + private _events: Map[]> = new Map[]>(); + + /** + * + */ + constructor() { + super(() => this.dispose()); + + } + + public on(evt: string, listener: (e: any) => any, thisArgs?: any) { + if (!this._events.has(evt)) { + this._events.set(evt, []); + } + let eventEmitter = new vscode.EventEmitter(); + eventEmitter.event(listener, thisArgs); + this._events.get(evt)?.push(eventEmitter); + return this; + } + + public fire(evt: string, arg?: any) { + if (!this._events.has(evt)) { + this._events.set(evt, []); + } + this._events.get(evt)?.forEach(eventEmitter => { + eventEmitter.fire(arg); + }); + } + + public dispose(): any { + this._events.forEach(events => { + events.forEach(event => { + event.dispose(); + }); + }); + } +} diff --git a/extensions/machine-learning-services/src/common/queryRunner.ts b/extensions/machine-learning-services/src/common/queryRunner.ts index c1495d8e20..a22fb432d9 100644 --- a/extensions/machine-learning-services/src/common/queryRunner.ts +++ b/extensions/machine-learning-services/src/common/queryRunner.ts @@ -148,7 +148,7 @@ export class QueryRunner { return isEnabled; } - private async runQuery(connection: azdata.connection.ConnectionProfile, query: string): Promise { + public async runQuery(connection: azdata.connection.ConnectionProfile, query: string): Promise { let result: azdata.SimpleExecuteResult | undefined = undefined; try { if (connection) { diff --git a/extensions/machine-learning-services/src/configurations/config.ts b/extensions/machine-learning-services/src/configurations/config.ts index 3f804c4647..27129dfc6f 100644 --- a/extensions/machine-learning-services/src/configurations/config.ts +++ b/extensions/machine-learning-services/src/configurations/config.ts @@ -75,6 +75,42 @@ export class Config { return this.config.get(constants.rEnabledConfigKey) || false; } + /** + * Returns registered models table name + */ + public get registeredModelTableName(): string { + return this._configValues.registeredModelsTableName; + } + + /** + * Returns registered models table name + */ + public get registeredModelDatabaseName(): string { + return this._configValues.registeredModelsDatabaseName; + } + + /** + * Returns Azure ML API + */ + public get amlModelManagementUrl(): string { + return this._configValues.amlModelManagementUrl; + } + + /** + * Returns Azure ML API + */ + public get amlExperienceUrl(): string { + return this._configValues.amlExperienceUrl; + } + + + /** + * Returns Azure ML API Version + */ + public get amlApiVersion(): string { + return this._configValues.amlApiVersion; + } + /** * Returns r path from user settings */ diff --git a/extensions/machine-learning-services/src/controllers/mainController.ts b/extensions/machine-learning-services/src/controllers/mainController.ts index bb91f661e0..1cb5130035 100644 --- a/extensions/machine-learning-services/src/controllers/mainController.ts +++ b/extensions/machine-learning-services/src/controllers/mainController.ts @@ -16,8 +16,12 @@ import { Config } from '../configurations/config'; import { ServerConfigWidget } from '../widgets/serverConfigWidgets'; import { ServerConfigManager } from '../serverConfig/serverConfigManager'; import { HttpClient } from '../common/httpClient'; -import { LanguageController } from '../externalLanguage/languageController'; +import { LanguageController } from '../views/externalLanguages/languageController'; import { LanguageService } from '../externalLanguage/languageService'; +import { ModelManagementController } from '../views/models/modelManagementController'; +import { RegisteredModelService } from '../modelManagement/registeredModelService'; +import { AzureModelRegistryService } from '../modelManagement/azureModelRegistryService'; +import { ModelImporter } from '../modelManagement/modelImporter'; /** * The main controller class that initializes the extension @@ -94,13 +98,28 @@ export default class MainController implements vscode.Disposable { await packageManager.managePackages(); })); + // External Languages + // let mssqlService = await this.getLanguageExtensionService(); let languagesModel = new LanguageService(this._apiWrapper, mssqlService); let languageController = new LanguageController(this._apiWrapper, this._rootPath, languagesModel); + let modelImporter = new ModelImporter(this._outputChannel, this._apiWrapper, this._processService, this._config); + + // Model Management + // + let registeredModelService = new RegisteredModelService(this._apiWrapper, this._config, this._queryRunner, modelImporter); + let azureModelsService = new AzureModelRegistryService(this._apiWrapper, this._config, this.httpClient, this._outputChannel); + let modelManagementController = new ModelManagementController(this._apiWrapper, this._rootPath, azureModelsService, registeredModelService); this._apiWrapper.registerCommand(constants.mlManageLanguagesCommand, (async () => { await languageController.manageLanguages(); })); + this._apiWrapper.registerCommand(constants.mlManageModelsCommand, (async () => { + await modelManagementController.manageRegisteredModels(); + })); + this._apiWrapper.registerCommand(constants.mlRegisterModelCommand, (async () => { + await modelManagementController.registerModel(); + })); this._apiWrapper.registerCommand(constants.mlsDependenciesCommand, (async () => { await packageManager.installDependencies(); })); @@ -110,6 +129,12 @@ export default class MainController implements vscode.Disposable { this._apiWrapper.registerTaskHandler(constants.mlManageLanguagesCommand, async () => { await languageController.manageLanguages(); }); + this._apiWrapper.registerTaskHandler(constants.mlManageModelsCommand, async () => { + await modelManagementController.manageRegisteredModels(); + }); + this._apiWrapper.registerTaskHandler(constants.mlRegisterModelCommand, async () => { + await modelManagementController.registerModel(); + }); this._apiWrapper.registerTaskHandler(constants.mlOdbcDriverCommand, async () => { await this.serverConfigManager.openOdbcDriverDocuments(); }); diff --git a/extensions/machine-learning-services/src/modelManagement/artifacts.ts b/extensions/machine-learning-services/src/modelManagement/artifacts.ts new file mode 100644 index 0000000000..9a694888f8 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/artifacts.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as msRest from '@azure/ms-rest-js'; +import * as Models from './interfaces'; +import * as Mappers from './mappers'; +import * as Parameters from './parameters'; +import { AzureMachineLearningWorkspacesContext } from '@azure/arm-machinelearningservices'; + +export class Artifacts { + private readonly client: AzureMachineLearningWorkspacesContext; + + constructor(client: AzureMachineLearningWorkspacesContext) { + this.client = client; + } + + getArtifactContentInformation2(subscriptionId: string, resourceGroupName: string, workspaceName: string, origin: string, container: string, options?: Models.ArtifactAPIGetArtifactContentInformation2OptionalParams): Promise; + getArtifactContentInformation2(subscriptionId: string, resourceGroupName: string, workspaceName: string, origin: string, container: string, callback: msRest.ServiceCallback): void; + getArtifactContentInformation2(subscriptionId: string, resourceGroupName: string, workspaceName: string, origin: string, container: string, options: Models.ArtifactAPIGetArtifactContentInformation2OptionalParams, callback: msRest.ServiceCallback): void; + getArtifactContentInformation2(subscriptionId: string, resourceGroupName: string, workspaceName: string, origin: string, container: string, options?: Models.ArtifactAPIGetArtifactContentInformation2OptionalParams | msRest.ServiceCallback, callback?: msRest.ServiceCallback): Promise { + return this.client.sendOperationRequest( + { + subscriptionId, + resourceGroupName, + workspaceName, + origin, + container, + options + }, + getArtifactContentInformation2OperationSpec, + callback) as Promise; + } + +} + +const serializer = new msRest.Serializer(Mappers); +const getArtifactContentInformation2OperationSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: 'artifact/v1.0/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/workspaces/{workspaceName}/artifacts/contentinfo/{origin}/{container}', + urlParameters: [ + Parameters.subscriptionId, + Parameters.resourceGroupName, + Parameters.workspaceName, + Parameters.origin, + Parameters.container, + Parameters.apiVersion + ], + queryParameters: [ + Parameters.projectName0, + Parameters.path1, + Parameters.accountName + ], + responses: { + 200: { + bodyMapper: Mappers.ArtifactContentInformationDto + }, + default: {} + }, + serializer +}; diff --git a/extensions/machine-learning-services/src/modelManagement/assets.ts b/extensions/machine-learning-services/src/modelManagement/assets.ts new file mode 100644 index 0000000000..ffb4542c59 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/assets.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as msRest from '@azure/ms-rest-js'; +import * as Models from './interfaces'; +import * as Mappers from './mappers'; +import * as Parameters from './parameters'; +import { AzureMachineLearningWorkspacesContext } from '@azure/arm-machinelearningservices'; + +export class Assets { + private readonly client: AzureMachineLearningWorkspacesContext; + + constructor(client: AzureMachineLearningWorkspacesContext) { + this.client = client; + } + + queryById( + subscriptionId: string, + resourceGroup: string, + workspace: string, + id: string, + options?: msRest.RequestOptionsBase + ): Promise; + queryById( + subscriptionId: string, + resourceGroup: string, + workspace: string, + id: string, + callback: msRest.ServiceCallback + ): void; + queryById( + subscriptionId: string, + resourceGroup: string, + workspace: string, + id: string, + options: msRest.RequestOptionsBase, + callback: msRest.ServiceCallback + ): void; + queryById( + subscriptionId: string, + resourceGroup: string, + workspace: string, + id: string, + options?: msRest.RequestOptionsBase | msRest.ServiceCallback, + callback?: msRest.ServiceCallback + ): Promise { + return this.client.sendOperationRequest( + { + subscriptionId, + resourceGroup, + workspace, + id, + options + }, + queryByIdOperationSpec, + callback + ) as Promise; + } +} + +const serializer = new msRest.Serializer(Mappers); +const queryByIdOperationSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: + 'modelmanagement/v1.0/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.MachineLearningServices/workspaces/{workspace}/assets/{id}', + urlParameters: [Parameters.subscriptionId, Parameters.resourceGroup, Parameters.workspace, Parameters.id], + responses: { + 200: { + bodyMapper: Mappers.Asset + }, + default: { + bodyMapper: Mappers.ModelErrorResponse + } + }, + serializer +}; diff --git a/extensions/machine-learning-services/src/modelManagement/azureModelRegistryService.ts b/extensions/machine-learning-services/src/modelManagement/azureModelRegistryService.ts new file mode 100644 index 0000000000..479ad7b885 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/azureModelRegistryService.ts @@ -0,0 +1,296 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { ApiWrapper } from '../common/apiWrapper'; +import * as constants from '../common/constants'; +import { azureResource } from '../typings/azure-resource'; +import { AzureMachineLearningWorkspaces } from '@azure/arm-machinelearningservices'; +import { TokenCredentials } from '@azure/ms-rest-js'; +import { WorkspaceModels } from './workspacesModels'; +import { AzureMachineLearningWorkspacesOptions, Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import { WorkspaceModel, Asset, IArtifactParts } from './interfaces'; +import { Config } from '../configurations/config'; +import { Assets } from './assets'; +import * as polly from 'polly-js'; +import { Artifacts } from './artifacts'; +import { HttpClient } from '../common/httpClient'; +import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import * as path from 'path'; +import * as os from 'os'; + +/** + * Azure Model Service + */ +export class AzureModelRegistryService { + + /** + * + */ + constructor(private _apiWrapper: ApiWrapper, private _config: Config, private _httpClient: HttpClient, private _outputChannel: vscode.OutputChannel) { + } + + /** + * Returns list of azure accounts + */ + public async getAccounts(): Promise { + return await this._apiWrapper.getAllAccounts(); + } + + /** + * Returns list of azure subscriptions + * @param account azure account + */ + public async getSubscriptions(account: azdata.Account | undefined): Promise { + const data = await this._apiWrapper.executeCommand(constants.azureSubscriptionsCommand, account, true); + return data?.subscriptions; + } + + /** + * Returns list of azure groups + * @param account azure account + * @param subscription azure subscription + */ + public async getGroups( + account: azdata.Account | undefined, + subscription: azureResource.AzureResourceSubscription | undefined): Promise { + const data = await this._apiWrapper.executeCommand(constants.azureResourceGroupsCommand, account, subscription, true); + return data?.resourceGroups; + } + + /** + * Returns list of workspaces + * @param account azure account + * @param subscription azure subscription + * @param resourceGroup azure resource group + */ + public async getWorkspaces( + account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroup: azureResource.AzureResource | undefined): Promise { + return await this.fetchWorkspaces(account, subscription, resourceGroup); + } + + /** + * Returns list of models + * @param account azure account + * @param subscription azure subscription + * @param resourceGroup azure resource group + * @param workspace azure workspace + */ + public async getModels( + account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroup: azureResource.AzureResource, + workspace: Workspace): Promise { + return await this.fetchModels(account, subscription, resourceGroup, workspace); + } + + /** + * Download an azure model to a temporary location + * @param account azure account + * @param subscription azure subscription + * @param resourceGroup azure resource group + * @param workspace azure workspace + * @param model azure model + */ + public async downloadModel( + account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroup: azureResource.AzureResource, + workspace: Workspace, + model: WorkspaceModel): Promise { + let downloadedFilePath: string = ''; + + for (const tenant of account.properties.tenants) { + try { + const downloadUrls = await this.getAssetArtifactsDownloadLinks(account, subscription, resourceGroup, workspace, model, tenant); + if (downloadUrls && downloadUrls.length > 0) { + downloadedFilePath = await this.downloadArtifact(downloadUrls[0]); + } + + } catch (error) { + console.log(error); + } + } + return downloadedFilePath; + } + + /** + * Installs dependencies for the extension + */ + public async downloadArtifact(downloadUrl: string): Promise { + return new Promise((resolve, reject) => { + let msgTaskName = constants.downloadModelMsgTaskName; + this._apiWrapper.startBackgroundOperation({ + displayName: msgTaskName, + description: msgTaskName, + isCancelable: false, + operation: async op => { + let tempFilePath: string = ''; + try { + tempFilePath = path.join(os.tmpdir(), `ads_ml_temp_${UUID.generateUuid()}`); + await this._httpClient.download(downloadUrl, tempFilePath, op, this._outputChannel); + + op.updateStatus(azdata.TaskStatus.Succeeded); + resolve(tempFilePath); + } catch (error) { + let errorMsg = constants.installDependenciesError(error ? error.message : ''); + op.updateStatus(azdata.TaskStatus.Failed, errorMsg); + reject(errorMsg); + } + } + }); + }); + } + + private async fetchWorkspaces(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroup: azureResource.AzureResource | undefined): Promise { + let resources: Workspace[] = []; + + try { + for (const tenant of account.properties.tenants) { + const tokens = await this._apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; + const client = new AzureMachineLearningWorkspaces(new TokenCredentials(token, tokenType), subscription.id); + let result = resourceGroup ? await client.workspaces.listByResourceGroup(resourceGroup.name) : await client.workspaces.listBySubscription(); + resources.push(...result); + } + } catch (error) { + + } + return resources; + } + + private async fetchModels( + account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroup: azureResource.AzureResource, + workspace: Workspace): Promise { + let resources: WorkspaceModel[] = []; + + for (const tenant of account.properties.tenants) { + try { + let baseUri = this.getBaseUrl(workspace, this._config.amlModelManagementUrl); + const client = await this.getClient(baseUri, account, subscription, tenant); + let modelsClient = new WorkspaceModels(client); + resources = resources.concat(await modelsClient.listModels(resourceGroup.name, workspace.name || '')); + + } catch (error) { + console.log(error); + } + } + + return resources; + } + + private async fetchModelAsset( + subscription: azureResource.AzureResourceSubscription, + resourceGroup: azureResource.AzureResource, + workspace: Workspace, + model: WorkspaceModel, + client: AzureMachineLearningWorkspaces): Promise { + + const modelId = this.getModelId(model); + let modelsClient = new Assets(client); + return await modelsClient.queryById(subscription.id, resourceGroup.name, workspace.name || '', modelId); + } + + public async getAssetArtifactsDownloadLinks( + account: azdata.Account, + subscription: azureResource.AzureResourceSubscription, + resourceGroup: azureResource.AzureResource, + workspace: Workspace, + model: WorkspaceModel, + tenant: any): Promise { + let baseUri = this.getBaseUrl(workspace, this._config.amlModelManagementUrl); + const modelManagementClient = await this.getClient(baseUri, account, subscription, tenant); + const asset = await this.fetchModelAsset(subscription, resourceGroup, workspace, model, modelManagementClient); + baseUri = this.getBaseUrl(workspace, this._config.amlExperienceUrl); + const experienceClient = await this.getClient(baseUri, account, subscription, tenant); + const artifactClient = new Artifacts(experienceClient); + let downloadLinks: string[] = []; + if (asset && asset.artifacts) { + const downloadLinkPromises: Array> = []; + for (const artifact of asset.artifacts) { + const parts = artifact.id + ? this.getPartsFromAssetIdOrPrefix(artifact.id) + : this.getPartsFromAssetIdOrPrefix(artifact.prefix); + + if (parts) { + const promise = polly() + .waitAndRetry(3) + .executeForPromise( + async (): Promise => { + const artifact = await artifactClient.getArtifactContentInformation2( + experienceClient.subscriptionId, + resourceGroup.name, + workspace.name || '', + parts.origin, + parts.container, + { path: parts.path } + ); + if (artifact) { + return artifact.contentUri || ''; + } else { + return Promise.reject(); + } + } + ); + downloadLinkPromises.push(promise); + } + } + + try { + downloadLinks = await Promise.all(downloadLinkPromises); + } catch (rejectedPromiseError) { + return rejectedPromiseError; + } + } + return downloadLinks; + } + + public getPartsFromAssetIdOrPrefix(idOrPrefix: string | undefined): IArtifactParts | undefined { + const artifactRegex = /^(.+?)\/(.+?)\/(.+?)$/; + if (idOrPrefix) { + const parts = artifactRegex.exec(idOrPrefix); + if (parts && parts.length === 4) { + return { + origin: parts[1], + container: parts[2], + path: parts[3] + }; + } + } + return undefined; + } + + private getBaseUrl(workspace: Workspace, server: string): string { + let baseUri = `https://${workspace.location}.${server}`; + if (workspace.location === 'chinaeast2') { + baseUri = `https://${workspace.location}.${server}`; + } + return baseUri; + } + + private async getClient(baseUri: string, account: azdata.Account, subscription: azureResource.AzureResourceSubscription, tenant: any): Promise { + const tokens = await this._apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; + const options: AzureMachineLearningWorkspacesOptions = { + baseUri: baseUri + }; + const client = new AzureMachineLearningWorkspaces(new TokenCredentials(token, tokenType), subscription.id, options); + client.apiVersion = this._config.amlApiVersion; + return client; + } + + private getModelId(model: WorkspaceModel): string { + const amlAssetRegex = /^aml:\/\/asset\/(.+)$/; + const id = model ? amlAssetRegex.exec(model.url || '') : undefined; + return id && id.length === 2 ? id[1] : ''; + } +} diff --git a/extensions/machine-learning-services/src/modelManagement/interfaces.ts b/extensions/machine-learning-services/src/modelManagement/interfaces.ts new file mode 100644 index 0000000000..37481061d8 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/interfaces.ts @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as msRest from '@azure/ms-rest-js'; +import { Resource } from '@azure/arm-machinelearningservices/esm/models'; + +/** + * An interface representing ListWorkspaceModelResult. + */ +export interface ListWorkspaceModelsResult extends Array { +} + +/** + * An interface representing Workspace model + */ +export interface WorkspaceModel extends Resource { + framework?: string; + frameworkVersion?: string; + createdBy?: string; + createdTime?: string; + experimentName?: string; + outputsSchema?: Array; + url?: string; +} + +/** + * An interface representing Workspace model list response + */ +export type WorkspacesModelsResponse = ListWorkspaceModelsResult & { + /** + * The underlying HTTP response. + */ + _response: msRest.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: ListWorkspaceModelsResult; + }; +}; + +/** + * An interface representing registered model + */ +export interface RegisteredModel { + id: number, + name: string +} + +/** + * The Artifact definition. + */ +export interface ArtifactDetails { + /** + * The Artifact Id. + */ + id?: string; + /** + * The Artifact prefix. + */ + prefix?: string; +} + +/** + * @interface + * An interface representing Asset. + * The Asset definition. + * + */ +export interface Asset { + /** + * @member {string} [id] The Asset Id. + */ + id?: string; + /** + * @member {string} [name] The name of the Asset. + */ + name?: string; + /** + * @member {string} [description] The Asset description. + */ + description?: string; + /** + * @member {ArtifactDetails[]} [artifacts] A list of child artifacts. + */ + artifacts?: ArtifactDetails[]; + /** + * @member {string[]} [tags] The Asset tag list. + */ + tags?: string[]; + /** + * @member {{ [propertyName: string]: string }} [kvTags] The Asset tag + * dictionary. Tags are mutable. + */ + kvTags?: { [propertyName: string]: string }; + /** + * @member {{ [propertyName: string]: string }} [properties] The Asset + * property dictionary. Properties are immutable. + */ + properties?: { [propertyName: string]: string }; + /** + * @member {string} [runid] The RunId associated with this Asset. + */ + runid?: string; + /** + * @member {string} [projectid] The project Id. + */ + projectid?: string; + /** + * @member {{ [propertyName: string]: string }} [meta] A dictionary + * containing metadata about the Asset. + */ + meta?: { [propertyName: string]: string }; + /** + * @member {Date} [createdTime] The time the Asset was created in UTC. + */ + createdTime?: Date; +} + + +/** + * Contains response data for the queryById operation. + */ +export type AssetsQueryByIdResponse = Asset & { + /** + * The underlying HTTP response. + */ + _response: msRest.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: Asset; + }; +}; + +export interface IArtifactParts { + origin: string; + container: string; + path: string; +} + +/** +* @interface +* An interface representing ArtifactContentInformationDto. +*/ +export interface ArtifactContentInformationDto { + /** + * @member {string} [contentUri] + */ + contentUri?: string; + /** + * @member {string} [origin] + */ + origin?: string; + /** + * @member {string} [container] + */ + container?: string; + /** + * @member {string} [path] + */ + path?: string; +} +/** + * Contains response data for the getArtifactContentInformation2 operation. + */ +export type GetArtifactContentInformation2Response = ArtifactContentInformationDto & { + /** + * The underlying HTTP response. + */ + _response: msRest.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: ArtifactContentInformationDto; + }; +}; +/** + * @interface + * An interface representing ArtifactAPIGetArtifactContentInformation2OptionalParams. + * Optional Parameters. + * + * @extends RequestOptionsBase + */ +export interface ArtifactAPIGetArtifactContentInformation2OptionalParams extends msRest.RequestOptionsBase { + /** + * @member {string} [projectName] + */ + projectName?: string; + /** + * @member {string} [path] + */ + path?: string; + /** + * @member {string} [accountName] + */ + accountName?: string; +} diff --git a/extensions/machine-learning-services/src/modelManagement/mappers.ts b/extensions/machine-learning-services/src/modelManagement/mappers.ts new file mode 100644 index 0000000000..62cab08f83 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/mappers.ts @@ -0,0 +1,320 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as msRest from '@azure/ms-rest-js'; + +export const Resource: msRest.CompositeMapper = { + serializedName: 'Resource', + type: { + name: 'Composite', + className: 'Resource', + modelProperties: { + id: { + readOnly: true, + serializedName: 'id', + type: { + name: 'String' + } + }, + name: { + readOnly: true, + serializedName: 'name', + type: { + name: 'String' + } + }, + identity: { + readOnly: true, + serializedName: 'identity', + type: { + name: 'Composite', + className: 'Identity' + } + }, + location: { + serializedName: 'location', + type: { + name: 'String' + } + }, + type: { + readOnly: true, + serializedName: 'type', + type: { + name: 'String' + } + }, + tags: { + serializedName: 'tags', + type: { + name: 'Dictionary', + value: { + type: { + name: 'String' + } + } + } + } + } + } +}; + +export const ListWorkspaceModelsResult: msRest.CompositeMapper = { + serializedName: 'ListWorkspaceModelsResult', + type: { + name: 'Composite', + className: 'ListWorkspaceModelsResult', + modelProperties: { + value: { + serializedName: '', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'WorkspaceModel' + } + } + } + }, + nextLink: { + serializedName: 'nextLink', + type: { + name: 'String' + } + } + } + } +}; + +export const WorkspaceModel: msRest.CompositeMapper = { + serializedName: 'WorkspaceModel', + type: { + name: 'Composite', + className: 'WorkspaceModel', + modelProperties: { + ...Resource.type.modelProperties, + framework: { + readOnly: true, + serializedName: 'framework', + type: { + name: 'String' + } + }, + } + } +}; + +export const MachineLearningServiceError: msRest.CompositeMapper = { + serializedName: 'MachineLearningServiceError', + type: { + name: 'Composite', + className: 'MachineLearningServiceError', + modelProperties: { + error: { + readOnly: true, + serializedName: 'error', + type: { + name: 'Composite', + className: 'ErrorResponse' + } + } + } + } +}; +export const ModelErrorResponse: msRest.CompositeMapper = { + serializedName: 'ModelErrorResponse', + type: { + name: 'Composite', + className: 'ModelErrorResponse', + modelProperties: { + code: { + serializedName: 'code', + type: { + name: 'String' + } + }, + statusCode: { + serializedName: 'statusCode', + type: { + name: 'Number' + } + }, + message: { + serializedName: 'message', + type: { + name: 'String' + } + }, + details: { + serializedName: 'details', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'ErrorDetails' + } + } + } + } + } + } +}; +export const ArtifactDetails: msRest.CompositeMapper = { + serializedName: 'ArtifactDetails', + type: { + name: 'Composite', + className: 'ArtifactDetails', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + prefix: { + serializedName: 'prefix', + type: { + name: 'String' + } + } + } + } +}; +export const Asset: msRest.CompositeMapper = { + serializedName: 'Asset', + type: { + name: 'Composite', + className: 'Asset', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String' + } + }, + name: { + serializedName: 'name', + type: { + name: 'String' + } + }, + description: { + serializedName: 'description', + type: { + name: 'String' + } + }, + artifacts: { + serializedName: 'artifacts', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'ArtifactDetails' + } + } + } + }, + tags: { + serializedName: 'tags', + type: { + name: 'Sequence', + element: { + type: { + name: 'String' + } + } + } + }, + kvTags: { + serializedName: 'kvTags', + type: { + name: 'Dictionary', + value: { + type: { + name: 'String' + } + } + } + }, + properties: { + serializedName: 'properties', + type: { + name: 'Dictionary', + value: { + type: { + name: 'String' + } + } + } + }, + runid: { + serializedName: 'runid', + type: { + name: 'String' + } + }, + projectid: { + serializedName: 'projectid', + type: { + name: 'String' + } + }, + meta: { + serializedName: 'meta', + type: { + name: 'Dictionary', + value: { + type: { + name: 'String' + } + } + } + }, + createdTime: { + serializedName: 'createdTime', + type: { + name: 'DateTime' + } + } + } + } +}; +export const ArtifactContentInformationDto: msRest.CompositeMapper = { + serializedName: 'ArtifactContentInformationDto', + type: { + name: 'Composite', + className: 'ArtifactContentInformationDto', + modelProperties: { + contentUri: { + serializedName: 'contentUri', + type: { + name: 'String' + } + }, + origin: { + serializedName: 'origin', + type: { + name: 'String' + } + }, + container: { + serializedName: 'container', + type: { + name: 'String' + } + }, + path: { + serializedName: 'path', + type: { + name: 'String' + } + } + } + } +}; diff --git a/extensions/machine-learning-services/src/modelManagement/modelImporter.ts b/extensions/machine-learning-services/src/modelManagement/modelImporter.ts new file mode 100644 index 0000000000..2412313f1a --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/modelImporter.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProcessService } from '../common/processService'; +import { Config } from '../configurations/config'; +import { ApiWrapper } from '../common/apiWrapper'; +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as UUID from 'vscode-languageclient/lib/utils/uuid'; + +/** + * Service to import model to database + */ +export class ModelImporter { + + /** + * + */ + constructor(private _outputChannel: vscode.OutputChannel, private _apiWrapper: ApiWrapper, private _processService: ProcessService, private _config: Config) { + } + + public async registerModel(connection: azdata.connection.ConnectionProfile, modelFolderPath: string): Promise { + await this.executeScripts(connection, modelFolderPath); + } + + protected async executeScripts(connection: azdata.connection.ConnectionProfile, modelFolderPath: string): Promise { + + const parts = modelFolderPath.split('\\'); + modelFolderPath = parts.join('/'); + + let credentials = await this._apiWrapper.getCredentials(connection.connectionId); + + if (connection) { + let server = connection.serverName; + + const experimentId = `ads_ml_experiment_${UUID.generateUuid()}`; + const credential = connection.userName ? `${connection.userName}:${credentials[azdata.ConnectionOptionSpecialType.password]}` : ''; + let scripts: string[] = [ + 'import mlflow.onnx', + 'import onnx', + 'from mlflow.tracking.client import MlflowClient', + `onx = onnx.load("${modelFolderPath}")`, + 'client = MlflowClient()', + `exp_name = "${experimentId}"`, + `db_uri_artifact = "mssql+pyodbc://${credential}@${server}/MlFlowDB?driver=ODBC+Driver+17+for+SQL+Server"`, + 'client.create_experiment(exp_name, artifact_location=db_uri_artifact)', + 'mlflow.set_experiment(exp_name)', + 'mlflow.onnx.log_model(onx, "pipeline_vectorize")' + ]; + let pythonExecutable = this._config.pythonExecutable; + await this._processService.execScripts(pythonExecutable, scripts, [], this._outputChannel); + } + } +} diff --git a/extensions/machine-learning-services/src/modelManagement/parameters.ts b/extensions/machine-learning-services/src/modelManagement/parameters.ts new file mode 100644 index 0000000000..46c4919892 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/parameters.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as msRest from '@azure/ms-rest-js'; + +export const subscriptionId: msRest.OperationURLParameter = { + parameterPath: 'subscriptionId', + mapper: { + required: true, + serializedName: 'subscriptionId', + type: { + name: 'String' + } + } +}; +export const resourceGroupName: msRest.OperationURLParameter = { + parameterPath: 'resourceGroupName', + mapper: { + required: true, + serializedName: 'resourceGroupName', + type: { + name: 'String' + } + } +}; +export const workspaceName: msRest.OperationURLParameter = { + parameterPath: 'workspaceName', + mapper: { + required: true, + serializedName: 'workspaceName', + type: { + name: 'String' + } + } +}; +export const workspace: msRest.OperationURLParameter = { + parameterPath: 'workspace', + mapper: { + required: true, + serializedName: 'workspace', + type: { + name: 'String' + } + } +}; +export const resourceGroup: msRest.OperationURLParameter = { + parameterPath: 'resourceGroup', + mapper: { + required: true, + serializedName: 'resourceGroup', + type: { + name: 'String' + } + } +}; +export const id: msRest.OperationURLParameter = { + parameterPath: 'id', + mapper: { + required: true, + serializedName: 'id', + type: { + name: 'String' + } + } +}; +export const acceptLanguage: msRest.OperationParameter = { + parameterPath: 'acceptLanguage', + mapper: { + serializedName: 'accept-language', + defaultValue: 'en-US', + type: { + name: 'String' + } + } +}; +export const apiVersion: msRest.OperationQueryParameter = { + parameterPath: 'apiVersion', + mapper: { + required: true, + serializedName: 'api-version', + type: { + name: 'String' + } + } +}; +export const origin: msRest.OperationURLParameter = { + parameterPath: 'origin', + mapper: { + required: true, + serializedName: 'origin', + type: { + name: 'String' + } + } +}; +export const container: msRest.OperationURLParameter = { + parameterPath: 'container', + mapper: { + required: true, + serializedName: 'container', + type: { + name: 'String' + } + } +}; +export const projectName0: msRest.OperationQueryParameter = { + parameterPath: [ + 'options', + 'projectName' + ], + mapper: { + serializedName: 'projectName', + type: { + name: 'String' + } + } +}; +export const path1: msRest.OperationQueryParameter = { + parameterPath: [ + 'options', + 'path' + ], + mapper: { + serializedName: 'path', + type: { + name: 'String' + } + } +}; +export const accountName: msRest.OperationQueryParameter = { + parameterPath: [ + 'options', + 'accountName' + ], + mapper: { + serializedName: 'accountName', + type: { + name: 'String' + } + } +}; diff --git a/extensions/machine-learning-services/src/modelManagement/registeredModelService.ts b/extensions/machine-learning-services/src/modelManagement/registeredModelService.ts new file mode 100644 index 0000000000..b393118d1d --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/registeredModelService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ApiWrapper } from '../common/apiWrapper'; +import { Config } from '../configurations/config'; +import { QueryRunner } from '../common/queryRunner'; +import { RegisteredModel } from './interfaces'; +import { ModelImporter } from './modelImporter'; + +/** + * Service to registered models + */ +export class RegisteredModelService { + + /** + * + */ + constructor( + private _apiWrapper: ApiWrapper, + private _config: Config, + private _queryRunner: QueryRunner, + private _modelImporter: ModelImporter) { + } + + public async getRegisteredModels(): Promise { + let connection = await this.getCurrentConnection(); + let list: RegisteredModel[] = []; + if (connection) { + let result = await this.runRegisteredModelsListQuery(connection); + if (result && result.rows && result.rows.length > 0) { + result.rows.forEach(row => { + list.push({ + id: +row[0].displayValue, + name: row[1].displayValue + }); + }); + } + } + return list; + } + + public async registerLocalModel(filePath: string) { + let connection = await this.getCurrentConnection(); + if (connection) { + await this._modelImporter.registerModel(connection, filePath); + } + } + + private async getCurrentConnection(): Promise { + return await this._apiWrapper.getCurrentConnection(); + } + + private async runRegisteredModelsListQuery(connection: azdata.connection.ConnectionProfile): Promise { + try { + return await this._queryRunner.runQuery(connection, this.registeredModelsQuery(this._config.registeredModelDatabaseName, this._config.registeredModelTableName)); + } catch { + return undefined; + } + } + + private registeredModelsQuery(databaseName: string, tableName: string) { + return ` + IF (EXISTS (SELECT name + FROM master.dbo.sysdatabases + WHERE ('[' + name + ']' = '${databaseName}' + OR name = '${databaseName}'))) + BEGIN + SELECT artifact_id, artifact_name, group_path, artifact_initial_size from ${databaseName}.${tableName} + WHERE artifact_name like '%.onnx' + END + `; + } +} diff --git a/extensions/machine-learning-services/src/modelManagement/workspacesModels.ts b/extensions/machine-learning-services/src/modelManagement/workspacesModels.ts new file mode 100644 index 0000000000..6337fa5a15 --- /dev/null +++ b/extensions/machine-learning-services/src/modelManagement/workspacesModels.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as msRest from '@azure/ms-rest-js'; +import { AzureMachineLearningWorkspacesContext } from '@azure/arm-machinelearningservices'; +import * as Models from './interfaces'; +import * as Mappers from './mappers'; +import * as Parameters from './parameters'; + +/** + * Workspace models client + */ +export class WorkspaceModels { + private readonly client: AzureMachineLearningWorkspacesContext; + + constructor(client: AzureMachineLearningWorkspacesContext) { + this.client = client; + } + + listModels(resourceGroupName: string, workspaceName: string, options?: msRest.RequestOptionsBase): Promise; + listModels(resourceGroupName: string, workspaceName: string, callback: msRest.ServiceCallback): void; + listModels(resourceGroupName: string, workspaceName: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback): void; + listModels(resourceGroupName: string, workspaceName: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback, callback?: msRest.ServiceCallback): Promise { + return this.client.sendOperationRequest( + { + resourceGroupName, + workspaceName, + options + }, + listModelsOperationSpec, + callback) as Promise; + } +} + +const serializer = new msRest.Serializer(Mappers); +const listModelsOperationSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: + 'modelmanagement/v1.0/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/workspaces/{workspaceName}/models', + urlParameters: [ + Parameters.subscriptionId, + Parameters.resourceGroupName, + Parameters.workspaceName + ], + queryParameters: [ + Parameters.apiVersion + ], + headerParameters: [ + Parameters.acceptLanguage + ], + responses: { + 200: { + bodyMapper: Mappers.ListWorkspaceModelsResult + }, + default: { + bodyMapper: Mappers.MachineLearningServiceError + } + }, + serializer +}; + + diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/languageController.test.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/languageController.test.ts index f4007d5d17..61c998b942 100644 --- a/extensions/machine-learning-services/src/test/views/externalLanguages/languageController.test.ts +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/languageController.test.ts @@ -7,7 +7,7 @@ import * as should from 'should'; import 'mocha'; import * as TypeMoq from 'typemoq'; import { createContext } from './utils'; -import { LanguageController } from '../../../externalLanguage/languageController'; +import { LanguageController } from '../../../views/externalLanguages/languageController'; import * as mssql from '../../../../../mssql/src/mssql'; describe('External Languages Controller', () => { diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts index a7fff7f7a4..95420783b7 100644 --- a/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts @@ -10,6 +10,7 @@ import { ApiWrapper } from '../../../common/apiWrapper'; import { LanguageViewBase } from '../../../views/externalLanguages/languageViewBase'; import * as mssql from '../../../../../mssql/src/mssql'; import { LanguageService } from '../../../externalLanguage/languageService'; +import { createViewContext } from '../utils'; export interface TestContext { apiWrapper: TypeMoq.IMock; @@ -30,195 +31,11 @@ export class ParentDialog extends LanguageViewBase { } export function createContext(): TestContext { - let onClick: vscode.EventEmitter = new vscode.EventEmitter(); - let apiWrapper = TypeMoq.Mock.ofType(ApiWrapper); - let componentBase: azdata.Component = { - id: '', - updateProperties: () => Promise.resolve(), - updateProperty: () => Promise.resolve(), - updateCssStyles: undefined!, - onValidityChanged: undefined!, - valid: true, - validate: undefined!, - focus: undefined! - }; - let button: azdata.ButtonComponent = Object.assign({}, componentBase, { - onDidClick: onClick.event - }); - let radioButton: azdata.RadioButtonComponent = Object.assign({}, componentBase, { - onDidClick: onClick.event - }); - let container = { - clearItems: () => { }, - addItems: () => { }, - addItem: () => { }, - removeItem: () => true, - insertItem: () => { }, - items: [], - setLayout: () => { } - }; - let form: azdata.FormContainer = Object.assign({}, componentBase, container, { - }); - let flex: azdata.FlexContainer = Object.assign({}, componentBase, container, { - }); - - let buttonBuilder: azdata.ComponentBuilder = { - component: () => button, - withProperties: () => buttonBuilder, - withValidation: () => buttonBuilder - }; - let radioButtonBuilder: azdata.ComponentBuilder = { - component: () => radioButton, - withProperties: () => radioButtonBuilder, - withValidation: () => radioButtonBuilder - }; - let inputBox: () => azdata.InputBoxComponent = () => Object.assign({}, componentBase, { - onTextChanged: undefined!, - onEnterKeyPressed: undefined!, - value: '' - }); - let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, { - onDataChanged: undefined!, - data: [], - columns: [] - }); - - let loadingComponent: () => azdata.LoadingComponent = () => Object.assign({}, componentBase, { - loading: false, - component: undefined! - }); - - let declarativeTableBuilder: azdata.ComponentBuilder = { - component: () => declarativeTable(), - withProperties: () => declarativeTableBuilder, - withValidation: () => declarativeTableBuilder - }; - - let loadingBuilder: azdata.LoadingComponentBuilder = { - component: () => loadingComponent(), - withProperties: () => loadingBuilder, - withValidation: () => loadingBuilder, - withItem: () => loadingBuilder - }; - - let formBuilder: azdata.FormBuilder = Object.assign({}, { - component: () => form, - addFormItem: () => { }, - insertFormItem: () => { }, - removeFormItem: () => true, - addFormItems: () => { }, - withFormItems: () => formBuilder, - withProperties: () => formBuilder, - withValidation: () => formBuilder, - withItems: () => formBuilder, - withLayout: () => formBuilder - }); - - let flexBuilder: azdata.FlexBuilder = Object.assign({}, { - component: () => flex, - withProperties: () => flexBuilder, - withValidation: () => flexBuilder, - withItems: () => flexBuilder, - withLayout: () => flexBuilder - }); - - let inputBoxBuilder: azdata.ComponentBuilder = { - component: () => { - let r = inputBox(); - return r; - }, - withProperties: () => inputBoxBuilder, - withValidation: () => inputBoxBuilder - }; - - let view: azdata.ModelView = { - onClosed: undefined!, - connection: undefined!, - serverInfo: undefined!, - valid: true, - onValidityChanged: undefined!, - validate: undefined!, - initializeModel: () => { return Promise.resolve(); }, - modelBuilder: { - radioCardGroup: undefined!, - navContainer: undefined!, - divContainer: undefined!, - flexContainer: () => flexBuilder, - splitViewContainer: undefined!, - dom: undefined!, - card: undefined!, - inputBox: () => inputBoxBuilder, - checkBox: undefined!, - radioButton: () => radioButtonBuilder, - webView: undefined!, - editor: undefined!, - diffeditor: undefined!, - text: () => inputBoxBuilder, - image: undefined!, - button: () => buttonBuilder, - dropDown: undefined!, - tree: undefined!, - listBox: undefined!, - table: undefined!, - declarativeTable: () => declarativeTableBuilder, - dashboardWidget: undefined!, - dashboardWebview: undefined!, - formContainer: () => formBuilder, - groupContainer: undefined!, - toolbarContainer: undefined!, - loadingComponent: () => loadingBuilder, - fileBrowserTree: undefined!, - hyperlink: undefined!, - separator: undefined! - } - }; - let tab: azdata.window.DialogTab = { - title: '', - content: '', - registerContent: async (handler) => { - try { - await handler(view); - } catch (err) { - console.log(err); - } - }, - onValidityChanged: undefined!, - valid: true, - modelView: undefined! - }; - - let dialogButton: azdata.window.Button = { - label: '', - enabled: true, - hidden: false, - onClick: onClick.event, - - }; - let dialogMessage: azdata.window.DialogMessage = { - text: '', - }; - let dialog: azdata.window.Dialog = { - title: '', - isWide: false, - content: [], - okButton: dialogButton, - cancelButton: dialogButton, - customButtons: [], - message: dialogMessage, - registerCloseValidator: () => { }, - registerOperation: () => { }, - onValidityChanged: new vscode.EventEmitter().event, - registerContent: () => { }, - modelView: undefined!, - valid: true - }; - apiWrapper.setup(x => x.createTab(TypeMoq.It.isAny())).returns(() => tab); - apiWrapper.setup(x => x.createModelViewDialog(TypeMoq.It.isAny())).returns(() => dialog); - apiWrapper.setup(x => x.openDialog(TypeMoq.It.isAny())).returns(() => { }); + let viewTestContext = createViewContext(); let connection = new azdata.connection.ConnectionProfile(); - apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); - apiWrapper.setup(x => x.getUriForConnection(TypeMoq.It.isAny())).returns(() => { return Promise.resolve('connectionUrl'); }); + viewTestContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + viewTestContext.apiWrapper.setup(x => x.getUriForConnection(TypeMoq.It.isAny())).returns(() => { return Promise.resolve('connectionUrl'); }); let languageExtensionService: mssql.ILanguageExtensionService = { listLanguages: () => { return Promise.resolve([]); }, @@ -226,12 +43,11 @@ export function createContext(): TestContext { updateLanguage: () => { return Promise.resolve(); } }; - return { - apiWrapper: apiWrapper, - view: view, + apiWrapper: viewTestContext.apiWrapper, + view: viewTestContext.view, languageExtensionService: languageExtensionService, - onClick: onClick, + onClick: viewTestContext.onClick, dialogModel: TypeMoq.Mock.ofType(LanguageService) }; } diff --git a/extensions/machine-learning-services/src/test/views/models/azureModelsComponent.test.ts b/extensions/machine-learning-services/src/test/views/models/azureModelsComponent.test.ts new file mode 100644 index 0000000000..85d918e946 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/models/azureModelsComponent.test.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import 'mocha'; +import { createContext, ParentDialog } from './utils'; +import { AzureModelsComponent } from '../../../views/models/azureModelsComponent'; +import { ListAccountsEventName, ListSubscriptionsEventName, ListGroupsEventName, ListWorkspacesEventName, ListAzureModelsEventName } from '../../../views/models/modelViewBase'; +import { azureResource } from '../../../typings/azure-resource'; +import { Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import { ViewBase } from '../../../views/viewBase'; +import { WorkspaceModel } from '../../../modelManagement/interfaces'; + +describe('Azure Models Component', () => { + it('Should create view components successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + + let view = new AzureModelsComponent(testContext.apiWrapper.object, parent); + view.registerComponent(testContext.view.modelBuilder); + should.notEqual(view.component, undefined); + }); + + it('Should load data successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + + let view = new AzureModelsComponent(testContext.apiWrapper.object, parent); + view.registerComponent(testContext.view.modelBuilder); + + let accounts: azdata.Account[] = [ + { + key: { + accountId: '1', + providerId: '' + }, + displayInfo: { + displayName: 'account', + userId: '', + accountType: '', + contextualDisplayName: '' + }, + isStale: false, + properties: [] + } + ]; + let subscriptions: azureResource.AzureResourceSubscription[] = [ + { + name: 'subscription', + id: '2' + } + ]; + let groups: azureResource.AzureResourceResourceGroup[] = [ + { + name: 'group', + id: '3' + } + ]; + let workspaces: Workspace[] = [ + { + name: 'workspace', + id: '4' + } + ]; + let models: WorkspaceModel[] = [ + { + id: '5', + name: 'model' + } + ]; + parent.on(ListAccountsEventName, () => { + parent.sendCallbackRequest(ViewBase.getCallbackEventName(ListAccountsEventName), { data: accounts }); + }); + parent.on(ListSubscriptionsEventName, () => { + + parent.sendCallbackRequest(ViewBase.getCallbackEventName(ListSubscriptionsEventName), { data: subscriptions }); + }); + parent.on(ListGroupsEventName, () => { + parent.sendCallbackRequest(ViewBase.getCallbackEventName(ListGroupsEventName), { data: groups }); + }); + parent.on(ListWorkspacesEventName, () => { + parent.sendCallbackRequest(ViewBase.getCallbackEventName(ListWorkspacesEventName), { data: workspaces }); + }); + parent.on(ListAzureModelsEventName, () => { + parent.sendCallbackRequest(ViewBase.getCallbackEventName(ListAzureModelsEventName), { data: models }); + }); + await view.refresh(); + testContext.onClick.fire(); + should.notEqual(view.data, undefined); + should.deepEqual(view.data?.account, accounts[0]); + should.deepEqual(view.data?.subscription, subscriptions[0]); + should.deepEqual(view.data?.group, groups[0]); + should.deepEqual(view.data?.workspace, workspaces[0]); + should.deepEqual(view.data?.model, models[0]); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/models/registerModelWizard.test.ts b/extensions/machine-learning-services/src/test/views/models/registerModelWizard.test.ts new file mode 100644 index 0000000000..e898fa47fa --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/models/registerModelWizard.test.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import 'mocha'; +import { createContext } from './utils'; +import { ListModelsEventName, ListAccountsEventName, ListSubscriptionsEventName, ListGroupsEventName, ListWorkspacesEventName, ListAzureModelsEventName } from '../../../views/models/modelViewBase'; +import { RegisteredModel } from '../../../modelManagement/interfaces'; +import { azureResource } from '../../../typings/azure-resource'; +import { Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import { ViewBase } from '../../../views/viewBase'; +import { WorkspaceModel } from '../../../modelManagement/interfaces'; +import { RegisterModelWizard } from '../../../views/models/registerModelWizard'; + +describe('Register Model Wizard', () => { + it('Should create view components successfully ', async function (): Promise { + let testContext = createContext(); + + let view = new RegisterModelWizard(testContext.apiWrapper.object, ''); + view.open(); + + should.notEqual(view.wizardView, undefined); + should.notEqual(view.localModelsComponent, undefined); + should.notEqual(view.azureModelsComponent, undefined); + should.notEqual(view.modelResources, undefined); + }); + + it('Should load data successfully ', async function (): Promise { + let testContext = createContext(); + + let view = new RegisterModelWizard(testContext.apiWrapper.object, ''); + view.open(); + let accounts: azdata.Account[] = [ + { + key: { + accountId: '1', + providerId: '' + }, + displayInfo: { + displayName: 'account', + userId: '', + accountType: '', + contextualDisplayName: '' + }, + isStale: false, + properties: [] + } + ]; + let subscriptions: azureResource.AzureResourceSubscription[] = [ + { + name: 'subscription', + id: '2' + } + ]; + let groups: azureResource.AzureResourceResourceGroup[] = [ + { + name: 'group', + id: '3' + } + ]; + let workspaces: Workspace[] = [ + { + name: 'workspace', + id: '4' + } + ]; + let models: WorkspaceModel[] = [ + { + id: '5', + name: 'model' + } + ]; + let localModels: RegisteredModel[] = [ + { + id: 1, + name: 'model' + } + ]; + view.on(ListModelsEventName, () => { + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListModelsEventName), { data: localModels }); + }); + view.on(ListAccountsEventName, () => { + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListAccountsEventName), { data: accounts }); + }); + view.on(ListSubscriptionsEventName, () => { + + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListSubscriptionsEventName), { data: subscriptions }); + }); + view.on(ListGroupsEventName, () => { + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListGroupsEventName), { data: groups }); + }); + view.on(ListWorkspacesEventName, () => { + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListWorkspacesEventName), { data: workspaces }); + }); + view.on(ListAzureModelsEventName, () => { + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListAzureModelsEventName), { data: models }); + }); + await view.refresh(); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/models/registeredModelsDialog.test.ts b/extensions/machine-learning-services/src/test/views/models/registeredModelsDialog.test.ts new file mode 100644 index 0000000000..c152314d85 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/models/registeredModelsDialog.test.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import 'mocha'; +import { createContext } from './utils'; +import { RegisteredModelsDialog } from '../../../views/models/registeredModelsDialog'; +import { ListModelsEventName } from '../../../views/models/modelViewBase'; +import { RegisteredModel } from '../../../modelManagement/interfaces'; +import { ViewBase } from '../../../views/viewBase'; + +describe('Registered Models Dialog', () => { + it('Should create view components successfully ', async function (): Promise { + let testContext = createContext(); + + let view = new RegisteredModelsDialog(testContext.apiWrapper.object, ''); + view.open(); + + should.notEqual(view.dialogView, undefined); + should.notEqual(view.currentLanguagesTab, undefined); + }); + + it('Should load data successfully ', async function (): Promise { + let testContext = createContext(); + + let view = new RegisteredModelsDialog(testContext.apiWrapper.object, ''); + view.open(); + let models: RegisteredModel[] = [ + { + id: 1, + name: 'model' + } + ]; + view.on(ListModelsEventName, () => { + view.sendCallbackRequest(ViewBase.getCallbackEventName(ListModelsEventName), { data: models }); + }); + await view.refresh(); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/models/utils.ts b/extensions/machine-learning-services/src/test/views/models/utils.ts new file mode 100644 index 0000000000..964ecd93e8 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/models/utils.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { ApiWrapper } from '../../../common/apiWrapper'; +import * as mssql from '../../../../../mssql/src/mssql'; +import { createViewContext } from '../utils'; +import { ModelViewBase } from '../../../views/models/modelViewBase'; + +export interface TestContext { + apiWrapper: TypeMoq.IMock; + view: azdata.ModelView; + languageExtensionService: mssql.ILanguageExtensionService; + onClick: vscode.EventEmitter; +} + +export class ParentDialog extends ModelViewBase { + public refresh(): Promise { + return Promise.resolve(); + } + public reset(): Promise { + return Promise.resolve(); + } + constructor( + apiWrapper: ApiWrapper) { + super(apiWrapper, ''); + } +} + +export function createContext(): TestContext { + + let viewTestContext = createViewContext(); + let languageExtensionService: mssql.ILanguageExtensionService = { + listLanguages: () => { return Promise.resolve([]); }, + deleteLanguage: () => { return Promise.resolve(); }, + updateLanguage: () => { return Promise.resolve(); } + }; + + return { + apiWrapper: viewTestContext.apiWrapper, + view: viewTestContext.view, + languageExtensionService: languageExtensionService, + onClick: viewTestContext.onClick + }; +} diff --git a/extensions/machine-learning-services/src/test/views/utils.ts b/extensions/machine-learning-services/src/test/views/utils.ts new file mode 100644 index 0000000000..9e0e370109 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/utils.ts @@ -0,0 +1,261 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export interface ViewTestContext { + apiWrapper: TypeMoq.IMock; + view: azdata.ModelView; + onClick: vscode.EventEmitter; +} + +export function createViewContext(): ViewTestContext { + let onClick: vscode.EventEmitter = new vscode.EventEmitter(); + + let apiWrapper = TypeMoq.Mock.ofType(ApiWrapper); + let componentBase: azdata.Component = { + id: '', + updateProperties: () => Promise.resolve(), + updateProperty: () => Promise.resolve(), + updateCssStyles: undefined!, + onValidityChanged: undefined!, + valid: true, + validate: undefined!, + focus: undefined! + }; + let button: azdata.ButtonComponent = Object.assign({}, componentBase, { + onDidClick: onClick.event + }); + let radioButton: azdata.RadioButtonComponent = Object.assign({}, componentBase, { + onDidClick: onClick.event + }); + let container = { + clearItems: () => { }, + addItems: () => { }, + addItem: () => { }, + removeItem: () => true, + insertItem: () => { }, + items: [], + setLayout: () => { } + }; + let form: azdata.FormContainer = Object.assign({}, componentBase, container, { + }); + let flex: azdata.FlexContainer = Object.assign({}, componentBase, container, { + }); + + let buttonBuilder: azdata.ComponentBuilder = { + component: () => button, + withProperties: () => buttonBuilder, + withValidation: () => buttonBuilder + }; + let radioButtonBuilder: azdata.ComponentBuilder = { + component: () => radioButton, + withProperties: () => radioButtonBuilder, + withValidation: () => radioButtonBuilder + }; + let inputBox: () => azdata.InputBoxComponent = () => Object.assign({}, componentBase, { + onTextChanged: undefined!, + onEnterKeyPressed: undefined!, + value: '' + }); + let dropdown: () => azdata.DropDownComponent = () => Object.assign({}, componentBase, { + onValueChanged: onClick.event, + value: { + name: '', + displayName: '' + }, + values: [] + }); + let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, { + onDataChanged: undefined!, + data: [], + columns: [] + }); + + let loadingComponent: () => azdata.LoadingComponent = () => Object.assign({}, componentBase, { + loading: false, + component: undefined! + }); + + let declarativeTableBuilder: azdata.ComponentBuilder = { + component: () => declarativeTable(), + withProperties: () => declarativeTableBuilder, + withValidation: () => declarativeTableBuilder + }; + + let loadingBuilder: azdata.LoadingComponentBuilder = { + component: () => loadingComponent(), + withProperties: () => loadingBuilder, + withValidation: () => loadingBuilder, + withItem: () => loadingBuilder + }; + + let formBuilder: azdata.FormBuilder = Object.assign({}, { + component: () => form, + addFormItem: () => { }, + insertFormItem: () => { }, + removeFormItem: () => true, + addFormItems: () => { }, + withFormItems: () => formBuilder, + withProperties: () => formBuilder, + withValidation: () => formBuilder, + withItems: () => formBuilder, + withLayout: () => formBuilder + }); + + let flexBuilder: azdata.FlexBuilder = Object.assign({}, { + component: () => flex, + withProperties: () => flexBuilder, + withValidation: () => flexBuilder, + withItems: () => flexBuilder, + withLayout: () => flexBuilder + }); + + let inputBoxBuilder: azdata.ComponentBuilder = { + component: () => { + let r = inputBox(); + return r; + }, + withProperties: () => inputBoxBuilder, + withValidation: () => inputBoxBuilder + }; + let dropdownBuilder: azdata.ComponentBuilder = { + component: () => { + let r = dropdown(); + return r; + }, + withProperties: () => dropdownBuilder, + withValidation: () => dropdownBuilder + }; + + let view: azdata.ModelView = { + onClosed: undefined!, + connection: undefined!, + serverInfo: undefined!, + valid: true, + onValidityChanged: undefined!, + validate: undefined!, + initializeModel: () => { return Promise.resolve(); }, + modelBuilder: { + radioCardGroup: undefined!, + navContainer: undefined!, + divContainer: undefined!, + flexContainer: () => flexBuilder, + splitViewContainer: undefined!, + dom: undefined!, + card: undefined!, + inputBox: () => inputBoxBuilder, + checkBox: undefined!, + radioButton: () => radioButtonBuilder, + webView: undefined!, + editor: undefined!, + diffeditor: undefined!, + text: () => inputBoxBuilder, + image: undefined!, + button: () => buttonBuilder, + dropDown: () => dropdownBuilder, + tree: undefined!, + listBox: undefined!, + table: undefined!, + declarativeTable: () => declarativeTableBuilder, + dashboardWidget: undefined!, + dashboardWebview: undefined!, + formContainer: () => formBuilder, + groupContainer: undefined!, + toolbarContainer: undefined!, + loadingComponent: () => loadingBuilder, + fileBrowserTree: undefined!, + hyperlink: undefined!, + separator: undefined! + } + }; + let tab: azdata.window.DialogTab = { + title: '', + content: '', + registerContent: async (handler) => { + try { + await handler(view); + } catch (err) { + console.log(err); + } + }, + onValidityChanged: undefined!, + valid: true, + modelView: undefined! + }; + + let dialogButton: azdata.window.Button = { + label: '', + enabled: true, + hidden: false, + onClick: onClick.event, + + }; + let dialogMessage: azdata.window.DialogMessage = { + text: '', + }; + let dialog: azdata.window.Dialog = { + title: '', + isWide: false, + content: [], + okButton: dialogButton, + cancelButton: dialogButton, + customButtons: [], + message: dialogMessage, + registerCloseValidator: () => { }, + registerOperation: () => { }, + onValidityChanged: new vscode.EventEmitter().event, + registerContent: () => { }, + modelView: undefined!, + valid: true + }; + let wizard: azdata.window.Wizard = { + title: '', + pages: [], + currentPage: 0, + doneButton: dialogButton, + cancelButton: dialogButton, + generateScriptButton: dialogButton, + nextButton: dialogButton, + backButton: dialogButton, + customButtons: [], + displayPageTitles: true, + onPageChanged: onClick.event, + addPage: () => { return Promise.resolve(); }, + removePage: () => { return Promise.resolve(); }, + setCurrentPage: () => { return Promise.resolve(); }, + open: () => { return Promise.resolve(); }, + close: () => { return Promise.resolve(); }, + registerNavigationValidator: () => { }, + message: dialogMessage, + registerOperation: () => { } + }; + let wizardPage: azdata.window.WizardPage = { + title: '', + content: '', + customButtons: [], + enabled: true, + description: '', + onValidityChanged: onClick.event, + registerContent: () => { }, + modelView: undefined!, + valid: true + }; + apiWrapper.setup(x => x.createTab(TypeMoq.It.isAny())).returns(() => tab); + apiWrapper.setup(x => x.createWizard(TypeMoq.It.isAny())).returns(() => wizard); + apiWrapper.setup(x => x.createWizardPage(TypeMoq.It.isAny())).returns(() => wizardPage); + apiWrapper.setup(x => x.createModelViewDialog(TypeMoq.It.isAny())).returns(() => dialog); + apiWrapper.setup(x => x.openDialog(TypeMoq.It.isAny())).returns(() => { }); + + return { + apiWrapper: apiWrapper, + view: view, + onClick: onClick, + }; +} + diff --git a/extensions/machine-learning-services/src/typings/azure-resource.d.ts b/extensions/machine-learning-services/src/typings/azure-resource.d.ts new file mode 100644 index 0000000000..2626f67367 --- /dev/null +++ b/extensions/machine-learning-services/src/typings/azure-resource.d.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Account } from 'azdata'; +import * as msRest from '@azure/ms-rest-js'; + +export namespace azureResource { + + export interface AzureResource { + name: string; + id: string; + } + + export interface AzureResourceSubscription extends AzureResource { + } + + export interface AzureResourceResourceGroup extends AzureResource { + } + + export interface IAzureResourceService { + getResources(subscription: AzureResourceSubscription, credential: msRest.ServiceClientCredentials): Promise; + } + + export type GetSubscriptionsResult = { subscriptions: AzureResourceSubscription[], errors: Error[] }; + export type GetResourceGroupsResult = { resourceGroups: AzureResourceResourceGroup[], errors: Error[] }; +} diff --git a/extensions/machine-learning-services/src/views/controllerBase.ts b/extensions/machine-learning-services/src/views/controllerBase.ts new file mode 100644 index 0000000000..c13a22f175 --- /dev/null +++ b/extensions/machine-learning-services/src/views/controllerBase.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ViewBase, LocalFileEventName, LocalFolderEventName } from './viewBase'; +import { ApiWrapper } from '../common/apiWrapper'; + +/** + * Base classes for UI controllers + */ +export abstract class ControllerBase { + + /** + * creates new instance + */ + constructor(protected _apiWrapper: ApiWrapper) { + } + + /** + * Executes an action and sends back callback event to the view + */ + public async executeAction(dialog: T, eventName: string, func: (...args: any[]) => Promise, ...args: any[]): Promise { + const callbackEvent = ViewBase.getCallbackEventName(eventName); + try { + let result = await func(...args); + dialog.sendCallbackRequest(callbackEvent, { data: result }); + + } catch (error) { + dialog.sendCallbackRequest(callbackEvent, { error: error }); + } + } + + /** + * Register common events for views + * @param view view + */ + public registerEvents(view: ViewBase): void { + view.on(LocalFileEventName, async () => { + await this.executeAction(view, LocalFileEventName, this.getLocalFilePath, this._apiWrapper); + }); + view.on(LocalFolderEventName, async () => { + await this.executeAction(view, LocalFolderEventName, this.getLocalFolderPath, this._apiWrapper); + }); + } + + /** + * Returns local file path picked by the user + * @param apiWrapper apiWrapper + */ + public async getLocalFilePath(apiWrapper: ApiWrapper): Promise { + let result = await apiWrapper.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false + }); + return result && result.length > 0 ? result[0].fsPath : ''; + } + + /** + * Returns local folder path picked by the user + * @param apiWrapper apiWrapper + */ + public async getLocalFolderPath(apiWrapper: ApiWrapper): Promise { + let result = await apiWrapper.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false + }); + return result && result.length > 0 ? result[0].fsPath : ''; + } +} diff --git a/extensions/machine-learning-services/src/views/dialogView.ts b/extensions/machine-learning-services/src/views/dialogView.ts new file mode 100644 index 0000000000..152d8f4199 --- /dev/null +++ b/extensions/machine-learning-services/src/views/dialogView.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ApiWrapper } from '../common/apiWrapper'; +import { MainViewBase } from './mainViewBase'; +import { IPageView } from './interfaces'; + +/** + * Dialog view to create and manage a dialog + */ +export class DialogView extends MainViewBase { + + private _dialog: azdata.window.Dialog | undefined; + + /** + * Creates new instance + */ + constructor(apiWrapper: ApiWrapper) { + super(apiWrapper); + } + + private createDialogPage(title: string, componentView: IPageView): azdata.window.DialogTab { + let viewPanel = this._apiWrapper.createTab(title); + this.addPage(componentView); + this.registerContent(viewPanel, componentView); + return viewPanel; + } + + /** + * Creates a new dialog + * @param title title + * @param pages pages + */ + public createDialog(title: string, pages: IPageView[]): azdata.window.Dialog { + this._dialog = this._apiWrapper.createModelViewDialog(title); + this._dialog.content = pages.map(x => this.createDialogPage(x.title || '', x)); + return this._dialog; + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts b/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts index aa3f2472eb..386ac052de 100644 --- a/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts +++ b/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts @@ -27,7 +27,7 @@ export class CurrentLanguagesTab extends LanguageViewBase { // TODO: only supporting single location for now. We should add a drop down for multi locations mode // - let locationTitle = await this.getLocationTitle(); + let locationTitle = await this.getServerTitle(); this._locationComponent = view.modelBuilder.text().withProperties({ value: locationTitle }).component(); diff --git a/extensions/machine-learning-services/src/externalLanguage/languageController.ts b/extensions/machine-learning-services/src/views/externalLanguages/languageController.ts similarity index 87% rename from extensions/machine-learning-services/src/externalLanguage/languageController.ts rename to extensions/machine-learning-services/src/views/externalLanguages/languageController.ts index 920c5e2439..63ad405bd4 100644 --- a/extensions/machine-learning-services/src/externalLanguage/languageController.ts +++ b/extensions/machine-learning-services/src/views/externalLanguages/languageController.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as mssql from '../../../mssql/src/mssql'; -import { ApiWrapper } from '../common/apiWrapper'; -import { LanguageService } from './languageService'; -import { LanguagesDialog } from '../views/externalLanguages/languagesDialog'; -import { LanguageEditDialog } from '../views/externalLanguages/languageEditDialog'; -import { FileBrowserDialog } from '../views/externalLanguages/fileBrowserDialog'; -import { LanguageViewBase, LanguageUpdateModel } from '../views/externalLanguages/languageViewBase'; -import * as constants from '../common/constants'; +import * as mssql from '../../../../mssql/src/mssql'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { LanguageService } from '../../externalLanguage/languageService'; +import { LanguagesDialog } from './languagesDialog'; +import { LanguageEditDialog } from './languageEditDialog'; +import { FileBrowserDialog } from './fileBrowserDialog'; +import { LanguageViewBase, LanguageUpdateModel } from './languageViewBase'; +import * as constants from '../../common/constants'; export class LanguageController { diff --git a/extensions/machine-learning-services/src/views/interfaces.ts b/extensions/machine-learning-services/src/views/interfaces.ts new file mode 100644 index 0000000000..294dd82d78 --- /dev/null +++ b/extensions/machine-learning-services/src/views/interfaces.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { azureResource } from '../typings/azure-resource'; +import { Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import { WorkspaceModel } from '../modelManagement/interfaces'; + +export interface IDataComponent { + data: T | undefined; +} + +export interface IPageView { + registerComponent: (modelBuilder: azdata.ModelBuilder) => azdata.Component; + component: azdata.Component | undefined; + onEnter?: () => Promise; + onLeave?: () => Promise; + refresh: () => Promise; + viewPanel: azdata.window.ModelViewPanel | undefined; + title: string; +} + +export interface AzureWorkspaceResource { + account?: azdata.Account, + subscription?: azureResource.AzureResourceSubscription, + group?: azureResource.AzureResource, + workspace?: Workspace +} + +export interface AzureModelResource extends AzureWorkspaceResource { + model?: WorkspaceModel; +} + diff --git a/extensions/machine-learning-services/src/views/mainViewBase.ts b/extensions/machine-learning-services/src/views/mainViewBase.ts new file mode 100644 index 0000000000..422382e494 --- /dev/null +++ b/extensions/machine-learning-services/src/views/mainViewBase.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ApiWrapper } from '../common/apiWrapper'; +import { IPageView } from './interfaces'; + +/** + * Base class for dialog and wizard + */ +export class MainViewBase { + + protected _pages: IPageView[] = []; + + /** + * + */ + constructor(protected _apiWrapper: ApiWrapper) { + } + + protected registerContent(viewPanel: azdata.window.DialogTab | azdata.window.WizardPage, componentView: IPageView) { + viewPanel.registerContent(async view => { + if (componentView) { + let component = componentView.registerComponent(view.modelBuilder); + await view.initializeModel(component); + await componentView.refresh(); + } + }); + } + + protected addPage(page: IPageView, index?: number): void { + if (index) { + this._pages[index] = page; + } else { + this._pages.push(page); + } + } + + public async refresh(): Promise { + if (this._pages) { + await Promise.all(this._pages.map(p => p.refresh())); + } + } +} diff --git a/extensions/machine-learning-services/src/views/models/azureModelsComponent.ts b/extensions/machine-learning-services/src/views/models/azureModelsComponent.ts new file mode 100644 index 0000000000..2557b9144e --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/azureModelsComponent.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ModelViewBase } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { AzureResourceFilterComponent } from './azureResourceFilterComponent'; +import { AzureModelsTable } from './azureModelsTable'; +import * as constants from '../../common/constants'; +import { IPageView, IDataComponent, AzureModelResource } from '../interfaces'; + +export class AzureModelsComponent extends ModelViewBase implements IPageView, IDataComponent { + + public azureModelsTable: AzureModelsTable | undefined; + public azureFilterComponent: AzureResourceFilterComponent | undefined; + + private _loader: azdata.LoadingComponent | undefined; + private _form: azdata.FormContainer | undefined; + + /** + * Component to render a view to pick an azure model + */ + constructor(apiWrapper: ApiWrapper, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + } + + /** + * Register components + * @param modelBuilder model builder + */ + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { + this.azureFilterComponent = new AzureResourceFilterComponent(this._apiWrapper, modelBuilder, this); + this.azureModelsTable = new AzureModelsTable(this._apiWrapper, modelBuilder, this); + this._loader = modelBuilder.loadingComponent() + .withItem(this.azureModelsTable.component) + .withProperties({ + loading: true + }).component(); + + this.azureFilterComponent.onWorkspacesSelected(async () => { + await this.onLoading(); + await this.azureModelsTable?.loadData(this.azureFilterComponent?.data); + await this.onLoaded(); + }); + + this._form = modelBuilder.formContainer().withFormItems([{ + title: constants.azureModelFilter, + component: this.azureFilterComponent.component + }, { + title: constants.azureModels, + component: this._loader + }]).component(); + return this._form; + } + + private async onLoading(): Promise { + if (this._loader) { + await this._loader.updateProperties({ loading: true }); + } + } + + private async onLoaded(): Promise { + if (this._loader) { + await this._loader.updateProperties({ loading: false }); + } + } + + public get component(): azdata.Component | undefined { + return this._form; + } + + /** + * Loads the data in the components + */ + public async loadData(): Promise { + await this.azureFilterComponent?.loadData(); + } + + /** + * Returns selected data + */ + public get data(): AzureModelResource | undefined { + return Object.assign({}, this.azureFilterComponent?.data, { + model: this.azureModelsTable?.data + }); + } + + /** + * Refreshes the view + */ + public async refresh(): Promise { + await this.loadData(); + } + + /** + * Returns the title of the page + */ + public get title(): string { + return constants.azureModelsTitle; + } +} diff --git a/extensions/machine-learning-services/src/views/models/azureModelsTable.ts b/extensions/machine-learning-services/src/views/models/azureModelsTable.ts new file mode 100644 index 0000000000..ed40f434d7 --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/azureModelsTable.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../../common/constants'; +import { ModelViewBase } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { WorkspaceModel } from '../../modelManagement/interfaces'; +import { IDataComponent, AzureWorkspaceResource } from '../interfaces'; + +/** + * View to render azure models in a table + */ +export class AzureModelsTable extends ModelViewBase implements IDataComponent { + + private _table: azdata.DeclarativeTableComponent; + private _selectedModelId: any; + private _models: WorkspaceModel[] | undefined; + + /** + * Creates a view to render azure models in a table + */ + constructor(apiWrapper: ApiWrapper, private _modelBuilder: azdata.ModelBuilder, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + this._table = this.registerComponent(this._modelBuilder); + } + + /** + * Register components + * @param modelBuilder model builder + */ + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.DeclarativeTableComponent { + this._table = modelBuilder.declarativeTable() + .withProperties( + { + columns: [ + { // Id + displayName: constants.modeIld, + ariaLabel: constants.modeIld, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 100, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, + { // Name + displayName: constants.modelName, + ariaLabel: constants.modelName, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 150, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, + { // Action + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 50, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + } + ], + data: [], + ariaLabel: constants.mlsConfigTitle + }) + .component(); + return this._table; + } + + public get component(): azdata.DeclarativeTableComponent { + return this._table; + } + + /** + * Load data in the component + * @param workspaceResource Azure workspace + */ + public async loadData(workspaceResource?: AzureWorkspaceResource | undefined): Promise { + + if (this._table && workspaceResource) { + this._models = await this.listAzureModels(workspaceResource); + let tableData: any[][] = []; + + if (this._models) { + tableData = tableData.concat(this._models.map(model => this.createTableRow(model))); + } + + this._table.data = tableData; + } + } + + private createTableRow(model: WorkspaceModel): any[] { + if (this._modelBuilder) { + let selectModelButton = this._modelBuilder.radioButton().withProperties({ + name: 'amlModel', + value: model.id, + width: 15, + height: 15, + checked: false + }).component(); + selectModelButton.onDidClick(() => { + this._selectedModelId = model.id; + }); + return [model.id, model.name, selectModelButton]; + } + + return []; + } + + /** + * Returns selected data + */ + public get data(): WorkspaceModel | undefined { + if (this._models && this._selectedModelId) { + return this._models.find(x => x.id === this._selectedModelId); + } + return undefined; + } + + /** + * Refreshes the view + */ + public async refresh(): Promise { + await this.loadData(); + } +} diff --git a/extensions/machine-learning-services/src/views/models/azureResourceFilterComponent.ts b/extensions/machine-learning-services/src/views/models/azureResourceFilterComponent.ts new file mode 100644 index 0000000000..ed474a3eaa --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/azureResourceFilterComponent.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ModelViewBase } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { azureResource } from '../../typings/azure-resource'; +import { Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import * as constants from '../../common/constants'; +import { AzureWorkspaceResource, IDataComponent } from '../interfaces'; + +/** + * View to render filters to pick an azure resource + */ +const componentWidth = 200; +export class AzureResourceFilterComponent extends ModelViewBase implements IDataComponent { + + private _form: azdata.FormContainer; + private _accounts: azdata.DropDownComponent; + private _subscriptions: azdata.DropDownComponent; + private _groups: azdata.DropDownComponent; + private _workspaces: azdata.DropDownComponent; + private _azureAccounts: azdata.Account[] = []; + private _azureSubscriptions: azureResource.AzureResourceSubscription[] = []; + private _azureGroups: azureResource.AzureResource[] = []; + private _azureWorkspaces: Workspace[] = []; + private _onWorkspacesSelected: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onWorkspacesSelected: vscode.Event = this._onWorkspacesSelected.event; + + /** + * Creates a new view + */ + constructor(apiWrapper: ApiWrapper, private _modelBuilder: azdata.ModelBuilder, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + this._accounts = this._modelBuilder.dropDown().withProperties({ + width: componentWidth + }).component(); + this._subscriptions = this._modelBuilder.dropDown().withProperties({ + width: componentWidth + }).component(); + this._groups = this._modelBuilder.dropDown().withProperties({ + width: componentWidth + }).component(); + this._workspaces = this._modelBuilder.dropDown().withProperties({ + width: componentWidth + }).component(); + + this._accounts.onValueChanged(async () => { + await this.onAccountSelected(); + }); + + this._subscriptions.onValueChanged(async () => { + await this.onSubscriptionSelected(); + }); + this._groups.onValueChanged(async () => { + await this.onGroupSelected(); + }); + this._workspaces.onValueChanged(async () => { + await this.onWorkspaceSelected(); + }); + + this._form = this._modelBuilder.formContainer().withFormItems([{ + title: constants.azureAccount, + component: this._accounts + }, { + title: constants.azureSubscription, + component: this._subscriptions + }, { + title: constants.azureGroup, + component: this._groups + }, { + title: constants.azureModelWorkspace, + component: this._workspaces + }]).component(); + } + + /** + * Returns the created component + */ + public get component(): azdata.Component { + return this._form; + } + + /** + * Returns selected data + */ + public get data(): AzureWorkspaceResource | undefined { + return { + account: this.account, + subscription: this.subscription, + group: this.group, + workspace: this.workspace + }; + } + + /** + * loads data in the components + */ + public async loadData(): Promise { + this._azureAccounts = await this.listAzureAccounts(); + if (this._azureAccounts && this._azureAccounts.length > 0) { + let values = this._azureAccounts.map(a => { return { displayName: a.displayInfo.displayName, name: a.key.accountId }; }); + this._accounts.values = values; + this._accounts.value = values[0]; + } + await this.onAccountSelected(); + } + + /** + * refreshes the view + */ + public async refresh(): Promise { + await this.loadData(); + } + + private async onAccountSelected(): Promise { + this._azureSubscriptions = await this.listAzureSubscriptions(this.account); + if (this._azureSubscriptions && this._azureSubscriptions.length > 0) { + let values = this._azureSubscriptions.map(s => { return { displayName: s.name, name: s.id }; }); + this._subscriptions.values = values; + this._subscriptions.value = values[0]; + } + await this.onSubscriptionSelected(); + } + + private async onSubscriptionSelected(): Promise { + this._azureGroups = await this.listAzureGroups(this.account, this.subscription); + if (this._azureGroups && this._azureGroups.length > 0) { + let values = this._azureGroups.map(s => { return { displayName: s.name, name: s.id }; }); + this._groups.values = values; + this._groups.value = values[0]; + } + await this.onGroupSelected(); + } + + private async onGroupSelected(): Promise { + this._azureWorkspaces = await this.listWorkspaces(this.account, this.subscription, this.group); + if (this._azureWorkspaces && this._azureWorkspaces.length > 0) { + let values = this._azureWorkspaces.map(s => { return { displayName: s.name || '', name: s.id || '' }; }); + this._workspaces.values = values; + this._workspaces.value = values[0]; + } + this.onWorkspaceSelected(); + } + + private onWorkspaceSelected(): void { + this._onWorkspacesSelected.fire(); + } + + private get workspace(): Workspace | undefined { + return this._azureWorkspaces ? this._azureWorkspaces.find(a => a.id === (this._workspaces.value).name) : undefined; + } + + private get account(): azdata.Account | undefined { + return this._azureAccounts ? this._azureAccounts.find(a => a.key.accountId === (this._accounts.value).name) : undefined; + } + + private get group(): azureResource.AzureResource | undefined { + return this._azureGroups ? this._azureGroups.find(a => a.id === (this._groups.value).name) : undefined; + } + + private get subscription(): azureResource.AzureResourceSubscription | undefined { + return this._azureSubscriptions ? this._azureSubscriptions.find(a => a.id === (this._subscriptions.value).name) : undefined; + } +} diff --git a/extensions/machine-learning-services/src/views/models/currentModelsPage.ts b/extensions/machine-learning-services/src/views/models/currentModelsPage.ts new file mode 100644 index 0000000000..f3bd38cc1a --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/currentModelsPage.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../../common/constants'; +import { ModelViewBase, RegisterModelEventName } from './modelViewBase'; +import { CurrentModelsTable } from './currentModelsTable'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { IPageView } from '../interfaces'; + +/** + * View to render current registered models + */ +export class CurrentModelsPage extends ModelViewBase implements IPageView { + private _tableComponent: azdata.DeclarativeTableComponent | undefined; + private _dataTable: CurrentModelsTable | undefined; + private _loader: azdata.LoadingComponent | undefined; + + /** + * + * @param apiWrapper Creates new view + * @param parent page parent + */ + constructor(apiWrapper: ApiWrapper, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + } + + /** + * + * @param modelBuilder register the components + */ + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { + this._dataTable = new CurrentModelsTable(this._apiWrapper, modelBuilder, this); + this._tableComponent = this._dataTable.component; + + let registerButton = modelBuilder.button().withProperties({ + label: constants.registerModelButton, + width: this.buttonMaxLength + }).component(); + registerButton.onDidClick(async () => { + await this.sendDataRequest(RegisterModelEventName); + }); + + let formModel = modelBuilder.formContainer() + .withFormItems([{ + title: '', + component: registerButton + }, { + component: this._tableComponent, + title: '' + }]).component(); + + this._loader = modelBuilder.loadingComponent() + .withItem(formModel) + .withProperties({ + loading: true + }).component(); + return this._loader; + } + + /** + * Returns the component + */ + public get component(): azdata.Component | undefined { + return this._loader; + } + + /** + * Refreshes the view + */ + public async refresh(): Promise { + await this.onLoading(); + + try { + await this._dataTable?.refresh(); + } catch (err) { + this.showErrorMessage(constants.getErrorMessage(err)); + } finally { + await this.onLoaded(); + } + } + + /** + * returns the title of the page + */ + public get title(): string { + return constants.currentModelsTitle; + } + + private async onLoading(): Promise { + if (this._loader) { + await this._loader.updateProperties({ loading: true }); + } + } + + private async onLoaded(): Promise { + if (this._loader) { + await this._loader.updateProperties({ loading: false }); + } + } +} diff --git a/extensions/machine-learning-services/src/views/models/currentModelsTable.ts b/extensions/machine-learning-services/src/views/models/currentModelsTable.ts new file mode 100644 index 0000000000..2875974dc0 --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/currentModelsTable.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../../common/constants'; +import { ModelViewBase } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { RegisteredModel } from '../../modelManagement/interfaces'; + +/** + * View to render registered models table + */ +export class CurrentModelsTable extends ModelViewBase { + + private _table: azdata.DeclarativeTableComponent; + + /** + * Creates new view + */ + constructor(apiWrapper: ApiWrapper, private _modelBuilder: azdata.ModelBuilder, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + this._table = this.registerComponent(this._modelBuilder); + } + + /** + * + * @param modelBuilder register the components + */ + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.DeclarativeTableComponent { + this._table = modelBuilder.declarativeTable() + .withProperties( + { + columns: [ + { // Id + displayName: constants.modeIld, + ariaLabel: constants.modeIld, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 100, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, + { // Name + displayName: constants.modelName, + ariaLabel: constants.modelName, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 150, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, + { // Action + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 50, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + } + ], + data: [], + ariaLabel: constants.mlsConfigTitle + }) + .component(); + return this._table; + } + + /** + * Returns the component + */ + public get component(): azdata.DeclarativeTableComponent { + return this._table; + } + + /** + * Loads the data in the component + */ + public async loadData(): Promise { + let models: RegisteredModel[] | undefined; + + models = await this.listModels(); + let tableData: any[][] = []; + + if (models) { + tableData = tableData.concat(models.map(model => this.createTableRow(model))); + } + + this._table.data = tableData; + } + + private createTableRow(model: RegisteredModel): any[] { + if (this._modelBuilder) { + let editLanguageButton = this._modelBuilder.button().withProperties({ + label: '', + title: constants.deleteTitle, + iconPath: { + dark: this.asAbsolutePath('images/dark/edit_inverse.svg'), + light: this.asAbsolutePath('images/light/edit.svg') + }, + width: 15, + height: 15 + }).component(); + editLanguageButton.onDidClick(() => { + }); + return [model.id, model.name, editLanguageButton]; + } + + return []; + } + + /** + * Refreshes the view + */ + public async refresh(): Promise { + await this.loadData(); + } +} diff --git a/extensions/machine-learning-services/src/views/models/localModelsComponent.ts b/extensions/machine-learning-services/src/views/models/localModelsComponent.ts new file mode 100644 index 0000000000..73ccaccc34 --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/localModelsComponent.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ModelViewBase } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import * as constants from '../../common/constants'; +import { IPageView, IDataComponent } from '../interfaces'; + +/** + * View to pick local models file + */ +export class LocalModelsComponent extends ModelViewBase implements IPageView, IDataComponent { + + private _form: azdata.FormContainer | undefined; + private _localPath: azdata.InputBoxComponent | undefined; + private _localBrowse: azdata.ButtonComponent | undefined; + + /** + * Creates new view + */ + constructor(apiWrapper: ApiWrapper, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + } + + /** + * + * @param modelBuilder Register the components + */ + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { + this._localPath = modelBuilder.inputBox().withProperties({ + value: '', + width: this.componentMaxLength - this.browseButtonMaxLength - this.spaceBetweenComponentsLength + }).component(); + this._localBrowse = modelBuilder.button().withProperties({ + label: constants.browseModels, + width: this.browseButtonMaxLength, + CSSStyles: { + 'text-align': 'end' + } + }).component(); + this._localBrowse.onDidClick(async () => { + const filePath = await this.getLocalFilePath(); + if (this._localPath) { + this._localPath.value = filePath; + } + }); + + let flexFilePathModel = modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + justifyContent: 'space-between' + }).withItems([ + this._localPath, this._localBrowse] + ).component(); + + this._form = modelBuilder.formContainer().withFormItems([{ + title: '', + component: flexFilePathModel + }]).component(); + return this._form; + } + + /** + * Returns selected data + */ + public get data(): string { + return this._localPath?.value || ''; + } + + /** + * Returns the component + */ + public get component(): azdata.Component | undefined { + return this._form; + } + + /** + * Refreshes the view + */ + public async refresh(): Promise { + } + + /** + * Returns the page title + */ + public get title(): string { + return constants.localModelsTitle; + } +} diff --git a/extensions/machine-learning-services/src/views/models/modelManagementController.ts b/extensions/machine-learning-services/src/views/models/modelManagementController.ts new file mode 100644 index 0000000000..d030c908db --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/modelManagementController.ts @@ -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 azdata from 'azdata'; + +import { azureResource } from '../../typings/azure-resource'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { AzureModelRegistryService } from '../../modelManagement/azureModelRegistryService'; +import { Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import { RegisteredModel, WorkspaceModel } from '../../modelManagement/interfaces'; +import { RegisteredModelService } from '../../modelManagement/registeredModelService'; +import { RegisteredModelsDialog } from './registeredModelsDialog'; +import { AzureResourceEventArgs, ListAzureModelsEventName, ListSubscriptionsEventName, ListModelsEventName, ListWorkspacesEventName, ListGroupsEventName, ListAccountsEventName, RegisterLocalModelEventName, RegisterLocalModelEventArgs, RegisterAzureModelEventName, RegisterAzureModelEventArgs, ModelViewBase, SourceModelSelectedEventName, RegisterModelEventName } from './modelViewBase'; +import { ControllerBase } from '../controllerBase'; +import { RegisterModelWizard } from './registerModelWizard'; +import * as fs from 'fs'; +import * as constants from '../../common/constants'; + +/** + * Model management UI controller + */ +export class ModelManagementController extends ControllerBase { + + /** + * Creates new instance + */ + constructor( + apiWrapper: ApiWrapper, + private _root: string, + private _amlService: AzureModelRegistryService, + private _registeredModelService: RegisteredModelService) { + super(apiWrapper); + } + + /** + * Opens the dialog for model registration + * @param parent parent if the view is opened from another view + * @param controller controller + * @param apiWrapper apiWrapper + * @param root root folder path + */ + public async registerModel(parent?: ModelViewBase, controller?: ModelManagementController, apiWrapper?: ApiWrapper, root?: string): Promise { + controller = controller || this; + apiWrapper = apiWrapper || this._apiWrapper; + root = root || this._root; + let view = new RegisterModelWizard(apiWrapper, root, parent); + + controller.registerEvents(view); + + // Open view + // + view.open(); + return view; + } + + /** + * Register events in the main view + * @param view main view + */ + public registerEvents(view: ModelViewBase): void { + + // Register events + // + super.registerEvents(view); + view.on(ListAccountsEventName, async () => { + await this.executeAction(view, ListAccountsEventName, this.getAzureAccounts, this._amlService); + }); + view.on(ListSubscriptionsEventName, async (arg) => { + let azureArgs = arg; + await this.executeAction(view, ListSubscriptionsEventName, this.getAzureSubscriptions, this._amlService, azureArgs.account); + }); + view.on(ListWorkspacesEventName, async (arg) => { + let azureArgs = arg; + await this.executeAction(view, ListWorkspacesEventName, this.getWorkspaces, this._amlService, azureArgs.account, azureArgs.subscription, azureArgs.group); + }); + view.on(ListGroupsEventName, async (arg) => { + let azureArgs = arg; + await this.executeAction(view, ListGroupsEventName, this.getAzureGroups, this._amlService, azureArgs.account, azureArgs.subscription); + }); + view.on(ListAzureModelsEventName, async (arg) => { + let azureArgs = arg; + await this.executeAction(view, ListAzureModelsEventName, this.getAzureModels, this._amlService + , azureArgs.account, azureArgs.subscription, azureArgs.group, azureArgs.workspace); + }); + + view.on(ListModelsEventName, async () => { + await this.executeAction(view, ListModelsEventName, this.getRegisteredModels, this._registeredModelService); + }); + view.on(RegisterLocalModelEventName, async (arg) => { + let registerArgs = arg; + await this.executeAction(view, RegisterLocalModelEventName, this.registerLocalModel, this._registeredModelService, registerArgs.filePath); + view.refresh(); + }); + view.on(RegisterModelEventName, async () => { + await this.executeAction(view, RegisterModelEventName, this.registerModel, view, this, this._apiWrapper, this._root); + }); + view.on(RegisterAzureModelEventName, async (arg) => { + let registerArgs = arg; + await this.executeAction(view, RegisterAzureModelEventName, this.registerAzureModel, this._amlService, this._registeredModelService, + registerArgs.account, registerArgs.subscription, registerArgs.group, registerArgs.workspace, registerArgs.model); + }); + view.on(SourceModelSelectedEventName, () => { + view.refresh(); + }); + } + + /** + * Opens the dialog for model management + */ + public async manageRegisteredModels(): Promise { + let view = new RegisteredModelsDialog(this._apiWrapper, this._root); + + // Register events + // + this.registerEvents(view); + + // Open view + // + view.open(); + return view; + } + + private async getAzureAccounts(service: AzureModelRegistryService): Promise { + return await service.getAccounts(); + } + + private async getAzureSubscriptions(service: AzureModelRegistryService, account: azdata.Account | undefined): Promise { + return await service.getSubscriptions(account); + } + + private async getAzureGroups(service: AzureModelRegistryService, account: azdata.Account | undefined, subscription: azureResource.AzureResourceSubscription | undefined): Promise { + return await service.getGroups(account, subscription); + } + + private async getWorkspaces(service: AzureModelRegistryService, account: azdata.Account | undefined, subscription: azureResource.AzureResourceSubscription | undefined, group: azureResource.AzureResource | undefined): Promise { + if (!account || !subscription) { + return []; + } + return await service.getWorkspaces(account, subscription, group); + } + + private async getRegisteredModels(registeredModelService: RegisteredModelService): Promise { + return registeredModelService.getRegisteredModels(); + } + + private async getAzureModels( + service: AzureModelRegistryService, + account: azdata.Account | undefined, + subscription: azureResource.AzureResourceSubscription | undefined, + resourceGroup: azureResource.AzureResource | undefined, + workspace: Workspace | undefined): Promise { + if (!account || !subscription || !resourceGroup || !workspace) { + return []; + } + return await service.getModels(account, subscription, resourceGroup, workspace) || []; + } + + private async registerLocalModel(service: RegisteredModelService, filePath?: string): Promise { + if (filePath) { + await service.registerLocalModel(filePath); + } else { + throw Error(constants.invalidModelToRegisterError); + + } + } + + private async registerAzureModel( + azureService: AzureModelRegistryService, + service: RegisteredModelService, + account: azdata.Account | undefined, + subscription: azureResource.AzureResourceSubscription | undefined, + resourceGroup: azureResource.AzureResource | undefined, + workspace: Workspace | undefined, + model: WorkspaceModel | undefined): Promise { + if (!account || !subscription || !resourceGroup || !workspace || !model) { + throw Error(constants.invalidAzureResourceError); + } + const filePath = await azureService.downloadModel(account, subscription, resourceGroup, workspace, model); + if (filePath) { + await service.registerLocalModel(filePath); + await fs.promises.unlink(filePath); + } else { + throw Error(constants.invalidModelToRegisterError); + } + } +} diff --git a/extensions/machine-learning-services/src/views/models/modelSourcesComponent.ts b/extensions/machine-learning-services/src/views/models/modelSourcesComponent.ts new file mode 100644 index 0000000000..935873c032 --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/modelSourcesComponent.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ModelViewBase, SourceModelSelectedEventName } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import * as constants from '../../common/constants'; +import { IPageView, IDataComponent } from '../interfaces'; + +export enum ModelSourceType { + Local, + Azure +} +/** + * View tp pick model source + */ +export class ModelSourcesComponent extends ModelViewBase implements IPageView, IDataComponent { + + private _form: azdata.FormContainer | undefined; + private _amlModel: azdata.RadioButtonComponent | undefined; + private _localModel: azdata.RadioButtonComponent | undefined; + private _isLocalModel: boolean = true; + + constructor(apiWrapper: ApiWrapper, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + } + + /** + * + * @param modelBuilder Register components + */ + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { + this._localModel = modelBuilder.radioButton() + .withProperties({ + value: 'local', + name: 'modelLocation', + label: constants.localModelSource, + checked: true + }).component(); + + + this._amlModel = modelBuilder.radioButton() + .withProperties({ + value: 'aml', + name: 'modelLocation', + label: constants.azureModelSource, + }).component(); + + this._localModel.onDidClick(() => { + this._isLocalModel = true; + this.sendRequest(SourceModelSelectedEventName); + + }); + this._amlModel.onDidClick(() => { + this._isLocalModel = false; + this.sendRequest(SourceModelSelectedEventName); + }); + + let flex = modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + justifyContent: 'space-between' + }).withItems([ + this._localModel, this._amlModel] + ).component(); + + this._form = modelBuilder.formContainer().withFormItems([{ + title: constants.modelSourcesTitle, + component: flex + }]).component(); + return this._form; + } + + /** + * Returns selected data + */ + public get data(): ModelSourceType { + return this._isLocalModel ? ModelSourceType.Local : ModelSourceType.Azure; + } + + /** + * Returns the component + */ + public get component(): azdata.Component | undefined { + return this._form; + } + + /** + * Refreshes the view + */ + public async refresh(): Promise { + } + + /** + * Returns page title + */ + public get title(): string { + return constants.modelSourcesTitle; + } +} diff --git a/extensions/machine-learning-services/src/views/models/modelViewBase.ts b/extensions/machine-learning-services/src/views/models/modelViewBase.ts new file mode 100644 index 0000000000..3c2f873697 --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/modelViewBase.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { azureResource } from '../../typings/azure-resource'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { ViewBase } from '../viewBase'; +import { RegisteredModel, WorkspaceModel } from '../../modelManagement/interfaces'; +import { Workspace } from '@azure/arm-machinelearningservices/esm/models'; +import { AzureWorkspaceResource, AzureModelResource } from '../interfaces'; + +export interface AzureResourceEventArgs extends AzureWorkspaceResource { +} + +export interface RegisterAzureModelEventArgs extends AzureModelResource { + model?: WorkspaceModel; +} + +export interface RegisterLocalModelEventArgs extends AzureResourceEventArgs { + filePath?: string; +} + +// Event names +// +export const ListModelsEventName = 'listModels'; +export const ListAzureModelsEventName = 'listAzureModels'; +export const ListAccountsEventName = 'listAccounts'; +export const ListSubscriptionsEventName = 'listSubscriptions'; +export const ListGroupsEventName = 'listGroups'; +export const ListWorkspacesEventName = 'listWorkspaces'; +export const RegisterLocalModelEventName = 'registerLocalModel'; +export const RegisterAzureModelEventName = 'registerAzureLocalModel'; +export const RegisterModelEventName = 'registerModel'; +export const SourceModelSelectedEventName = 'sourceModelSelected'; + +/** + * Base class for all model management views + */ +export abstract class ModelViewBase extends ViewBase { + + constructor(apiWrapper: ApiWrapper, root?: string, parent?: ModelViewBase) { + super(apiWrapper, root, parent); + } + + protected getEventNames(): string[] { + return super.getEventNames().concat([ListModelsEventName, + ListAzureModelsEventName, + ListAccountsEventName, + ListSubscriptionsEventName, + ListGroupsEventName, + ListWorkspacesEventName, + RegisterLocalModelEventName, + RegisterAzureModelEventName, + RegisterModelEventName, + SourceModelSelectedEventName]); + } + + /** + * Parent view + */ + public get parent(): ModelViewBase | undefined { + return this._parent ? this._parent : undefined; + } + + /** + * list azure models + */ + public async listAzureModels(workspaceResource: AzureWorkspaceResource): Promise { + const args: AzureResourceEventArgs = workspaceResource; + return await this.sendDataRequest(ListAzureModelsEventName, args); + } + + /** + * list registered models + */ + public async listModels(): Promise { + return await this.sendDataRequest(ListModelsEventName); + } + + /** + * lists azure accounts + */ + public async listAzureAccounts(): Promise { + return await this.sendDataRequest(ListAccountsEventName); + } + + /** + * lists azure subscriptions + * @param account azure account + */ + public async listAzureSubscriptions(account: azdata.Account | undefined): Promise { + const args: AzureResourceEventArgs = { + account: account + }; + return await this.sendDataRequest(ListSubscriptionsEventName, args); + } + + /** + * registers local model + * @param localFilePath local file path + */ + public async registerLocalModel(localFilePath: string | undefined): Promise { + const args: RegisterLocalModelEventArgs = { + filePath: localFilePath + }; + return await this.sendDataRequest(RegisterLocalModelEventName, args); + } + + /** + * registers azure model + * @param args azure resource + */ + public async registerAzureModel(args: RegisterAzureModelEventArgs | undefined): Promise { + return await this.sendDataRequest(RegisterAzureModelEventName, args); + } + + /** + * list resource groups + * @param account azure account + * @param subscription azure subscription + */ + public async listAzureGroups(account: azdata.Account | undefined, subscription: azureResource.AzureResourceSubscription | undefined): Promise { + const args: AzureResourceEventArgs = { + account: account, + subscription: subscription + }; + return await this.sendDataRequest(ListGroupsEventName, args); + } + + /** + * lists azure workspaces + * @param account azure account + * @param subscription azure subscription + * @param group azure resource group + */ + public async listWorkspaces(account: azdata.Account | undefined, subscription: azureResource.AzureResourceSubscription | undefined, group: azureResource.AzureResource | undefined): Promise { + const args: AzureResourceEventArgs = { + account: account, + subscription: subscription, + group: group + }; + return await this.sendDataRequest(ListWorkspacesEventName, args); + } +} diff --git a/extensions/machine-learning-services/src/views/models/registerModelWizard.ts b/extensions/machine-learning-services/src/views/models/registerModelWizard.ts new file mode 100644 index 0000000000..96255ca40c --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/registerModelWizard.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ModelViewBase } from './modelViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { ModelSourcesComponent, ModelSourceType } from './modelSourcesComponent'; +import { LocalModelsComponent } from './localModelsComponent'; +import { AzureModelsComponent } from './azureModelsComponent'; +import * as constants from '../../common/constants'; +import { WizardView } from '../wizardView'; + +/** + * Wizard to register a model + */ +export class RegisterModelWizard extends ModelViewBase { + + public modelResources: ModelSourcesComponent | undefined; + public localModelsComponent: LocalModelsComponent | undefined; + public azureModelsComponent: AzureModelsComponent | undefined; + public wizardView: WizardView | undefined; + private _parentView: ModelViewBase | undefined; + + constructor( + apiWrapper: ApiWrapper, + root: string, + parent?: ModelViewBase) { + super(apiWrapper, root); + this._parentView = parent; + } + + /** + * Opens a dialog to manage packages used by notebooks. + */ + public open(): void { + + this.modelResources = new ModelSourcesComponent(this._apiWrapper, this); + this.localModelsComponent = new LocalModelsComponent(this._apiWrapper, this); + this.azureModelsComponent = new AzureModelsComponent(this._apiWrapper, this); + + this.wizardView = new WizardView(this._apiWrapper); + + let wizard = this.wizardView.createWizard(constants.registerModelWizardTitle, [this.modelResources, this.localModelsComponent]); + this.mainViewPanel = wizard; + wizard.doneButton.label = constants.azureRegisterModel; + wizard.generateScriptButton.hidden = true; + + wizard.registerNavigationValidator(async (pageInfo: azdata.window.WizardPageChangeInfo) => { + if (pageInfo.newPage === undefined) { + await this.registerModel(); + if (this._parentView) { + this._parentView?.refresh(); + } + return true; + + } + return true; + }); + + wizard.open(); + } + + private async registerModel(): Promise { + try { + if (this.modelResources && this.localModelsComponent && this.modelResources.data === ModelSourceType.Local) { + await this.registerLocalModel(this.localModelsComponent.data); + } else { + await this.registerAzureModel(this.azureModelsComponent?.data); + } + this.showInfoMessage(constants.modelRegisteredSuccessfully); + return true; + } catch (error) { + this.showErrorMessage(`${constants.modelFailedToRegister} ${constants.getErrorMessage(error)}`); + return false; + } + } + + private loadPages(): void { + if (this.modelResources && this.localModelsComponent && this.modelResources.data === ModelSourceType.Local) { + this.wizardView?.addWizardPage(this.localModelsComponent, 1); + + } else if (this.azureModelsComponent) { + this.wizardView?.addWizardPage(this.azureModelsComponent, 1); + } + } + + /** + * Refresh the pages + */ + public async refresh(): Promise { + this.loadPages(); + this.wizardView?.refresh(); + } +} diff --git a/extensions/machine-learning-services/src/views/models/registeredModelsDialog.ts b/extensions/machine-learning-services/src/views/models/registeredModelsDialog.ts new file mode 100644 index 0000000000..aeb6bdbde4 --- /dev/null +++ b/extensions/machine-learning-services/src/views/models/registeredModelsDialog.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CurrentModelsPage } from './currentModelsPage'; + +import { ModelViewBase } from './modelViewBase'; +import * as constants from '../../common/constants'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { DialogView } from '../dialogView'; + +/** + * Dialog to render registered model views + */ +export class RegisteredModelsDialog extends ModelViewBase { + + constructor( + apiWrapper: ApiWrapper, + root: string) { + super(apiWrapper, root); + this.dialogView = new DialogView(this._apiWrapper); + } + public dialogView: DialogView; + public currentLanguagesTab: CurrentModelsPage | undefined; + + /** + * Opens a dialog to manage packages used by notebooks. + */ + public open(): void { + + this.currentLanguagesTab = new CurrentModelsPage(this._apiWrapper, this); + + let dialog = this.dialogView.createDialog('', [this.currentLanguagesTab]); + this.mainViewPanel = dialog; + dialog.okButton.hidden = true; + dialog.cancelButton.label = constants.extLangDoneButtonText; + + dialog.registerCloseValidator(() => { + return false; // Blocks Enter key from closing dialog. + }); + + this._apiWrapper.openDialog(dialog); + } + + /** + * Resets the tabs for given provider Id + */ + public async refresh(): Promise { + if (this.dialogView) { + this.dialogView.refresh(); + } + } +} diff --git a/extensions/machine-learning-services/src/views/viewBase.ts b/extensions/machine-learning-services/src/views/viewBase.ts new file mode 100644 index 0000000000..912a9781e3 --- /dev/null +++ b/extensions/machine-learning-services/src/views/viewBase.ts @@ -0,0 +1,198 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../common/constants'; +import { ApiWrapper } from '../common/apiWrapper'; +import * as path from 'path'; +import { EventEmitterCollection } from '../common/eventEmitter'; + +export interface CallbackEventArgs { + data?: any; + error?: (reason?: any) => void; +} + + +export interface CallbackEventArgs { + data?: any; + error?: (reason?: any) => void; +} + +export const CallEventNamePostfix = 'Callback'; +export const LocalFileEventName = 'localFile'; +export const LocalFolderEventName = 'localFolder'; + +/** + * Base class for views + */ +export abstract class ViewBase extends EventEmitterCollection { + protected _mainViewPanel: azdata.window.Dialog | azdata.window.Wizard | undefined; + public viewPanel: azdata.window.ModelViewPanel | undefined; + public connection: azdata.connection.ConnectionProfile | undefined; + public connectionUrl: string = ''; + + public componentMaxLength = 350; + public buttonMaxLength = 150; + public browseButtonMaxLength = 20; + public spaceBetweenComponentsLength = 10; + + constructor(protected _apiWrapper: ApiWrapper, protected _root?: string, protected _parent?: ViewBase) { + super(); + if (this._parent) { + if (!this._root) { + this._root = this._parent.root; + } + this.connection = this._parent.connection; + this.connectionUrl = this._parent.connectionUrl; + } + this.registerEvents(); + } + + protected getEventNames(): string[] { + return [LocalFolderEventName, LocalFileEventName]; + } + + protected getCallbackEventNames(): string[] { + return this.getEventNames().map(eventName => { + return ViewBase.getCallbackEventName(eventName); + }); + } + + public static getCallbackEventName(eventName: string) { + return `${eventName}${CallEventNamePostfix}`; + } + + protected registerEvents() { + if (this._parent) { + const events = this.getEventNames(); + if (events) { + events.forEach(eventName => { + this.on(eventName, (arg) => { + this._parent?.sendRequest(eventName, arg); + }); + + }); + } + const callbackEvents = this.getCallbackEventNames(); + if (callbackEvents) { + callbackEvents.forEach(eventName => { + this._parent?.on(eventName, (arg) => { + this.sendRequest(eventName, arg); + }); + }); + } + } + } + + public sendRequest(requestType: string, arg?: any) { + this.fire(requestType, arg); + } + + public sendCallbackRequest(requestType: string, arg: CallbackEventArgs) { + this.fire(requestType, arg); + } + + public sendDataRequest( + eventName: string, + arg?: any, + callbackEventName?: string): Promise { + return new Promise((resolve, reject) => { + if (!callbackEventName) { + callbackEventName = ViewBase.getCallbackEventName(eventName); + } + this.on(callbackEventName, result => { + let callbackArgs = result; + if (callbackArgs) { + if (callbackArgs.error) { + reject(callbackArgs.error); + } else { + resolve(callbackArgs.data); + } + } else { + reject(constants.notSupportedEventArg); + } + }); + this.fire(eventName, arg); + }); + } + + public async getLocalFilePath(): Promise { + return await this.sendDataRequest(LocalFileEventName); + } + + public async getLocalFolderPath(): Promise { + return await this.sendDataRequest(LocalFolderEventName); + } + + public async getLocationTitle(): Promise { + let connection = await this.getCurrentConnection(); + if (connection) { + return `${connection.serverName} ${connection.databaseName ? connection.databaseName : ''}`; + } + return constants.packageManagerNoConnection; + } + + public getServerTitle(): string { + if (this.connection) { + return this.connection.serverName; + } + return constants.packageManagerNoConnection; + } + + private async getCurrentConnectionUrl(): Promise { + let connection = await this.getCurrentConnection(); + if (connection) { + return await this._apiWrapper.getUriForConnection(connection.connectionId); + } + return ''; + } + + private async getCurrentConnection(): Promise { + return await this._apiWrapper.getCurrentConnection(); + } + + public async loadConnection(): Promise { + this.connection = await this.getCurrentConnection(); + this.connectionUrl = await this.getCurrentConnectionUrl(); + } + + /** + * Dialog model instance + */ + public get mainViewPanel(): azdata.window.Dialog | azdata.window.Wizard | undefined { + return this._mainViewPanel || this._parent?.mainViewPanel; + } + + public set mainViewPanel(value: azdata.window.Dialog | azdata.window.Wizard | undefined) { + this._mainViewPanel = value; + } + + public showInfoMessage(message: string): void { + this.showMessage(message, azdata.window.MessageLevel.Information); + } + + public showErrorMessage(message: string, error?: any): void { + this.showMessage(`${message} ${constants.getErrorMessage(error)}`, azdata.window.MessageLevel.Error); + } + + private showMessage(message: string, level: azdata.window.MessageLevel): void { + if (this._mainViewPanel) { + this._mainViewPanel.message = { + text: message, + level: level + }; + } + } + + public get root(): string { + return this._root || ''; + } + + public asAbsolutePath(filePath: string): string { + return path.join(this._root || '', filePath); + } + + public abstract refresh(): Promise; +} diff --git a/extensions/machine-learning-services/src/views/wizardView.ts b/extensions/machine-learning-services/src/views/wizardView.ts new file mode 100644 index 0000000000..6c1f613ce3 --- /dev/null +++ b/extensions/machine-learning-services/src/views/wizardView.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ApiWrapper } from '../common/apiWrapper'; +import { MainViewBase } from './mainViewBase'; +import { IPageView } from './interfaces'; + +/** + * Wizard view to creates wizard and pages + */ +export class WizardView extends MainViewBase { + + private _wizard: azdata.window.Wizard | undefined; + + /** + * + */ + constructor(apiWrapper: ApiWrapper) { + super(apiWrapper); + } + + private createWizardPage(title: string, componentView: IPageView): azdata.window.WizardPage { + let viewPanel = this._apiWrapper.createWizardPage(title); + this.registerContent(viewPanel, componentView); + componentView.viewPanel = viewPanel; + return viewPanel; + } + + /** + * Adds wizard page + * @param page page + * @param index page index + */ + public addWizardPage(page: IPageView, index: number): void { + if (this._wizard) { + this.addPage(page, index); + this._wizard.removePage(index); + if (!page.viewPanel) { + this.createWizardPage(page.title || '', page); + } + this._wizard.addPage(page.viewPanel, index); + this._wizard.setCurrentPage(index); + } + } + + /** + * + * @param title Creates anew wizard + * @param pages wizard pages + */ + public createWizard(title: string, pages: IPageView[]): azdata.window.Wizard { + this._wizard = this._apiWrapper.createWizard(title); + this._pages = pages; + this._wizard.pages = pages.map(x => this.createWizardPage(x.title || '', x)); + this._wizard.onPageChanged(async (info) => { + this.onWizardPageChanged(info); + }); + return this._wizard; + } + + private onWizardPageChanged(pageInfo: azdata.window.WizardPageChangeInfo) { + let idxLast = pageInfo.lastPage; + let lastPage = this._pages[idxLast]; + if (lastPage && lastPage.onLeave) { + lastPage.onLeave(); + } + + let idx = pageInfo.newPage; + let page = this._pages[idx]; + if (page && page.onEnter) { + page.onEnter(); + } + } +} diff --git a/extensions/machine-learning-services/yarn.lock b/extensions/machine-learning-services/yarn.lock index f83c44f07b..d0d7befdb0 100644 --- a/extensions/machine-learning-services/yarn.lock +++ b/extensions/machine-learning-services/yarn.lock @@ -2,6 +2,37 @@ # yarn lockfile v1 +"@azure/arm-machinelearningservices@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@azure/arm-machinelearningservices/-/arm-machinelearningservices-3.0.0.tgz#02fd0f98bbc24e75aa3fd384fe5125af983d47af" + integrity sha512-An0/K+fay1/fMmn/hW/GNHFiaaGuRs8KRUfJLiOxQkBaSzafYuH2N159lUY+okVvo0JKrhEaOQ1+iQ7G9apZQg== + dependencies: + "@azure/ms-rest-azure-js" "^1.3.2" + "@azure/ms-rest-js" "^1.8.1" + tslib "^1.9.3" + +"@azure/ms-rest-azure-js@^1.3.2": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-js/-/ms-rest-azure-js-1.3.8.tgz#96b518223d3baa2496b2981bc07288b3d887486e" + integrity sha512-AHLfDTCyIH6wBK6+CpImI6sc9mLZ17ZgUrTx3Rhwv+3Mb3Z73BxormkarfR6Stb6scrBYitxJ27FXyndXlGAYg== + dependencies: + "@azure/ms-rest-js" "^1.8.10" + tslib "^1.9.3" + +"@azure/ms-rest-js@^1.8.1", "@azure/ms-rest-js@^1.8.10": + version "1.8.14" + resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-1.8.14.tgz#657fc145db20b6eb3d58d1a2055473aa72eb609d" + integrity sha512-IrCPN22c8RbKWA06ZXuFwwEb15cSnr0zZ6J8Fspp9ns1SSNTERf7hv+gWvTIis1FlwHy42Mfk8hVu0/r3a0AWA== + dependencies: + "@types/tunnel" "0.0.0" + axios "^0.19.0" + form-data "^2.3.2" + tough-cookie "^2.4.3" + tslib "^1.9.2" + tunnel "0.0.6" + uuid "^3.2.1" + xml2js "^0.4.19" + "@babel/code-frame@^7.0.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -117,6 +148,13 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ== +"@types/tunnel@0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.0.tgz#c2a42943ee63c90652a5557b8c4e56cda77f944e" + integrity sha512-FGDp0iBRiBdPjOgjJmn1NH0KDLN+Z8fRmo+9J7XGBhubq1DPrGrbmG4UTlGzrpbCpesMqD0sWkzi27EYkOMHyg== + dependencies: + "@types/node" "*" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -410,6 +448,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^0.19.0: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + bach@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" @@ -820,7 +865,7 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -1311,6 +1356,13 @@ flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -1328,7 +1380,7 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@^2.5.0: +form-data@^2.3.2, form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -2866,6 +2918,11 @@ pm-mocha-jenkins-reporter@^0.2.6: mkdirp "0.5.0" mocha ">=2.0.0" +polly-js@^1.6.3: + version "1.6.5" + resolved "https://registry.yarnpkg.com/polly-js/-/polly-js-1.6.5.tgz#78ebd7e87516eddd5da51db34290b6b8e3332aac" + integrity sha512-gGouufZpvrYBAeGkLJfty/89hAvFYia3lCxufMj066+aT9ZnR1Edfn/cAkY71Y22EcMPGfIKV4Z4iyi/+YauwQ== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2901,6 +2958,11 @@ psl@^1.1.24: resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== +psl@^1.1.28: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -2923,7 +2985,7 @@ punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -3203,6 +3265,11 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + semver-greatest-satisfied-range@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" @@ -3679,6 +3746,14 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" +tough-cookie@^2.4.3: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -3687,7 +3762,7 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" -tslib@^1.8.0, tslib@^1.8.1: +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.2, tslib@^1.9.3: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== @@ -3739,6 +3814,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -3861,6 +3941,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^3.2.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -4054,11 +4139,24 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +xml2js@^0.4.19: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xml@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"